001// License: GPL. For details, see LICENSE file. 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.Arrays; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.EnumSet; 029import java.util.LinkedHashSet; 030import java.util.List; 031import java.util.ListIterator; 032import java.util.Set; 033import java.util.concurrent.CopyOnWriteArrayList; 034 035import javax.swing.AbstractButton; 036import javax.swing.ActionMap; 037import javax.swing.InputMap; 038import javax.swing.JComponent; 039import javax.swing.JFrame; 040import javax.swing.JPanel; 041 042import org.openstreetmap.josm.Main; 043import org.openstreetmap.josm.actions.mapmode.MapMode; 044import org.openstreetmap.josm.data.Bounds; 045import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 046import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 047import org.openstreetmap.josm.data.SelectionChangedListener; 048import org.openstreetmap.josm.data.ViewportData; 049import org.openstreetmap.josm.data.coor.EastNorth; 050import org.openstreetmap.josm.data.coor.LatLon; 051import org.openstreetmap.josm.data.imagery.ImageryInfo; 052import org.openstreetmap.josm.data.osm.DataSet; 053import org.openstreetmap.josm.data.osm.OsmPrimitive; 054import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 055import org.openstreetmap.josm.data.osm.visitor.paint.Rendering; 056import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 057import org.openstreetmap.josm.gui.layer.GpxLayer; 058import org.openstreetmap.josm.gui.layer.ImageryLayer; 059import org.openstreetmap.josm.gui.layer.Layer; 060import org.openstreetmap.josm.gui.layer.MapViewPaintable; 061import org.openstreetmap.josm.gui.layer.OsmDataLayer; 062import org.openstreetmap.josm.gui.layer.NativeScaleLayer; 063import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer; 064import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; 065import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker; 066import org.openstreetmap.josm.gui.util.GuiHelper; 067import org.openstreetmap.josm.tools.AudioPlayer; 068import org.openstreetmap.josm.tools.BugReportExceptionHandler; 069import org.openstreetmap.josm.tools.Shortcut; 070import org.openstreetmap.josm.tools.Utils; 071 072/** 073 * This is a component used in the {@link MapFrame} for browsing the map. It use is to 074 * provide the MapMode's enough capabilities to operate.<br><br> 075 * 076 * {@code MapView} holds meta-data about the data set currently displayed, as scale level, 077 * center point viewed, what scrolling mode or editing mode is selected or with 078 * what projection the map is viewed etc..<br><br> 079 * 080 * {@code MapView} is able to administrate several layers. 081 * 082 * @author imi 083 */ 084public class MapView extends NavigatableComponent 085implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.LayerStateChangeListener { 086 087 /** 088 * Interface to notify listeners of a layer change. 089 * @author imi 090 */ 091 public interface LayerChangeListener { 092 093 /** 094 * Notifies this listener that the active layer has changed. 095 * @param oldLayer The previous active layer 096 * @param newLayer The new activer layer 097 */ 098 void activeLayerChange(Layer oldLayer, Layer newLayer); 099 100 /** 101 * Notifies this listener that a layer has been added. 102 * @param newLayer The new added layer 103 */ 104 void layerAdded(Layer newLayer); 105 106 /** 107 * Notifies this listener that a layer has been removed. 108 * @param oldLayer The old removed layer 109 */ 110 void layerRemoved(Layer oldLayer); 111 } 112 113 /** 114 * An interface that needs to be implemented in order to listen for changes to the active edit layer. 115 */ 116 public interface EditLayerChangeListener { 117 118 /** 119 * Called after the active edit layer was changed. 120 * @param oldLayer The old edit layer 121 * @param newLayer The current (new) edit layer 122 */ 123 void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer); 124 } 125 126 public boolean viewportFollowing; 127 128 /** 129 * the layer listeners 130 */ 131 private static final CopyOnWriteArrayList<LayerChangeListener> layerChangeListeners = new CopyOnWriteArrayList<>(); 132 private static final CopyOnWriteArrayList<EditLayerChangeListener> editLayerChangeListeners = new CopyOnWriteArrayList<>(); 133 134 /** 135 * Removes a layer change listener 136 * 137 * @param listener the listener. Ignored if null or already registered. 138 */ 139 public static void removeLayerChangeListener(LayerChangeListener listener) { 140 layerChangeListeners.remove(listener); 141 } 142 143 public static void removeEditLayerChangeListener(EditLayerChangeListener listener) { 144 editLayerChangeListeners.remove(listener); 145 } 146 147 /** 148 * Adds a layer change listener 149 * 150 * @param listener the listener. Ignored if null or already registered. 151 */ 152 public static void addLayerChangeListener(LayerChangeListener listener) { 153 if (listener != null) { 154 layerChangeListeners.addIfAbsent(listener); 155 } 156 } 157 158 /** 159 * Adds a layer change listener 160 * 161 * @param listener the listener. Ignored if null or already registered. 162 * @param initialFire fire an active-layer-changed-event right after adding 163 * the listener in case there is a layer present (should be) 164 */ 165 public static void addLayerChangeListener(LayerChangeListener listener, boolean initialFire) { 166 addLayerChangeListener(listener); 167 if (initialFire && Main.isDisplayingMapView()) { 168 listener.activeLayerChange(null, Main.map.mapView.getActiveLayer()); 169 } 170 } 171 172 /** 173 * Adds an edit layer change listener 174 * 175 * @param listener the listener. Ignored if null or already registered. 176 * @param initialFire fire an edit-layer-changed-event right after adding 177 * the listener in case there is an edit layer present 178 */ 179 public static void addEditLayerChangeListener(EditLayerChangeListener listener, boolean initialFire) { 180 addEditLayerChangeListener(listener); 181 if (initialFire && Main.isDisplayingMapView() && Main.map.mapView.getEditLayer() != null) { 182 listener.editLayerChanged(null, Main.map.mapView.getEditLayer()); 183 } 184 } 185 186 /** 187 * Adds an edit layer change listener 188 * 189 * @param listener the listener. Ignored if null or already registered. 190 */ 191 public static void addEditLayerChangeListener(EditLayerChangeListener listener) { 192 if (listener != null) { 193 editLayerChangeListeners.addIfAbsent(listener); 194 } 195 } 196 197 /** 198 * Calls the {@link LayerChangeListener#activeLayerChange(Layer, Layer)} method of all listeners. 199 * 200 * @param oldLayer The old layer 201 * @param newLayer The new active layer. 202 */ 203 protected void fireActiveLayerChanged(Layer oldLayer, Layer newLayer) { 204 for (LayerChangeListener l : layerChangeListeners) { 205 l.activeLayerChange(oldLayer, newLayer); 206 } 207 } 208 209 protected void fireLayerAdded(Layer newLayer) { 210 for (MapView.LayerChangeListener l : MapView.layerChangeListeners) { 211 l.layerAdded(newLayer); 212 } 213 } 214 215 protected void fireLayerRemoved(Layer layer) { 216 for (MapView.LayerChangeListener l : MapView.layerChangeListeners) { 217 l.layerRemoved(layer); 218 } 219 } 220 221 protected void fireEditLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) { 222 for (EditLayerChangeListener l : editLayerChangeListeners) { 223 l.editLayerChanged(oldLayer, newLayer); 224 } 225 } 226 227 /** 228 * A list of all layers currently loaded. 229 */ 230 private final transient List<Layer> layers = new ArrayList<>(); 231 232 /** 233 * The play head marker: there is only one of these so it isn't in any specific layer 234 */ 235 public transient PlayHeadMarker playHeadMarker; 236 237 /** 238 * The layer from the layers list that is currently active. 239 */ 240 private transient Layer activeLayer; 241 242 /** 243 * The edit layer is the current active data layer. 244 */ 245 private transient OsmDataLayer editLayer; 246 247 /** 248 * The last event performed by mouse. 249 */ 250 public MouseEvent lastMEvent = new MouseEvent(this, 0, 0, 0, 0, 0, 0, false); // In case somebody reads it before first mouse move 251 252 /** 253 * Temporary layers (selection rectangle, etc.) that are never cached and 254 * drawn on top of regular layers. 255 * Access must be synchronized. 256 */ 257 private final transient Set<MapViewPaintable> temporaryLayers = new LinkedHashSet<>(); 258 259 private transient BufferedImage nonChangedLayersBuffer; 260 private transient BufferedImage offscreenBuffer; 261 // Layers that wasn't changed since last paint 262 private final transient List<Layer> nonChangedLayers = new ArrayList<>(); 263 private transient Layer changedLayer; 264 private int lastViewID; 265 private boolean paintPreferencesChanged = true; 266 private Rectangle lastClipBounds = new Rectangle(); 267 private transient MapMover mapMover; 268 269 /** 270 * Constructs a new {@code MapView}. 271 * @param contentPane The content pane used to register shortcuts in its 272 * {@link InputMap} and {@link ActionMap} 273 * @param viewportData the initial viewport of the map. Can be null, then 274 * the viewport is derived from the layer data. 275 */ 276 public MapView(final JPanel contentPane, final ViewportData viewportData) { 277 initialViewport = viewportData; 278 Main.pref.addPreferenceChangeListener(this); 279 280 addComponentListener(new ComponentAdapter() { 281 @Override public void componentResized(ComponentEvent e) { 282 removeComponentListener(this); 283 284 for (JComponent c : getMapNavigationComponents(MapView.this)) { 285 MapView.this.add(c); 286 } 287 288 mapMover = new MapMover(MapView.this, contentPane); 289 } 290 }); 291 292 // listend to selection changes to redraw the map 293 DataSet.addSelectionListener(repaintSelectionChangedListener); 294 295 //store the last mouse action 296 this.addMouseMotionListener(new MouseMotionListener() { 297 @Override 298 public void mouseDragged(MouseEvent e) { 299 mouseMoved(e); 300 } 301 302 @Override 303 public void mouseMoved(MouseEvent e) { 304 lastMEvent = e; 305 } 306 }); 307 this.addMouseListener(new MouseAdapter() { 308 @Override 309 public void mousePressed(MouseEvent me) { 310 // focus the MapView component when mouse is pressed inside it 311 requestFocus(); 312 } 313 }); 314 315 if (Shortcut.findShortcut(KeyEvent.VK_TAB, 0) != null) { 316 setFocusTraversalKeysEnabled(false); 317 } 318 } 319 320 /** 321 * Adds the map navigation components to a 322 * @param forMapView The map view to get the components for. 323 * @return A list containing the correctly positioned map navigation components. 324 */ 325 public static List<? extends JComponent> getMapNavigationComponents(MapView forMapView) { 326 MapSlider zoomSlider = new MapSlider(forMapView); 327 zoomSlider.setBounds(3, 0, 114, 30); 328 zoomSlider.setFocusTraversalKeysEnabled(Shortcut.findShortcut(KeyEvent.VK_TAB, 0) == null); 329 330 MapScaler scaler = new MapScaler(forMapView); 331 scaler.setLocation(10, 30); 332 333 return Arrays.asList(zoomSlider, scaler); 334 } 335 336 // remebered geometry of the component 337 private Dimension oldSize; 338 private Point oldLoc; 339 340 /** 341 * Call this method to keep map position on screen during next repaint 342 */ 343 public void rememberLastPositionOnScreen() { 344 oldSize = getSize(); 345 oldLoc = getLocationOnScreen(); 346 } 347 348 /** 349 * Adds a GPX layer. A GPX layer is added below the lowest data layer. 350 * <p> 351 * Does not call {@link #fireLayerAdded(Layer)}. 352 * 353 * @param layer the GPX layer 354 */ 355 protected void addGpxLayer(GpxLayer layer) { 356 synchronized (layers) { 357 if (layers.isEmpty()) { 358 layers.add(layer); 359 return; 360 } 361 for (int i = layers.size()-1; i >= 0; i--) { 362 if (layers.get(i) instanceof OsmDataLayer) { 363 if (i == layers.size()-1) { 364 layers.add(layer); 365 } else { 366 layers.add(i+1, layer); 367 } 368 return; 369 } 370 } 371 layers.add(0, layer); 372 } 373 } 374 375 /** 376 * Add a layer to the current MapView. The layer will be added at topmost 377 * position. 378 * @param layer The layer to add 379 */ 380 public void addLayer(Layer layer) { 381 boolean isOsmDataLayer = layer instanceof OsmDataLayer; 382 EnumSet<LayerListenerType> listenersToFire = EnumSet.noneOf(LayerListenerType.class); 383 Layer oldActiveLayer = activeLayer; 384 OsmDataLayer oldEditLayer = editLayer; 385 386 synchronized (layers) { 387 if (layer instanceof MarkerLayer && playHeadMarker == null) { 388 playHeadMarker = PlayHeadMarker.create(); 389 } 390 391 if (layer instanceof GpxLayer) { 392 addGpxLayer((GpxLayer) layer); 393 } else if (layers.isEmpty()) { 394 layers.add(layer); 395 } else if (layer.isBackgroundLayer()) { 396 int i = 0; 397 for (; i < layers.size(); i++) { 398 if (layers.get(i).isBackgroundLayer()) { 399 break; 400 } 401 } 402 layers.add(i, layer); 403 } else { 404 layers.add(0, layer); 405 } 406 407 if (isOsmDataLayer || oldActiveLayer == null) { 408 // autoselect the new layer 409 listenersToFire.addAll(setActiveLayer(layer, true)); 410 } 411 412 if (isOsmDataLayer) { 413 ((OsmDataLayer) layer).addLayerStateChangeListener(this); 414 } 415 416 if (layer instanceof NativeScaleLayer) { 417 Main.map.mapView.setNativeScaleLayer((NativeScaleLayer) layer); 418 } 419 420 layer.addPropertyChangeListener(this); 421 Main.addProjectionChangeListener(layer); 422 AudioPlayer.reset(); 423 } 424 fireLayerAdded(layer); 425 onActiveEditLayerChanged(oldActiveLayer, oldEditLayer, listenersToFire); 426 427 if (!listenersToFire.isEmpty()) { 428 repaint(); 429 } 430 } 431 432 @Override 433 protected DataSet getCurrentDataSet() { 434 synchronized (layers) { 435 if (editLayer != null) 436 return editLayer.data; 437 else 438 return null; 439 } 440 } 441 442 /** 443 * Replies true if the active data layer (edit layer) is drawable. 444 * 445 * @return true if the active data layer (edit layer) is drawable, false otherwise 446 */ 447 public boolean isActiveLayerDrawable() { 448 synchronized (layers) { 449 return editLayer != null; 450 } 451 } 452 453 /** 454 * Replies true if the active data layer (edit layer) is visible. 455 * 456 * @return true if the active data layer (edit layer) is visible, false otherwise 457 */ 458 public boolean isActiveLayerVisible() { 459 synchronized (layers) { 460 return isActiveLayerDrawable() && editLayer.isVisible(); 461 } 462 } 463 464 /** 465 * Determines the next active data layer according to the following 466 * rules: 467 * <ul> 468 * <li>if there is at least one {@link OsmDataLayer} the first one 469 * becomes active</li> 470 * <li>otherwise, the top most layer of any type becomes active</li> 471 * </ul> 472 * @param layersList lit of layers 473 * 474 * @return the next active data layer 475 */ 476 protected Layer determineNextActiveLayer(List<Layer> layersList) { 477 // First look for data layer 478 for (Layer layer:layersList) { 479 if (layer instanceof OsmDataLayer) 480 return layer; 481 } 482 483 // Then any layer 484 if (!layersList.isEmpty()) 485 return layersList.get(0); 486 487 // and then give up 488 return null; 489 490 } 491 492 /** 493 * Remove the layer from the mapview. If the layer was in the list before, 494 * an LayerChange event is fired. 495 * @param layer The layer to remove 496 */ 497 public void removeLayer(Layer layer) { 498 EnumSet<LayerListenerType> listenersToFire = EnumSet.noneOf(LayerListenerType.class); 499 Layer oldActiveLayer = activeLayer; 500 OsmDataLayer oldEditLayer = editLayer; 501 502 synchronized (layers) { 503 List<Layer> layersList = new ArrayList<>(layers); 504 505 if (!layersList.remove(layer)) 506 return; 507 508 listenersToFire = setEditLayer(layersList); 509 510 if (layer == activeLayer) { 511 listenersToFire.addAll(setActiveLayer(determineNextActiveLayer(layersList), false)); 512 } 513 514 if (layer instanceof OsmDataLayer) { 515 ((OsmDataLayer) layer).removeLayerPropertyChangeListener(this); 516 } 517 518 layers.remove(layer); 519 Main.removeProjectionChangeListener(layer); 520 layer.removePropertyChangeListener(this); 521 layer.destroy(); 522 AudioPlayer.reset(); 523 } 524 onActiveEditLayerChanged(oldActiveLayer, oldEditLayer, listenersToFire); 525 fireLayerRemoved(layer); 526 527 repaint(); 528 } 529 530 private void onEditLayerChanged(OsmDataLayer oldEditLayer) { 531 fireEditLayerChanged(oldEditLayer, editLayer); 532 refreshTitle(); 533 } 534 535 private boolean virtualNodesEnabled; 536 537 public void setVirtualNodesEnabled(boolean enabled) { 538 if (virtualNodesEnabled != enabled) { 539 virtualNodesEnabled = enabled; 540 repaint(); 541 } 542 } 543 544 /** 545 * Checks if virtual nodes should be drawn. Default is <code>false</code> 546 * @return The virtual nodes property. 547 * @see Rendering#render(DataSet, boolean, Bounds) 548 */ 549 public boolean isVirtualNodesEnabled() { 550 return virtualNodesEnabled; 551 } 552 553 /** 554 * Moves the layer to the given new position. No event is fired, but repaints 555 * according to the new Z-Order of the layers. 556 * 557 * @param layer The layer to move 558 * @param pos The new position of the layer 559 */ 560 public void moveLayer(Layer layer, int pos) { 561 EnumSet<LayerListenerType> listenersToFire; 562 Layer oldActiveLayer = activeLayer; 563 OsmDataLayer oldEditLayer = editLayer; 564 565 synchronized (layers) { 566 int curLayerPos = layers.indexOf(layer); 567 if (curLayerPos == -1) 568 throw new IllegalArgumentException(tr("Layer not in list.")); 569 if (pos == curLayerPos) 570 return; // already in place. 571 layers.remove(curLayerPos); 572 if (pos >= layers.size()) { 573 layers.add(layer); 574 } else { 575 layers.add(pos, layer); 576 } 577 listenersToFire = setEditLayer(layers); 578 AudioPlayer.reset(); 579 } 580 onActiveEditLayerChanged(oldActiveLayer, oldEditLayer, listenersToFire); 581 582 repaint(); 583 } 584 585 /** 586 * Gets the index of the layer in the layer list. 587 * @param layer The layer to search for. 588 * @return The index in the list. 589 * @throws IllegalArgumentException if that layer does not belong to this view. 590 */ 591 public int getLayerPos(Layer layer) { 592 int curLayerPos; 593 synchronized (layers) { 594 curLayerPos = layers.indexOf(layer); 595 } 596 if (curLayerPos == -1) 597 throw new IllegalArgumentException(tr("Layer not in list.")); 598 return curLayerPos; 599 } 600 601 /** 602 * Creates a list of the visible layers in Z-Order, the layer with the lowest Z-Order 603 * first, layer with the highest Z-Order last. 604 * <p> 605 * The active data layer is pulled above all adjacent data layers. 606 * 607 * @return a list of the visible in Z-Order, the layer with the lowest Z-Order 608 * first, layer with the highest Z-Order last. 609 */ 610 public List<Layer> getVisibleLayersInZOrder() { 611 synchronized (layers) { 612 List<Layer> ret = new ArrayList<>(); 613 // This is set while we delay the addition of the active layer. 614 boolean activeLayerDelayed = false; 615 for (ListIterator<Layer> iterator = layers.listIterator(layers.size()); iterator.hasPrevious();) { 616 Layer l = iterator.previous(); 617 if (!l.isVisible()) { 618 // ignored 619 } else if (l == activeLayer && l instanceof OsmDataLayer) { 620 // delay and add after the current block of OsmDataLayer 621 activeLayerDelayed = true; 622 } else { 623 if (activeLayerDelayed && !(l instanceof OsmDataLayer)) { 624 // add active layer before the current one. 625 ret.add(activeLayer); 626 activeLayerDelayed = false; 627 } 628 // Add this layer now 629 ret.add(l); 630 } 631 } 632 if (activeLayerDelayed) { 633 ret.add(activeLayer); 634 } 635 return ret; 636 } 637 } 638 639 private void paintLayer(Layer layer, Graphics2D g, Bounds box) { 640 if (layer.getOpacity() < 1) { 641 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) layer.getOpacity())); 642 } 643 layer.paint(g, this, box); 644 g.setPaintMode(); 645 } 646 647 /** 648 * Draw the component. 649 */ 650 @Override 651 public void paint(Graphics g) { 652 if (!prepareToDraw()) { 653 return; 654 } 655 656 List<Layer> visibleLayers = getVisibleLayersInZOrder(); 657 658 int nonChangedLayersCount = 0; 659 for (Layer l: visibleLayers) { 660 if (l.isChanged() || l == changedLayer) { 661 break; 662 } else { 663 nonChangedLayersCount++; 664 } 665 } 666 667 boolean canUseBuffer; 668 669 synchronized (this) { 670 canUseBuffer = !paintPreferencesChanged; 671 paintPreferencesChanged = false; 672 } 673 canUseBuffer = canUseBuffer && nonChangedLayers.size() <= nonChangedLayersCount && 674 lastViewID == getViewID() && lastClipBounds.contains(g.getClipBounds()); 675 if (canUseBuffer) { 676 for (int i = 0; i < nonChangedLayers.size(); i++) { 677 if (visibleLayers.get(i) != nonChangedLayers.get(i)) { 678 canUseBuffer = false; 679 break; 680 } 681 } 682 } 683 684 if (null == offscreenBuffer || offscreenBuffer.getWidth() != getWidth() || offscreenBuffer.getHeight() != getHeight()) { 685 offscreenBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR); 686 } 687 688 Graphics2D tempG = offscreenBuffer.createGraphics(); 689 tempG.setClip(g.getClip()); 690 Bounds box = getLatLonBounds(g.getClipBounds()); 691 692 if (!canUseBuffer || nonChangedLayersBuffer == null) { 693 if (null == nonChangedLayersBuffer 694 || nonChangedLayersBuffer.getWidth() != getWidth() || nonChangedLayersBuffer.getHeight() != getHeight()) { 695 nonChangedLayersBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR); 696 } 697 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 698 g2.setClip(g.getClip()); 699 g2.setColor(PaintColors.getBackgroundColor()); 700 g2.fillRect(0, 0, getWidth(), getHeight()); 701 702 for (int i = 0; i < nonChangedLayersCount; i++) { 703 paintLayer(visibleLayers.get(i), g2, box); 704 } 705 } else { 706 // Maybe there were more unchanged layers then last time - draw them to buffer 707 if (nonChangedLayers.size() != nonChangedLayersCount) { 708 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 709 g2.setClip(g.getClip()); 710 for (int i = nonChangedLayers.size(); i < nonChangedLayersCount; i++) { 711 paintLayer(visibleLayers.get(i), g2, box); 712 } 713 } 714 } 715 716 nonChangedLayers.clear(); 717 changedLayer = null; 718 for (int i = 0; i < nonChangedLayersCount; i++) { 719 nonChangedLayers.add(visibleLayers.get(i)); 720 } 721 lastViewID = getViewID(); 722 lastClipBounds = g.getClipBounds(); 723 724 tempG.drawImage(nonChangedLayersBuffer, 0, 0, null); 725 726 for (int i = nonChangedLayersCount; i < visibleLayers.size(); i++) { 727 paintLayer(visibleLayers.get(i), tempG, box); 728 } 729 730 synchronized (temporaryLayers) { 731 for (MapViewPaintable mvp : temporaryLayers) { 732 mvp.paint(tempG, this, box); 733 } 734 } 735 736 // draw world borders 737 tempG.setColor(Color.WHITE); 738 Bounds b = getProjection().getWorldBoundsLatLon(); 739 double lat = b.getMinLat(); 740 double lon = b.getMinLon(); 741 742 Point p = getPoint(b.getMin()); 743 744 GeneralPath path = new GeneralPath(); 745 746 double d = 1.0; 747 path.moveTo(p.x, p.y); 748 double max = b.getMax().lat(); 749 for (; lat <= max; lat += d) { 750 p = getPoint(new LatLon(lat >= max ? max : lat, lon)); 751 path.lineTo(p.x, p.y); 752 } 753 lat = max; max = b.getMax().lon(); 754 for (; lon <= max; lon += d) { 755 p = getPoint(new LatLon(lat, lon >= max ? max : lon)); 756 path.lineTo(p.x, p.y); 757 } 758 lon = max; max = b.getMinLat(); 759 for (; lat >= max; lat -= d) { 760 p = getPoint(new LatLon(lat <= max ? max : lat, lon)); 761 path.lineTo(p.x, p.y); 762 } 763 lat = max; max = b.getMinLon(); 764 for (; lon >= max; lon -= d) { 765 p = getPoint(new LatLon(lat, lon <= max ? max : lon)); 766 path.lineTo(p.x, p.y); 767 } 768 769 int w = getWidth(); 770 int h = getHeight(); 771 772 // Work around OpenJDK having problems when drawing out of bounds 773 final Area border = new Area(path); 774 // Make the viewport 1px larger in every direction to prevent an 775 // additional 1px border when zooming in 776 final Area viewport = new Area(new Rectangle(-1, -1, w + 2, h + 2)); 777 border.intersect(viewport); 778 tempG.draw(border); 779 780 if (Main.isDisplayingMapView() && Main.map.filterDialog != null) { 781 Main.map.filterDialog.drawOSDText(tempG); 782 } 783 784 if (playHeadMarker != null) { 785 playHeadMarker.paint(tempG, this); 786 } 787 788 g.drawImage(offscreenBuffer, 0, 0, null); 789 super.paint(g); 790 } 791 792 /** 793 * Sets up the viewport to prepare for drawing the view. 794 * @return <code>true</code> if the view can be drawn, <code>false</code> otherwise. 795 */ 796 public boolean prepareToDraw() { 797 if (initialViewport != null) { 798 zoomTo(initialViewport); 799 initialViewport = null; 800 } 801 if (BugReportExceptionHandler.exceptionHandlingInProgress()) 802 return false; 803 804 if (getCenter() == null) 805 return false; // no data loaded yet. 806 807 // if the position was remembered, we need to adjust center once before repainting 808 if (oldLoc != null && oldSize != null) { 809 Point l1 = getLocationOnScreen(); 810 final EastNorth newCenter = new EastNorth( 811 getCenter().getX()+ (l1.x-oldLoc.x - (oldSize.width-getWidth())/2.0)*getScale(), 812 getCenter().getY()+ (oldLoc.y-l1.y + (oldSize.height-getHeight())/2.0)*getScale() 813 ); 814 oldLoc = null; oldSize = null; 815 zoomTo(newCenter); 816 } 817 818 return true; 819 } 820 821 /** 822 * @return An unmodifiable collection of all layers 823 */ 824 public Collection<Layer> getAllLayers() { 825 synchronized (layers) { 826 return Collections.unmodifiableCollection(new ArrayList<>(layers)); 827 } 828 } 829 830 /** 831 * @return An unmodifiable ordered list of all layers 832 */ 833 public List<Layer> getAllLayersAsList() { 834 synchronized (layers) { 835 return Collections.unmodifiableList(new ArrayList<>(layers)); 836 } 837 } 838 839 /** 840 * Replies an unmodifiable list of layers of a certain type. 841 * 842 * Example: 843 * <pre> 844 * List<WMSLayer> wmsLayers = getLayersOfType(WMSLayer.class); 845 * </pre> 846 * @param <T> layer type 847 * 848 * @param ofType The layer type. 849 * @return an unmodifiable list of layers of a certain type. 850 */ 851 public <T extends Layer> List<T> getLayersOfType(Class<T> ofType) { 852 return new ArrayList<>(Utils.filteredCollection(getAllLayers(), ofType)); 853 } 854 855 /** 856 * Replies the number of layers managed by this map view 857 * 858 * @return the number of layers managed by this map view 859 */ 860 public int getNumLayers() { 861 synchronized (layers) { 862 return layers.size(); 863 } 864 } 865 866 /** 867 * Replies true if there is at least one layer in this map view 868 * 869 * @return true if there is at least one layer in this map view 870 */ 871 public boolean hasLayers() { 872 return getNumLayers() > 0; 873 } 874 875 /** 876 * Sets the active edit layer. 877 * <p> 878 * @param layersList A list to select that layer from. 879 * @return A list of change listeners that should be fired using {@link #onActiveEditLayerChanged(Layer, OsmDataLayer, EnumSet)} 880 */ 881 private EnumSet<LayerListenerType> setEditLayer(List<Layer> layersList) { 882 final OsmDataLayer newEditLayer = findNewEditLayer(layersList); 883 884 // Set new edit layer 885 if (newEditLayer != editLayer) { 886 if (newEditLayer == null) { 887 // Note: Unsafe to call while layer write lock is held. 888 getCurrentDataSet().setSelected(); 889 } 890 891 editLayer = newEditLayer; 892 return EnumSet.of(LayerListenerType.EDIT_LAYER_CHANGE); 893 } else { 894 return EnumSet.noneOf(LayerListenerType.class); 895 } 896 897 } 898 899 private OsmDataLayer findNewEditLayer(List<Layer> layersList) { 900 OsmDataLayer newEditLayer = layersList.contains(editLayer) ? editLayer : null; 901 // Find new edit layer 902 if (activeLayer != editLayer || !layersList.contains(editLayer)) { 903 if (activeLayer instanceof OsmDataLayer && layersList.contains(activeLayer)) { 904 newEditLayer = (OsmDataLayer) activeLayer; 905 } else { 906 for (Layer layer:layersList) { 907 if (layer instanceof OsmDataLayer) { 908 newEditLayer = (OsmDataLayer) layer; 909 break; 910 } 911 } 912 } 913 } 914 return newEditLayer; 915 } 916 917 /** 918 * Sets the active layer to <code>layer</code>. If <code>layer</code> is an instance 919 * of {@link OsmDataLayer} also sets {@link #editLayer} to <code>layer</code>. 920 * 921 * @param layer the layer to be activate; must be one of the layers in the list of layers 922 * @throws IllegalArgumentException if layer is not in the list of layers 923 */ 924 public void setActiveLayer(Layer layer) { 925 EnumSet<LayerListenerType> listenersToFire; 926 Layer oldActiveLayer; 927 OsmDataLayer oldEditLayer; 928 929 synchronized (layers) { 930 oldActiveLayer = activeLayer; 931 oldEditLayer = editLayer; 932 listenersToFire = setActiveLayer(layer, true); 933 } 934 onActiveEditLayerChanged(oldActiveLayer, oldEditLayer, listenersToFire); 935 936 repaint(); 937 } 938 939 /** 940 * Sets the active layer. Propagates this change to all map buttons. 941 * @param layer The layer to be active. 942 * @param setEditLayer if this is <code>true</code>, the edit layer is also set. 943 * @return A list of change listeners that should be fired using {@link #onActiveEditLayerChanged(Layer, OsmDataLayer, EnumSet)} 944 */ 945 private EnumSet<LayerListenerType> setActiveLayer(final Layer layer, boolean setEditLayer) { 946 if (layer != null && !layers.contains(layer)) 947 throw new IllegalArgumentException(tr("Layer ''{0}'' must be in list of layers", layer.toString())); 948 949 if (layer == activeLayer) 950 return EnumSet.noneOf(LayerListenerType.class); 951 952 activeLayer = layer; 953 EnumSet<LayerListenerType> listenersToFire = EnumSet.of(LayerListenerType.ACTIVE_LAYER_CHANGE); 954 if (setEditLayer) { 955 listenersToFire.addAll(setEditLayer(layers)); 956 } 957 958 return listenersToFire; 959 } 960 961 /** 962 * Replies the currently active layer 963 * 964 * @return the currently active layer (may be null) 965 */ 966 public Layer getActiveLayer() { 967 synchronized (layers) { 968 return activeLayer; 969 } 970 } 971 972 private enum LayerListenerType { 973 ACTIVE_LAYER_CHANGE, 974 EDIT_LAYER_CHANGE 975 } 976 977 /** 978 * This is called whenever one of active layer/edit layer or both may have been changed, 979 * @param oldActive The old active layer 980 * @param oldEdit The old edit layer. 981 * @param listenersToFire A mask of listeners to fire using {@link LayerListenerType}s 982 */ 983 private void onActiveEditLayerChanged(final Layer oldActive, final OsmDataLayer oldEdit, EnumSet<LayerListenerType> listenersToFire) { 984 if (listenersToFire.contains(LayerListenerType.EDIT_LAYER_CHANGE)) { 985 onEditLayerChanged(oldEdit); 986 } 987 if (listenersToFire.contains(LayerListenerType.ACTIVE_LAYER_CHANGE)) { 988 onActiveLayerChanged(oldActive); 989 } 990 } 991 992 private void onActiveLayerChanged(final Layer old) { 993 fireActiveLayerChanged(old, activeLayer); 994 995 /* This only makes the buttons look disabled. Disabling the actions as well requires 996 * the user to re-select the tool after i.e. moving a layer. While testing I found 997 * that I switch layers and actions at the same time and it was annoying to mind the 998 * order. This way it works as visual clue for new users */ 999 for (final AbstractButton b: Main.map.allMapModeButtons) { 1000 MapMode mode = (MapMode) b.getAction(); 1001 final boolean activeLayerSupported = mode.layerIsSupported(activeLayer); 1002 if (activeLayerSupported) { 1003 Main.registerActionShortcut(mode, mode.getShortcut()); //fix #6876 1004 } else { 1005 Main.unregisterShortcut(mode.getShortcut()); 1006 } 1007 GuiHelper.runInEDTAndWait(new Runnable() { 1008 @Override public void run() { 1009 b.setEnabled(activeLayerSupported); 1010 } 1011 }); 1012 } 1013 AudioPlayer.reset(); 1014 repaint(); 1015 } 1016 1017 /** 1018 * Replies the current edit layer, if any 1019 * 1020 * @return the current edit layer. May be null. 1021 */ 1022 public OsmDataLayer getEditLayer() { 1023 synchronized (layers) { 1024 return editLayer; 1025 } 1026 } 1027 1028 /** 1029 * replies true if the list of layers managed by this map view contain layer 1030 * 1031 * @param layer the layer 1032 * @return true if the list of layers managed by this map view contain layer 1033 */ 1034 public boolean hasLayer(Layer layer) { 1035 synchronized (layers) { 1036 return layers.contains(layer); 1037 } 1038 } 1039 1040 /** 1041 * Adds a new temporary layer. 1042 * <p> 1043 * A temporary layer is a layer that is painted above all normal layers. Layers are painted in the order they are added. 1044 * 1045 * @param mvp The layer to paint. 1046 * @return <code>true</code> if the layer was added. 1047 */ 1048 public boolean addTemporaryLayer(MapViewPaintable mvp) { 1049 synchronized (temporaryLayers) { 1050 return temporaryLayers.add(mvp); 1051 } 1052 } 1053 1054 /** 1055 * Removes a layer previously added as temporary layer. 1056 * @param mvp The layer to remove. 1057 * @return <code>true</code> if that layer was removed. 1058 */ 1059 public boolean removeTemporaryLayer(MapViewPaintable mvp) { 1060 synchronized (temporaryLayers) { 1061 return temporaryLayers.remove(mvp); 1062 } 1063 } 1064 1065 /** 1066 * Gets a list of temporary layers. 1067 * @return The layers in the order they are added. 1068 */ 1069 public List<MapViewPaintable> getTemporaryLayers() { 1070 synchronized (temporaryLayers) { 1071 return Collections.unmodifiableList(new ArrayList<>(temporaryLayers)); 1072 } 1073 } 1074 1075 @Override 1076 public void propertyChange(PropertyChangeEvent evt) { 1077 if (evt.getPropertyName().equals(Layer.VISIBLE_PROP)) { 1078 repaint(); 1079 } else if (evt.getPropertyName().equals(Layer.OPACITY_PROP) || 1080 evt.getPropertyName().equals(Layer.FILTER_STATE_PROP)) { 1081 Layer l = (Layer) evt.getSource(); 1082 if (l.isVisible()) { 1083 changedLayer = l; 1084 repaint(); 1085 } 1086 } else if (evt.getPropertyName().equals(OsmDataLayer.REQUIRES_SAVE_TO_DISK_PROP) 1087 || evt.getPropertyName().equals(OsmDataLayer.REQUIRES_UPLOAD_TO_SERVER_PROP)) { 1088 OsmDataLayer layer = (OsmDataLayer) evt.getSource(); 1089 if (layer == getEditLayer()) { 1090 refreshTitle(); 1091 } 1092 } 1093 } 1094 1095 /** 1096 * Sets the title of the JOSM main window, adding a star if there are dirty layers. 1097 * @see Main#parent 1098 */ 1099 protected void refreshTitle() { 1100 if (Main.parent != null) { 1101 synchronized (layers) { 1102 boolean dirty = editLayer != null && 1103 (editLayer.requiresSaveToFile() || (editLayer.requiresUploadToServer() && !editLayer.isUploadDiscouraged())); 1104 ((JFrame) Main.parent).setTitle((dirty ? "* " : "") + tr("Java OpenStreetMap Editor")); 1105 ((JFrame) Main.parent).getRootPane().putClientProperty("Window.documentModified", dirty); 1106 } 1107 } 1108 } 1109 1110 @Override 1111 public void preferenceChanged(PreferenceChangeEvent e) { 1112 synchronized (this) { 1113 paintPreferencesChanged = true; 1114 } 1115 } 1116 1117 private final transient SelectionChangedListener repaintSelectionChangedListener = new SelectionChangedListener() { 1118 @Override 1119 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 1120 repaint(); 1121 } 1122 }; 1123 1124 public void destroy() { 1125 Main.pref.removePreferenceChangeListener(this); 1126 DataSet.removeSelectionListener(repaintSelectionChangedListener); 1127 MultipolygonCache.getInstance().clear(this); 1128 if (mapMover != null) { 1129 mapMover.destroy(); 1130 } 1131 synchronized (layers) { 1132 activeLayer = null; 1133 changedLayer = null; 1134 editLayer = null; 1135 layers.clear(); 1136 nonChangedLayers.clear(); 1137 } 1138 synchronized (temporaryLayers) { 1139 temporaryLayers.clear(); 1140 } 1141 } 1142 1143 @Override 1144 public void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue) { 1145 if (layer == getEditLayer()) { 1146 refreshTitle(); 1147 } 1148 } 1149 1150 /** 1151 * Get a string representation of all layers suitable for the {@code source} changeset tag. 1152 * @return A String of sources separated by ';' 1153 */ 1154 public String getLayerInformationForSourceTag() { 1155 final Collection<String> layerInfo = new ArrayList<>(); 1156 if (!getLayersOfType(GpxLayer.class).isEmpty()) { 1157 // no i18n for international values 1158 layerInfo.add("survey"); 1159 } 1160 for (final GeoImageLayer i : getLayersOfType(GeoImageLayer.class)) { 1161 if (i.isVisible()) { 1162 layerInfo.add(i.getName()); 1163 } 1164 } 1165 for (final ImageryLayer i : getLayersOfType(ImageryLayer.class)) { 1166 if (i.isVisible()) { 1167 layerInfo.add(ImageryInfo.ImageryType.BING.equals(i.getInfo().getImageryType()) ? "Bing" : i.getName()); 1168 } 1169 } 1170 return Utils.join("; ", layerInfo); 1171 } 1172 1173 /** 1174 * This is a listener that gets informed whenever repaint is called for this MapView. 1175 * <p> 1176 * This is the only safe method to find changes to the map view, since many components call MapView.repaint() directly. 1177 * @author Michael Zangl 1178 */ 1179 public interface RepaintListener { 1180 /** 1181 * Called when any repaint method is called (using default arguments if required). 1182 * @param tm see {@link JComponent#repaint(long, int, int, int, int)} 1183 * @param x see {@link JComponent#repaint(long, int, int, int, int)} 1184 * @param y see {@link JComponent#repaint(long, int, int, int, int)} 1185 * @param width see {@link JComponent#repaint(long, int, int, int, int)} 1186 * @param height see {@link JComponent#repaint(long, int, int, int, int)} 1187 */ 1188 void repaint(long tm, int x, int y, int width, int height); 1189 } 1190 1191 private final transient CopyOnWriteArrayList<RepaintListener> repaintListeners = new CopyOnWriteArrayList<>(); 1192 1193 /** 1194 * Adds a listener that gets informed whenever repaint() is called for this class. 1195 * @param l The listener. 1196 */ 1197 public void addRepaintListener(RepaintListener l) { 1198 repaintListeners.add(l); 1199 } 1200 1201 /** 1202 * Removes a registered repaint listener. 1203 * @param l The listener. 1204 */ 1205 public void removeRepaintListener(RepaintListener l) { 1206 repaintListeners.remove(l); 1207 } 1208 1209 @Override 1210 public void repaint(long tm, int x, int y, int width, int height) { 1211 // This is the main repaint method, all other methods are convenience methods and simply call this method. 1212 // This is just an observation, not a must, but seems to be true for all implementations I found so far. 1213 if (repaintListeners != null) { 1214 // Might get called early in super constructor 1215 for (RepaintListener l : repaintListeners) { 1216 l.repaint(tm, x, y, width, height); 1217 } 1218 } 1219 super.repaint(tm, x, y, width, height); 1220 } 1221}