001// License: GPL. See LICENSE file for details. 002package org.openstreetmap.josm.gui; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.AlphaComposite; 007import java.awt.Color; 008import java.awt.Dimension; 009import java.awt.Graphics; 010import java.awt.Graphics2D; 011import java.awt.Point; 012import java.awt.Rectangle; 013import java.awt.event.ComponentAdapter; 014import java.awt.event.ComponentEvent; 015import java.awt.event.KeyEvent; 016import java.awt.event.MouseAdapter; 017import java.awt.event.MouseEvent; 018import java.awt.event.MouseMotionListener; 019import java.awt.geom.Area; 020import java.awt.geom.GeneralPath; 021import java.awt.image.BufferedImage; 022import java.beans.PropertyChangeEvent; 023import java.beans.PropertyChangeListener; 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.Comparator; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.concurrent.CopyOnWriteArrayList; 031 032import javax.swing.AbstractButton; 033import javax.swing.ActionMap; 034import javax.swing.InputMap; 035import javax.swing.JFrame; 036import javax.swing.JPanel; 037 038import org.openstreetmap.josm.Main; 039import org.openstreetmap.josm.actions.AutoScaleAction; 040import org.openstreetmap.josm.actions.mapmode.MapMode; 041import org.openstreetmap.josm.data.Bounds; 042import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 043import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 044import org.openstreetmap.josm.data.DataSource; 045import org.openstreetmap.josm.data.SelectionChangedListener; 046import org.openstreetmap.josm.data.ViewportData; 047import org.openstreetmap.josm.data.coor.EastNorth; 048import org.openstreetmap.josm.data.coor.LatLon; 049import org.openstreetmap.josm.data.imagery.ImageryInfo; 050import org.openstreetmap.josm.data.osm.DataSet; 051import org.openstreetmap.josm.data.osm.OsmPrimitive; 052import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 053import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 054import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 055import org.openstreetmap.josm.gui.layer.GpxLayer; 056import org.openstreetmap.josm.gui.layer.ImageryLayer; 057import org.openstreetmap.josm.gui.layer.Layer; 058import org.openstreetmap.josm.gui.layer.MapViewPaintable; 059import org.openstreetmap.josm.gui.layer.OsmDataLayer; 060import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer; 061import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; 062import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker; 063import org.openstreetmap.josm.gui.util.GuiHelper; 064import org.openstreetmap.josm.tools.AudioPlayer; 065import org.openstreetmap.josm.tools.BugReportExceptionHandler; 066import org.openstreetmap.josm.tools.Shortcut; 067import org.openstreetmap.josm.tools.Utils; 068 069/** 070 * This is a component used in the {@link MapFrame} for browsing the map. It use is to 071 * provide the MapMode's enough capabilities to operate.<br><br> 072 * 073 * {@code MapView} holds meta-data about the data set currently displayed, as scale level, 074 * center point viewed, what scrolling mode or editing mode is selected or with 075 * what projection the map is viewed etc..<br><br> 076 * 077 * {@code MapView} is able to administrate several layers. 078 * 079 * @author imi 080 */ 081public class MapView extends NavigatableComponent implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.LayerStateChangeListener { 082 083 /** 084 * Interface to notify listeners of a layer change. 085 * @author imi 086 */ 087 public interface LayerChangeListener { 088 089 /** 090 * Notifies this listener that the active layer has changed. 091 * @param oldLayer The previous active layer 092 * @param newLayer The new activer layer 093 */ 094 void activeLayerChange(Layer oldLayer, Layer newLayer); 095 096 /** 097 * Notifies this listener that a layer has been added. 098 * @param newLayer The new added layer 099 */ 100 void layerAdded(Layer newLayer); 101 102 /** 103 * Notifies this listener that a layer has been removed. 104 * @param oldLayer The old removed layer 105 */ 106 void layerRemoved(Layer oldLayer); 107 } 108 109 public interface EditLayerChangeListener { 110 void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer); 111 } 112 113 public boolean viewportFollowing = false; 114 115 /** 116 * the layer listeners 117 */ 118 private static final CopyOnWriteArrayList<LayerChangeListener> layerChangeListeners = new CopyOnWriteArrayList<>(); 119 private static final CopyOnWriteArrayList<EditLayerChangeListener> editLayerChangeListeners = new CopyOnWriteArrayList<>(); 120 121 /** 122 * Removes a layer change listener 123 * 124 * @param listener the listener. Ignored if null or already registered. 125 */ 126 public static void removeLayerChangeListener(LayerChangeListener listener) { 127 layerChangeListeners.remove(listener); 128 } 129 130 public static void removeEditLayerChangeListener(EditLayerChangeListener listener) { 131 editLayerChangeListeners.remove(listener); 132 } 133 134 /** 135 * Adds a layer change listener 136 * 137 * @param listener the listener. Ignored if null or already registered. 138 */ 139 public static void addLayerChangeListener(LayerChangeListener listener) { 140 if (listener != null) { 141 layerChangeListeners.addIfAbsent(listener); 142 } 143 } 144 145 /** 146 * Adds an edit layer change listener 147 * 148 * @param listener the listener. Ignored if null or already registered. 149 * @param initialFire Fire an edit-layer-changed-event right after adding 150 * the listener in case there is an edit layer present 151 */ 152 public static void addEditLayerChangeListener(EditLayerChangeListener listener, boolean initialFire) { 153 addEditLayerChangeListener(listener); 154 if (initialFire && Main.isDisplayingMapView() && Main.map.mapView.getEditLayer() != null) { 155 fireEditLayerChanged(null, Main.map.mapView.getEditLayer()); 156 } 157 } 158 159 /** 160 * Adds an edit layer change listener 161 * 162 * @param listener the listener. Ignored if null or already registered. 163 */ 164 public static void addEditLayerChangeListener(EditLayerChangeListener listener) { 165 if (listener != null) { 166 editLayerChangeListeners.addIfAbsent(listener); 167 } 168 } 169 170 protected static void fireActiveLayerChanged(Layer oldLayer, Layer newLayer) { 171 for (LayerChangeListener l : layerChangeListeners) { 172 l.activeLayerChange(oldLayer, newLayer); 173 } 174 } 175 176 protected static void fireLayerAdded(Layer newLayer) { 177 for (MapView.LayerChangeListener l : MapView.layerChangeListeners) { 178 l.layerAdded(newLayer); 179 } 180 } 181 182 protected static void fireLayerRemoved(Layer layer) { 183 for (MapView.LayerChangeListener l : MapView.layerChangeListeners) { 184 l.layerRemoved(layer); 185 } 186 } 187 188 protected static void fireEditLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) { 189 for (EditLayerChangeListener l : editLayerChangeListeners) { 190 l.editLayerChanged(oldLayer, newLayer); 191 } 192 } 193 194 /** 195 * A list of all layers currently loaded. 196 */ 197 private final List<Layer> layers = new ArrayList<>(); 198 /** 199 * The play head marker: there is only one of these so it isn't in any specific layer 200 */ 201 public PlayHeadMarker playHeadMarker = null; 202 203 /** 204 * The layer from the layers list that is currently active. 205 */ 206 private Layer activeLayer; 207 208 private OsmDataLayer editLayer; 209 210 /** 211 * The last event performed by mouse. 212 */ 213 public MouseEvent lastMEvent = new MouseEvent(this, 0, 0, 0, 0, 0, 0, false); // In case somebody reads it before first mouse move 214 215 private final List<MapViewPaintable> temporaryLayers = new LinkedList<>(); 216 217 private BufferedImage nonChangedLayersBuffer; 218 private BufferedImage offscreenBuffer; 219 // Layers that wasn't changed since last paint 220 private final List<Layer> nonChangedLayers = new ArrayList<>(); 221 private Layer changedLayer; 222 private int lastViewID; 223 private boolean paintPreferencesChanged = true; 224 private Rectangle lastClipBounds = new Rectangle(); 225 private MapMover mapMover; 226 227 /** 228 * Constructs a new {@code MapView}. 229 * @param contentPane The content pane used to register shortcuts in its 230 * {@link InputMap} and {@link ActionMap} 231 * @param viewportData the initial viewport of the map. Can be null, then 232 * the viewport is derived from the layer data. 233 */ 234 public MapView(final JPanel contentPane, final ViewportData viewportData) { 235 Main.pref.addPreferenceChangeListener(this); 236 final boolean unregisterTab = Shortcut.findShortcut(KeyEvent.VK_TAB, 0)!=null; 237 238 addComponentListener(new ComponentAdapter(){ 239 @Override public void componentResized(ComponentEvent e) { 240 removeComponentListener(this); 241 242 MapSlider zoomSlider = new MapSlider(MapView.this); 243 add(zoomSlider); 244 zoomSlider.setBounds(3, 0, 114, 30); 245 zoomSlider.setFocusTraversalKeysEnabled(!unregisterTab); 246 247 MapScaler scaler = new MapScaler(MapView.this); 248 add(scaler); 249 scaler.setLocation(10,30); 250 251 mapMover = new MapMover(MapView.this, contentPane); 252 if (viewportData != null) { 253 zoomTo(viewportData.getCenter(), viewportData.getScale()); 254 } else { 255 OsmDataLayer layer = getEditLayer(); 256 if (layer != null) { 257 if (!zoomToDataSetBoundingBox(layer.data)) { 258 // no bounding box defined 259 AutoScaleAction.autoScale("data"); 260 } 261 } else { 262 AutoScaleAction.autoScale("layer"); 263 } 264 } 265 } 266 }); 267 268 // listend to selection changes to redraw the map 269 DataSet.addSelectionListener(repaintSelectionChangedListener); 270 271 //store the last mouse action 272 this.addMouseMotionListener(new MouseMotionListener() { 273 @Override public void mouseDragged(MouseEvent e) { 274 mouseMoved(e); 275 } 276 @Override public void mouseMoved(MouseEvent e) { 277 lastMEvent = e; 278 } 279 }); 280 this.addMouseListener(new MouseAdapter() { 281 @Override 282 public void mousePressed(MouseEvent me) { 283 // focus the MapView component when mouse is pressed inside it 284 requestFocus(); 285 } 286 }); 287 288 if (Shortcut.findShortcut(KeyEvent.VK_TAB, 0)!=null) { 289 setFocusTraversalKeysEnabled(false); 290 } 291 } 292 293 // remebered geometry of the component 294 private Dimension oldSize = null; 295 private Point oldLoc = null; 296 297 /* 298 * Call this method to keep map position on screen during next repaint 299 */ 300 public void rememberLastPositionOnScreen() { 301 oldSize = getSize(); 302 oldLoc = getLocationOnScreen(); 303 } 304 305 /** 306 * Adds a GPX layer. A GPX layer is added below the lowest data layer. 307 * 308 * @param layer the GPX layer 309 */ 310 protected void addGpxLayer(GpxLayer layer) { 311 if (layers.isEmpty()) { 312 layers.add(layer); 313 return; 314 } 315 for (int i=layers.size()-1; i>= 0; i--) { 316 if (layers.get(i) instanceof OsmDataLayer) { 317 if (i == layers.size()-1) { 318 layers.add(layer); 319 } else { 320 layers.add(i+1, layer); 321 } 322 return; 323 } 324 } 325 layers.add(0, layer); 326 } 327 328 /** 329 * Add a layer to the current MapView. The layer will be added at topmost 330 * position. 331 * @param layer The layer to add 332 */ 333 public void addLayer(Layer layer) { 334 if (layer instanceof MarkerLayer && playHeadMarker == null) { 335 playHeadMarker = PlayHeadMarker.create(); 336 } 337 338 if (layer instanceof GpxLayer) { 339 addGpxLayer((GpxLayer)layer); 340 } else if (layers.isEmpty()) { 341 layers.add(layer); 342 } else if (layer.isBackgroundLayer()) { 343 int i = 0; 344 for (; i < layers.size(); i++) { 345 if (layers.get(i).isBackgroundLayer()) { 346 break; 347 } 348 } 349 layers.add(i, layer); 350 } else { 351 layers.add(0, layer); 352 } 353 fireLayerAdded(layer); 354 boolean isOsmDataLayer = layer instanceof OsmDataLayer; 355 if (isOsmDataLayer) { 356 ((OsmDataLayer)layer).addLayerStateChangeListener(this); 357 } 358 boolean callSetActiveLayer = isOsmDataLayer || activeLayer == null; 359 if (callSetActiveLayer) { 360 // autoselect the new layer 361 setActiveLayer(layer); // also repaints this MapView 362 } 363 layer.addPropertyChangeListener(this); 364 Main.addProjectionChangeListener(layer); 365 AudioPlayer.reset(); 366 if (!callSetActiveLayer) { 367 repaint(); 368 } 369 } 370 371 @Override 372 protected DataSet getCurrentDataSet() { 373 if (editLayer != null) 374 return editLayer.data; 375 else 376 return null; 377 } 378 379 /** 380 * Replies true if the active layer is drawable. 381 * 382 * @return true if the active layer is drawable, false otherwise 383 */ 384 public boolean isActiveLayerDrawable() { 385 return editLayer != null; 386 } 387 388 /** 389 * Replies true if the active layer is visible. 390 * 391 * @return true if the active layer is visible, false otherwise 392 */ 393 public boolean isActiveLayerVisible() { 394 return isActiveLayerDrawable() && editLayer.isVisible(); 395 } 396 397 /** 398 * Determines the next active data layer according to the following 399 * rules: 400 * <ul> 401 * <li>if there is at least one {@link OsmDataLayer} the first one 402 * becomes active</li> 403 * <li>otherwise, the top most layer of any type becomes active</li> 404 * </ul> 405 * 406 * @return the next active data layer 407 */ 408 protected Layer determineNextActiveLayer(List<Layer> layersList) { 409 // First look for data layer 410 for (Layer layer:layersList) { 411 if (layer instanceof OsmDataLayer) 412 return layer; 413 } 414 415 // Then any layer 416 if (!layersList.isEmpty()) 417 return layersList.get(0); 418 419 // and then give up 420 return null; 421 422 } 423 424 /** 425 * Remove the layer from the mapview. If the layer was in the list before, 426 * an LayerChange event is fired. 427 * @param layer The layer to remove 428 */ 429 public void removeLayer(Layer layer) { 430 List<Layer> layersList = new ArrayList<>(layers); 431 432 if (!layersList.remove(layer)) 433 return; 434 435 setEditLayer(layersList); 436 437 if (layer == activeLayer) { 438 setActiveLayer(determineNextActiveLayer(layersList), false); 439 } 440 441 if (layer instanceof OsmDataLayer) { 442 ((OsmDataLayer)layer).removeLayerPropertyChangeListener(this); 443 } 444 445 layers.remove(layer); 446 Main.removeProjectionChangeListener(layer); 447 fireLayerRemoved(layer); 448 layer.removePropertyChangeListener(this); 449 layer.destroy(); 450 AudioPlayer.reset(); 451 repaint(); 452 } 453 454 private boolean virtualNodesEnabled = false; 455 456 public void setVirtualNodesEnabled(boolean enabled) { 457 if(virtualNodesEnabled != enabled) { 458 virtualNodesEnabled = enabled; 459 repaint(); 460 } 461 } 462 public boolean isVirtualNodesEnabled() { 463 return virtualNodesEnabled; 464 } 465 466 /** 467 * Moves the layer to the given new position. No event is fired, but repaints 468 * according to the new Z-Order of the layers. 469 * 470 * @param layer The layer to move 471 * @param pos The new position of the layer 472 */ 473 public void moveLayer(Layer layer, int pos) { 474 int curLayerPos = layers.indexOf(layer); 475 if (curLayerPos == -1) 476 throw new IllegalArgumentException(tr("Layer not in list.")); 477 if (pos == curLayerPos) 478 return; // already in place. 479 layers.remove(curLayerPos); 480 if (pos >= layers.size()) { 481 layers.add(layer); 482 } else { 483 layers.add(pos, layer); 484 } 485 setEditLayer(layers); 486 AudioPlayer.reset(); 487 repaint(); 488 } 489 490 public int getLayerPos(Layer layer) { 491 int curLayerPos = layers.indexOf(layer); 492 if (curLayerPos == -1) 493 throw new IllegalArgumentException(tr("Layer not in list.")); 494 return curLayerPos; 495 } 496 497 /** 498 * Creates a list of the visible layers in Z-Order, the layer with the lowest Z-Order 499 * first, layer with the highest Z-Order last. 500 * 501 * @return a list of the visible in Z-Order, the layer with the lowest Z-Order 502 * first, layer with the highest Z-Order last. 503 */ 504 protected List<Layer> getVisibleLayersInZOrder() { 505 List<Layer> ret = new ArrayList<>(); 506 for (Layer l: layers) { 507 if (l.isVisible()) { 508 ret.add(l); 509 } 510 } 511 // sort according to position in the list of layers, with one exception: 512 // an active data layer always becomes a higher Z-Order than all other 513 // data layers 514 // 515 Collections.sort( 516 ret, 517 new Comparator<Layer>() { 518 @Override public int compare(Layer l1, Layer l2) { 519 if (l1 instanceof OsmDataLayer && l2 instanceof OsmDataLayer) { 520 if (l1 == getActiveLayer()) return -1; 521 if (l2 == getActiveLayer()) return 1; 522 return Integer.valueOf(layers.indexOf(l1)).compareTo(layers.indexOf(l2)); 523 } else 524 return Integer.valueOf(layers.indexOf(l1)).compareTo(layers.indexOf(l2)); 525 } 526 } 527 ); 528 Collections.reverse(ret); 529 return ret; 530 } 531 532 private void paintLayer(Layer layer, Graphics2D g, Bounds box) { 533 if (layer.getOpacity() < 1) { 534 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,(float)layer.getOpacity())); 535 } 536 layer.paint(g, this, box); 537 g.setPaintMode(); 538 } 539 540 /** 541 * Draw the component. 542 */ 543 @Override public void paint(Graphics g) { 544 if (BugReportExceptionHandler.exceptionHandlingInProgress()) 545 return; 546 547 if (center == null) 548 return; // no data loaded yet. 549 550 // if the position was remembered, we need to adjust center once before repainting 551 if (oldLoc != null && oldSize != null) { 552 Point l1 = getLocationOnScreen(); 553 final EastNorth newCenter = new EastNorth( 554 center.getX()+ (l1.x-oldLoc.x - (oldSize.width-getWidth())/2.0)*getScale(), 555 center.getY()+ (oldLoc.y-l1.y + (oldSize.height-getHeight())/2.0)*getScale() 556 ); 557 oldLoc = null; oldSize = null; 558 zoomTo(newCenter); 559 } 560 561 List<Layer> visibleLayers = getVisibleLayersInZOrder(); 562 563 int nonChangedLayersCount = 0; 564 for (Layer l: visibleLayers) { 565 if (l.isChanged() || l == changedLayer) { 566 break; 567 } else { 568 nonChangedLayersCount++; 569 } 570 } 571 572 boolean canUseBuffer; 573 574 synchronized (this) { 575 canUseBuffer = !paintPreferencesChanged; 576 paintPreferencesChanged = false; 577 } 578 canUseBuffer = canUseBuffer && nonChangedLayers.size() <= nonChangedLayersCount && 579 lastViewID == getViewID() && lastClipBounds.contains(g.getClipBounds()); 580 if (canUseBuffer) { 581 for (int i=0; i<nonChangedLayers.size(); i++) { 582 if (visibleLayers.get(i) != nonChangedLayers.get(i)) { 583 canUseBuffer = false; 584 break; 585 } 586 } 587 } 588 589 if (null == offscreenBuffer || offscreenBuffer.getWidth() != getWidth() || offscreenBuffer.getHeight() != getHeight()) { 590 offscreenBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR); 591 } 592 593 Graphics2D tempG = offscreenBuffer.createGraphics(); 594 tempG.setClip(g.getClip()); 595 Bounds box = getLatLonBounds(g.getClipBounds()); 596 597 if (!canUseBuffer || nonChangedLayersBuffer == null) { 598 if (null == nonChangedLayersBuffer || nonChangedLayersBuffer.getWidth() != getWidth() || nonChangedLayersBuffer.getHeight() != getHeight()) { 599 nonChangedLayersBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR); 600 } 601 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 602 g2.setClip(g.getClip()); 603 g2.setColor(PaintColors.getBackgroundColor()); 604 g2.fillRect(0, 0, getWidth(), getHeight()); 605 606 for (int i=0; i<nonChangedLayersCount; i++) { 607 paintLayer(visibleLayers.get(i),g2, box); 608 } 609 } else { 610 // Maybe there were more unchanged layers then last time - draw them to buffer 611 if (nonChangedLayers.size() != nonChangedLayersCount) { 612 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 613 g2.setClip(g.getClip()); 614 for (int i=nonChangedLayers.size(); i<nonChangedLayersCount; i++) { 615 paintLayer(visibleLayers.get(i),g2, box); 616 } 617 } 618 } 619 620 nonChangedLayers.clear(); 621 changedLayer = null; 622 for (int i=0; i<nonChangedLayersCount; i++) { 623 nonChangedLayers.add(visibleLayers.get(i)); 624 } 625 lastViewID = getViewID(); 626 lastClipBounds = g.getClipBounds(); 627 628 tempG.drawImage(nonChangedLayersBuffer, 0, 0, null); 629 630 for (int i=nonChangedLayersCount; i<visibleLayers.size(); i++) { 631 paintLayer(visibleLayers.get(i),tempG, box); 632 } 633 634 for (MapViewPaintable mvp : temporaryLayers) { 635 mvp.paint(tempG, this, box); 636 } 637 638 // draw world borders 639 tempG.setColor(Color.WHITE); 640 Bounds b = getProjection().getWorldBoundsLatLon(); 641 double lat = b.getMinLat(); 642 double lon = b.getMinLon(); 643 644 Point p = getPoint(b.getMin()); 645 646 GeneralPath path = new GeneralPath(); 647 648 path.moveTo(p.x, p.y); 649 double max = b.getMax().lat(); 650 for(; lat <= max; lat += 1.0) 651 { 652 p = getPoint(new LatLon(lat >= max ? max : lat, lon)); 653 path.lineTo(p.x, p.y); 654 } 655 lat = max; max = b.getMax().lon(); 656 for(; lon <= max; lon += 1.0) 657 { 658 p = getPoint(new LatLon(lat, lon >= max ? max : lon)); 659 path.lineTo(p.x, p.y); 660 } 661 lon = max; max = b.getMinLat(); 662 for(; lat >= max; lat -= 1.0) 663 { 664 p = getPoint(new LatLon(lat <= max ? max : lat, lon)); 665 path.lineTo(p.x, p.y); 666 } 667 lat = max; max = b.getMinLon(); 668 for(; lon >= max; lon -= 1.0) 669 { 670 p = getPoint(new LatLon(lat, lon <= max ? max : lon)); 671 path.lineTo(p.x, p.y); 672 } 673 674 int w = getWidth(); 675 int h = getHeight(); 676 677 // Work around OpenJDK having problems when drawing out of bounds 678 final Area border = new Area(path); 679 // Make the viewport 1px larger in every direction to prevent an 680 // additional 1px border when zooming in 681 final Area viewport = new Area(new Rectangle(-1, -1, w + 2, h + 2)); 682 border.intersect(viewport); 683 tempG.draw(border); 684 685 if (Main.isDisplayingMapView() && Main.map.filterDialog != null) { 686 Main.map.filterDialog.drawOSDText(tempG); 687 } 688 689 if (playHeadMarker != null) { 690 playHeadMarker.paint(tempG, this); 691 } 692 693 g.drawImage(offscreenBuffer, 0, 0, null); 694 super.paint(g); 695 } 696 697 /** 698 * Set the new dimension to the view. 699 */ 700 public void recalculateCenterScale(BoundingXYVisitor box) { 701 if (box == null) { 702 box = new BoundingXYVisitor(); 703 } 704 if (box.getBounds() == null) { 705 box.visit(getProjection().getWorldBoundsLatLon()); 706 } 707 if (!box.hasExtend()) { 708 box.enlargeBoundingBox(); 709 } 710 711 zoomTo(box.getBounds()); 712 } 713 714 /** 715 * @return An unmodifiable collection of all layers 716 */ 717 public Collection<Layer> getAllLayers() { 718 return Collections.unmodifiableCollection(new ArrayList<>(layers)); 719 } 720 721 /** 722 * @return An unmodifiable ordered list of all layers 723 */ 724 public List<Layer> getAllLayersAsList() { 725 return Collections.unmodifiableList(new ArrayList<>(layers)); 726 } 727 728 /** 729 * Replies an unmodifiable list of layers of a certain type. 730 * 731 * Example: 732 * <pre> 733 * List<WMSLayer> wmsLayers = getLayersOfType(WMSLayer.class); 734 * </pre> 735 * 736 * @return an unmodifiable list of layers of a certain type. 737 */ 738 public <T extends Layer> List<T> getLayersOfType(Class<T> ofType) { 739 return new ArrayList<>(Utils.filteredCollection(getAllLayers(), ofType)); 740 } 741 742 /** 743 * Replies the number of layers managed by this mav view 744 * 745 * @return the number of layers managed by this mav view 746 */ 747 public int getNumLayers() { 748 return layers.size(); 749 } 750 751 /** 752 * Replies true if there is at least one layer in this map view 753 * 754 * @return true if there is at least one layer in this map view 755 */ 756 public boolean hasLayers() { 757 return getNumLayers() > 0; 758 } 759 760 private void setEditLayer(List<Layer> layersList) { 761 OsmDataLayer newEditLayer = layersList.contains(editLayer)?editLayer:null; 762 OsmDataLayer oldEditLayer = editLayer; 763 764 // Find new edit layer 765 if (activeLayer != editLayer || !layersList.contains(editLayer)) { 766 if (activeLayer instanceof OsmDataLayer && layersList.contains(activeLayer)) { 767 newEditLayer = (OsmDataLayer) activeLayer; 768 } else { 769 for (Layer layer:layersList) { 770 if (layer instanceof OsmDataLayer) { 771 newEditLayer = (OsmDataLayer) layer; 772 break; 773 } 774 } 775 } 776 } 777 778 // Set new edit layer 779 if (newEditLayer != editLayer) { 780 if (newEditLayer == null) { 781 getCurrentDataSet().setSelected(); 782 } 783 784 editLayer = newEditLayer; 785 fireEditLayerChanged(oldEditLayer, newEditLayer); 786 refreshTitle(); 787 } 788 789 } 790 791 /** 792 * Sets the active layer to <code>layer</code>. If <code>layer</code> is an instance 793 * of {@link OsmDataLayer} also sets {@link #editLayer} to <code>layer</code>. 794 * 795 * @param layer the layer to be activate; must be one of the layers in the list of layers 796 * @exception IllegalArgumentException thrown if layer is not in the lis of layers 797 */ 798 public void setActiveLayer(Layer layer) { 799 setActiveLayer(layer, true); 800 } 801 802 private void setActiveLayer(Layer layer, boolean setEditLayer) { 803 if (layer != null && !layers.contains(layer)) 804 throw new IllegalArgumentException(tr("Layer ''{0}'' must be in list of layers", layer.toString())); 805 806 if (layer == activeLayer) 807 return; 808 809 Layer old = activeLayer; 810 activeLayer = layer; 811 if (setEditLayer) { 812 setEditLayer(layers); 813 } 814 fireActiveLayerChanged(old, layer); 815 816 /* This only makes the buttons look disabled. Disabling the actions as well requires 817 * the user to re-select the tool after i.e. moving a layer. While testing I found 818 * that I switch layers and actions at the same time and it was annoying to mind the 819 * order. This way it works as visual clue for new users */ 820 for (final AbstractButton b: Main.map.allMapModeButtons) { 821 MapMode mode = (MapMode)b.getAction(); 822 if (mode.layerIsSupported(layer)) { 823 Main.registerActionShortcut(mode, mode.getShortcut()); //fix #6876 824 GuiHelper.runInEDTAndWait(new Runnable() { 825 @Override public void run() { 826 b.setEnabled(true); 827 } 828 }); 829 } else { 830 Main.unregisterShortcut(mode.getShortcut()); 831 GuiHelper.runInEDTAndWait(new Runnable() { 832 @Override public void run() { 833 b.setEnabled(false); 834 } 835 }); 836 } 837 } 838 AudioPlayer.reset(); 839 repaint(); 840 } 841 842 /** 843 * Replies the currently active layer 844 * 845 * @return the currently active layer (may be null) 846 */ 847 public Layer getActiveLayer() { 848 return activeLayer; 849 } 850 851 /** 852 * Replies the current edit layer, if any 853 * 854 * @return the current edit layer. May be null. 855 */ 856 public OsmDataLayer getEditLayer() { 857 return editLayer; 858 } 859 860 /** 861 * replies true if the list of layers managed by this map view contain layer 862 * 863 * @param layer the layer 864 * @return true if the list of layers managed by this map view contain layer 865 */ 866 public boolean hasLayer(Layer layer) { 867 return layers.contains(layer); 868 } 869 870 /** 871 * Tries to zoom to the download boundingbox[es] of the current edit layer 872 * (aka {@link OsmDataLayer}). If the edit layer has multiple download bounding 873 * boxes it zooms to a large virtual bounding box containing all smaller ones. 874 * 875 * @return <code>true</code> if a zoom operation has been performed 876 */ 877 public boolean zoomToDataSetBoundingBox(DataSet ds) { 878 // In case we already have an existing data layer ... 879 OsmDataLayer layer= getEditLayer(); 880 if (layer == null) 881 return false; 882 Collection<DataSource> dataSources = ds.dataSources; 883 // ... with bounding box[es] of data loaded from OSM or a file... 884 BoundingXYVisitor bbox = new BoundingXYVisitor(); 885 for (DataSource source : dataSources) { 886 bbox.visit(source.bounds); 887 } 888 if (bbox.hasExtend()) { 889 // ... we zoom to it's bounding box 890 recalculateCenterScale(bbox); 891 return true; 892 } 893 return false; 894 } 895 896 public boolean addTemporaryLayer(MapViewPaintable mvp) { 897 if (temporaryLayers.contains(mvp)) return false; 898 return temporaryLayers.add(mvp); 899 } 900 901 public boolean removeTemporaryLayer(MapViewPaintable mvp) { 902 return temporaryLayers.remove(mvp); 903 } 904 905 @Override 906 public void propertyChange(PropertyChangeEvent evt) { 907 if (evt.getPropertyName().equals(Layer.VISIBLE_PROP)) { 908 repaint(); 909 } else if (evt.getPropertyName().equals(Layer.OPACITY_PROP)) { 910 Layer l = (Layer)evt.getSource(); 911 if (l.isVisible()) { 912 changedLayer = l; 913 repaint(); 914 } 915 } else if (evt.getPropertyName().equals(OsmDataLayer.REQUIRES_SAVE_TO_DISK_PROP) 916 || evt.getPropertyName().equals(OsmDataLayer.REQUIRES_UPLOAD_TO_SERVER_PROP)) { 917 OsmDataLayer layer = (OsmDataLayer)evt.getSource(); 918 if (layer == getEditLayer()) { 919 refreshTitle(); 920 } 921 } 922 } 923 924 protected void refreshTitle() { 925 if (Main.parent != null) { 926 boolean dirty = editLayer != null && 927 (editLayer.requiresSaveToFile() || (editLayer.requiresUploadToServer() && !editLayer.isUploadDiscouraged())); 928 ((JFrame) Main.parent).setTitle((dirty ? "* " : "") + tr("Java OpenStreetMap Editor")); 929 ((JFrame) Main.parent).getRootPane().putClientProperty("Window.documentModified", dirty); 930 } 931 } 932 933 @Override 934 public void preferenceChanged(PreferenceChangeEvent e) { 935 synchronized (this) { 936 paintPreferencesChanged = true; 937 } 938 } 939 940 private SelectionChangedListener repaintSelectionChangedListener = new SelectionChangedListener(){ 941 @Override public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 942 repaint(); 943 } 944 }; 945 946 public void destroy() { 947 Main.pref.removePreferenceChangeListener(this); 948 DataSet.removeSelectionListener(repaintSelectionChangedListener); 949 MultipolygonCache.getInstance().clear(this); 950 if (mapMover != null) { 951 mapMover.destroy(); 952 } 953 activeLayer = null; 954 changedLayer = null; 955 editLayer = null; 956 layers.clear(); 957 nonChangedLayers.clear(); 958 temporaryLayers.clear(); 959 } 960 961 @Override 962 public void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue) { 963 if (layer == getEditLayer()) { 964 refreshTitle(); 965 } 966 } 967 968 /** 969 * Get a string representation of all layers suitable for the {@code source} changeset tag. 970 */ 971 public String getLayerInformationForSourceTag() { 972 final Collection<String> layerInfo = new ArrayList<>(); 973 if (!getLayersOfType(GpxLayer.class).isEmpty()) { 974 // no i18n for international values 975 layerInfo.add("survey"); 976 } 977 for (final GeoImageLayer i : getLayersOfType(GeoImageLayer.class)) { 978 layerInfo.add(i.getName()); 979 } 980 for (final ImageryLayer i : getLayersOfType(ImageryLayer.class)) { 981 layerInfo.add(ImageryInfo.ImageryType.BING.equals(i.getInfo().getImageryType()) ? "Bing" : i.getName()); 982 } 983 return Utils.join("; ", layerInfo); 984 } 985}