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