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.mapmode.MapMode; 040import org.openstreetmap.josm.data.Bounds; 041import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 042import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 043import org.openstreetmap.josm.data.SelectionChangedListener; 044import org.openstreetmap.josm.data.ViewportData; 045import org.openstreetmap.josm.data.coor.EastNorth; 046import org.openstreetmap.josm.data.coor.LatLon; 047import org.openstreetmap.josm.data.imagery.ImageryInfo; 048import org.openstreetmap.josm.data.osm.DataSet; 049import org.openstreetmap.josm.data.osm.OsmPrimitive; 050import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 051import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 052import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 053import org.openstreetmap.josm.gui.layer.GpxLayer; 054import org.openstreetmap.josm.gui.layer.ImageryLayer; 055import org.openstreetmap.josm.gui.layer.Layer; 056import org.openstreetmap.josm.gui.layer.MapViewPaintable; 057import org.openstreetmap.josm.gui.layer.OsmDataLayer; 058import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer; 059import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; 060import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker; 061import org.openstreetmap.josm.gui.util.GuiHelper; 062import org.openstreetmap.josm.tools.AudioPlayer; 063import org.openstreetmap.josm.tools.BugReportExceptionHandler; 064import org.openstreetmap.josm.tools.Shortcut; 065import org.openstreetmap.josm.tools.Utils; 066 067/** 068 * This is a component used in the {@link MapFrame} for browsing the map. It use is to 069 * provide the MapMode's enough capabilities to operate.<br><br> 070 * 071 * {@code MapView} holds meta-data about the data set currently displayed, as scale level, 072 * center point viewed, what scrolling mode or editing mode is selected or with 073 * what projection the map is viewed etc..<br><br> 074 * 075 * {@code MapView} is able to administrate several layers. 076 * 077 * @author imi 078 */ 079public class MapView extends NavigatableComponent implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.LayerStateChangeListener { 080 081 /** 082 * Interface to notify listeners of a layer change. 083 * @author imi 084 */ 085 public interface LayerChangeListener { 086 087 /** 088 * Notifies this listener that the active layer has changed. 089 * @param oldLayer The previous active layer 090 * @param newLayer The new activer layer 091 */ 092 void activeLayerChange(Layer oldLayer, Layer newLayer); 093 094 /** 095 * Notifies this listener that a layer has been added. 096 * @param newLayer The new added layer 097 */ 098 void layerAdded(Layer newLayer); 099 100 /** 101 * Notifies this listener that a layer has been removed. 102 * @param oldLayer The old removed layer 103 */ 104 void layerRemoved(Layer oldLayer); 105 } 106 107 public interface EditLayerChangeListener { 108 void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer); 109 } 110 111 public boolean viewportFollowing = false; 112 113 /** 114 * the layer listeners 115 */ 116 private static final CopyOnWriteArrayList<LayerChangeListener> layerChangeListeners = new CopyOnWriteArrayList<>(); 117 private static final CopyOnWriteArrayList<EditLayerChangeListener> editLayerChangeListeners = new CopyOnWriteArrayList<>(); 118 119 /** 120 * Removes a layer change listener 121 * 122 * @param listener the listener. Ignored if null or already registered. 123 */ 124 public static void removeLayerChangeListener(LayerChangeListener listener) { 125 layerChangeListeners.remove(listener); 126 } 127 128 public static void removeEditLayerChangeListener(EditLayerChangeListener listener) { 129 editLayerChangeListeners.remove(listener); 130 } 131 132 /** 133 * Adds a layer change listener 134 * 135 * @param listener the listener. Ignored if null or already registered. 136 */ 137 public static void addLayerChangeListener(LayerChangeListener listener) { 138 if (listener != null) { 139 layerChangeListeners.addIfAbsent(listener); 140 } 141 } 142 143 /** 144 * Adds a layer change listener 145 * 146 * @param listener the listener. Ignored if null or already registered. 147 * @param initialFire fire an active-layer-changed-event right after adding 148 * the listener in case there is a layer present (should be) 149 */ 150 public static void addLayerChangeListener(LayerChangeListener listener, boolean initialFire) { 151 addLayerChangeListener(listener); 152 if (initialFire && Main.isDisplayingMapView()) { 153 listener.activeLayerChange(null, Main.map.mapView.getActiveLayer()); 154 } 155 } 156 157 /** 158 * Adds an edit layer change listener 159 * 160 * @param listener the listener. Ignored if null or already registered. 161 * @param initialFire fire an edit-layer-changed-event right after adding 162 * the listener in case there is an edit layer present 163 */ 164 public static void addEditLayerChangeListener(EditLayerChangeListener listener, boolean initialFire) { 165 addEditLayerChangeListener(listener); 166 if (initialFire && Main.isDisplayingMapView() && Main.map.mapView.getEditLayer() != null) { 167 listener.editLayerChanged(null, Main.map.mapView.getEditLayer()); 168 } 169 } 170 171 /** 172 * Adds an edit layer change listener 173 * 174 * @param listener the listener. Ignored if null or already registered. 175 */ 176 public static void addEditLayerChangeListener(EditLayerChangeListener listener) { 177 if (listener != null) { 178 editLayerChangeListeners.addIfAbsent(listener); 179 } 180 } 181 182 protected static void fireActiveLayerChanged(Layer oldLayer, Layer newLayer) { 183 for (LayerChangeListener l : layerChangeListeners) { 184 l.activeLayerChange(oldLayer, newLayer); 185 } 186 } 187 188 protected static void fireLayerAdded(Layer newLayer) { 189 for (MapView.LayerChangeListener l : MapView.layerChangeListeners) { 190 l.layerAdded(newLayer); 191 } 192 } 193 194 protected static void fireLayerRemoved(Layer layer) { 195 for (MapView.LayerChangeListener l : MapView.layerChangeListeners) { 196 l.layerRemoved(layer); 197 } 198 } 199 200 protected static void fireEditLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) { 201 for (EditLayerChangeListener l : editLayerChangeListeners) { 202 l.editLayerChanged(oldLayer, newLayer); 203 } 204 } 205 206 /** 207 * A list of all layers currently loaded. 208 */ 209 private final List<Layer> layers = new ArrayList<>(); 210 /** 211 * The play head marker: there is only one of these so it isn't in any specific layer 212 */ 213 public PlayHeadMarker playHeadMarker = null; 214 215 /** 216 * The layer from the layers list that is currently active. 217 */ 218 private Layer activeLayer; 219 220 private OsmDataLayer editLayer; 221 222 /** 223 * The last event performed by mouse. 224 */ 225 public MouseEvent lastMEvent = new MouseEvent(this, 0, 0, 0, 0, 0, 0, false); // In case somebody reads it before first mouse move 226 227 private final List<MapViewPaintable> temporaryLayers = new LinkedList<>(); 228 229 private BufferedImage nonChangedLayersBuffer; 230 private BufferedImage offscreenBuffer; 231 // Layers that wasn't changed since last paint 232 private final List<Layer> nonChangedLayers = new ArrayList<>(); 233 private Layer changedLayer; 234 private int lastViewID; 235 private boolean paintPreferencesChanged = true; 236 private Rectangle lastClipBounds = new Rectangle(); 237 private MapMover mapMover; 238 239 /** 240 * Constructs a new {@code MapView}. 241 * @param contentPane The content pane used to register shortcuts in its 242 * {@link InputMap} and {@link ActionMap} 243 * @param viewportData the initial viewport of the map. Can be null, then 244 * the viewport is derived from the layer data. 245 */ 246 public MapView(final JPanel contentPane, final ViewportData viewportData) { 247 initialViewport = viewportData; 248 Main.pref.addPreferenceChangeListener(this); 249 final boolean unregisterTab = Shortcut.findShortcut(KeyEvent.VK_TAB, 0)!=null; 250 251 addComponentListener(new ComponentAdapter(){ 252 @Override public void componentResized(ComponentEvent e) { 253 removeComponentListener(this); 254 255 MapSlider zoomSlider = new MapSlider(MapView.this); 256 add(zoomSlider); 257 zoomSlider.setBounds(3, 0, 114, 30); 258 zoomSlider.setFocusTraversalKeysEnabled(!unregisterTab); 259 260 MapScaler scaler = new MapScaler(MapView.this); 261 add(scaler); 262 scaler.setLocation(10,30); 263 264 mapMover = new MapMover(MapView.this, contentPane); 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 (initialViewport != null) { 545 zoomTo(initialViewport); 546 initialViewport = null; 547 } 548 if (BugReportExceptionHandler.exceptionHandlingInProgress()) 549 return; 550 551 if (center == null) 552 return; // no data loaded yet. 553 554 // if the position was remembered, we need to adjust center once before repainting 555 if (oldLoc != null && oldSize != null) { 556 Point l1 = getLocationOnScreen(); 557 final EastNorth newCenter = new EastNorth( 558 center.getX()+ (l1.x-oldLoc.x - (oldSize.width-getWidth())/2.0)*getScale(), 559 center.getY()+ (oldLoc.y-l1.y + (oldSize.height-getHeight())/2.0)*getScale() 560 ); 561 oldLoc = null; oldSize = null; 562 zoomTo(newCenter); 563 } 564 565 List<Layer> visibleLayers = getVisibleLayersInZOrder(); 566 567 int nonChangedLayersCount = 0; 568 for (Layer l: visibleLayers) { 569 if (l.isChanged() || l == changedLayer) { 570 break; 571 } else { 572 nonChangedLayersCount++; 573 } 574 } 575 576 boolean canUseBuffer; 577 578 synchronized (this) { 579 canUseBuffer = !paintPreferencesChanged; 580 paintPreferencesChanged = false; 581 } 582 canUseBuffer = canUseBuffer && nonChangedLayers.size() <= nonChangedLayersCount && 583 lastViewID == getViewID() && lastClipBounds.contains(g.getClipBounds()); 584 if (canUseBuffer) { 585 for (int i=0; i<nonChangedLayers.size(); i++) { 586 if (visibleLayers.get(i) != nonChangedLayers.get(i)) { 587 canUseBuffer = false; 588 break; 589 } 590 } 591 } 592 593 if (null == offscreenBuffer || offscreenBuffer.getWidth() != getWidth() || offscreenBuffer.getHeight() != getHeight()) { 594 offscreenBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR); 595 } 596 597 Graphics2D tempG = offscreenBuffer.createGraphics(); 598 tempG.setClip(g.getClip()); 599 Bounds box = getLatLonBounds(g.getClipBounds()); 600 601 if (!canUseBuffer || nonChangedLayersBuffer == null) { 602 if (null == nonChangedLayersBuffer || nonChangedLayersBuffer.getWidth() != getWidth() || nonChangedLayersBuffer.getHeight() != getHeight()) { 603 nonChangedLayersBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR); 604 } 605 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 606 g2.setClip(g.getClip()); 607 g2.setColor(PaintColors.getBackgroundColor()); 608 g2.fillRect(0, 0, getWidth(), getHeight()); 609 610 for (int i=0; i<nonChangedLayersCount; i++) { 611 paintLayer(visibleLayers.get(i),g2, box); 612 } 613 } else { 614 // Maybe there were more unchanged layers then last time - draw them to buffer 615 if (nonChangedLayers.size() != nonChangedLayersCount) { 616 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 617 g2.setClip(g.getClip()); 618 for (int i=nonChangedLayers.size(); i<nonChangedLayersCount; i++) { 619 paintLayer(visibleLayers.get(i),g2, box); 620 } 621 } 622 } 623 624 nonChangedLayers.clear(); 625 changedLayer = null; 626 for (int i=0; i<nonChangedLayersCount; i++) { 627 nonChangedLayers.add(visibleLayers.get(i)); 628 } 629 lastViewID = getViewID(); 630 lastClipBounds = g.getClipBounds(); 631 632 tempG.drawImage(nonChangedLayersBuffer, 0, 0, null); 633 634 for (int i=nonChangedLayersCount; i<visibleLayers.size(); i++) { 635 paintLayer(visibleLayers.get(i),tempG, box); 636 } 637 638 for (MapViewPaintable mvp : temporaryLayers) { 639 mvp.paint(tempG, this, box); 640 } 641 642 // draw world borders 643 tempG.setColor(Color.WHITE); 644 Bounds b = getProjection().getWorldBoundsLatLon(); 645 double lat = b.getMinLat(); 646 double lon = b.getMinLon(); 647 648 Point p = getPoint(b.getMin()); 649 650 GeneralPath path = new GeneralPath(); 651 652 path.moveTo(p.x, p.y); 653 double max = b.getMax().lat(); 654 for(; lat <= max; lat += 1.0) 655 { 656 p = getPoint(new LatLon(lat >= max ? max : lat, lon)); 657 path.lineTo(p.x, p.y); 658 } 659 lat = max; max = b.getMax().lon(); 660 for(; lon <= max; lon += 1.0) 661 { 662 p = getPoint(new LatLon(lat, lon >= max ? max : lon)); 663 path.lineTo(p.x, p.y); 664 } 665 lon = max; max = b.getMinLat(); 666 for(; lat >= max; lat -= 1.0) 667 { 668 p = getPoint(new LatLon(lat <= max ? max : lat, lon)); 669 path.lineTo(p.x, p.y); 670 } 671 lat = max; max = b.getMinLon(); 672 for(; lon >= max; lon -= 1.0) 673 { 674 p = getPoint(new LatLon(lat, lon <= max ? max : lon)); 675 path.lineTo(p.x, p.y); 676 } 677 678 int w = getWidth(); 679 int h = getHeight(); 680 681 // Work around OpenJDK having problems when drawing out of bounds 682 final Area border = new Area(path); 683 // Make the viewport 1px larger in every direction to prevent an 684 // additional 1px border when zooming in 685 final Area viewport = new Area(new Rectangle(-1, -1, w + 2, h + 2)); 686 border.intersect(viewport); 687 tempG.draw(border); 688 689 if (Main.isDisplayingMapView() && Main.map.filterDialog != null) { 690 Main.map.filterDialog.drawOSDText(tempG); 691 } 692 693 if (playHeadMarker != null) { 694 playHeadMarker.paint(tempG, this); 695 } 696 697 g.drawImage(offscreenBuffer, 0, 0, null); 698 super.paint(g); 699 } 700 701 /** 702 * Set the new dimension to the view. 703 * 704 * @deprecated use #zoomTo(BoundingXYVisitor) 705 */ 706 @Deprecated 707 public void recalculateCenterScale(BoundingXYVisitor box) { 708 zoomTo(box); 709 } 710 711 /** 712 * @return An unmodifiable collection of all layers 713 */ 714 public Collection<Layer> getAllLayers() { 715 return Collections.unmodifiableCollection(new ArrayList<>(layers)); 716 } 717 718 /** 719 * @return An unmodifiable ordered list of all layers 720 */ 721 public List<Layer> getAllLayersAsList() { 722 return Collections.unmodifiableList(new ArrayList<>(layers)); 723 } 724 725 /** 726 * Replies an unmodifiable list of layers of a certain type. 727 * 728 * Example: 729 * <pre> 730 * List<WMSLayer> wmsLayers = getLayersOfType(WMSLayer.class); 731 * </pre> 732 * 733 * @return an unmodifiable list of layers of a certain type. 734 */ 735 public <T extends Layer> List<T> getLayersOfType(Class<T> ofType) { 736 return new ArrayList<>(Utils.filteredCollection(getAllLayers(), ofType)); 737 } 738 739 /** 740 * Replies the number of layers managed by this mav view 741 * 742 * @return the number of layers managed by this mav view 743 */ 744 public int getNumLayers() { 745 return layers.size(); 746 } 747 748 /** 749 * Replies true if there is at least one layer in this map view 750 * 751 * @return true if there is at least one layer in this map view 752 */ 753 public boolean hasLayers() { 754 return getNumLayers() > 0; 755 } 756 757 private void setEditLayer(List<Layer> layersList) { 758 OsmDataLayer newEditLayer = layersList.contains(editLayer)?editLayer:null; 759 OsmDataLayer oldEditLayer = editLayer; 760 761 // Find new edit layer 762 if (activeLayer != editLayer || !layersList.contains(editLayer)) { 763 if (activeLayer instanceof OsmDataLayer && layersList.contains(activeLayer)) { 764 newEditLayer = (OsmDataLayer) activeLayer; 765 } else { 766 for (Layer layer:layersList) { 767 if (layer instanceof OsmDataLayer) { 768 newEditLayer = (OsmDataLayer) layer; 769 break; 770 } 771 } 772 } 773 } 774 775 // Set new edit layer 776 if (newEditLayer != editLayer) { 777 if (newEditLayer == null) { 778 getCurrentDataSet().setSelected(); 779 } 780 781 editLayer = newEditLayer; 782 fireEditLayerChanged(oldEditLayer, newEditLayer); 783 refreshTitle(); 784 } 785 786 } 787 788 /** 789 * Sets the active layer to <code>layer</code>. If <code>layer</code> is an instance 790 * of {@link OsmDataLayer} also sets {@link #editLayer} to <code>layer</code>. 791 * 792 * @param layer the layer to be activate; must be one of the layers in the list of layers 793 * @exception IllegalArgumentException thrown if layer is not in the lis of layers 794 */ 795 public void setActiveLayer(Layer layer) { 796 setActiveLayer(layer, true); 797 } 798 799 private void setActiveLayer(Layer layer, boolean setEditLayer) { 800 if (layer != null && !layers.contains(layer)) 801 throw new IllegalArgumentException(tr("Layer ''{0}'' must be in list of layers", layer.toString())); 802 803 if (layer == activeLayer) 804 return; 805 806 Layer old = activeLayer; 807 activeLayer = layer; 808 if (setEditLayer) { 809 setEditLayer(layers); 810 } 811 fireActiveLayerChanged(old, layer); 812 813 /* This only makes the buttons look disabled. Disabling the actions as well requires 814 * the user to re-select the tool after i.e. moving a layer. While testing I found 815 * that I switch layers and actions at the same time and it was annoying to mind the 816 * order. This way it works as visual clue for new users */ 817 for (final AbstractButton b: Main.map.allMapModeButtons) { 818 MapMode mode = (MapMode)b.getAction(); 819 if (mode.layerIsSupported(layer)) { 820 Main.registerActionShortcut(mode, mode.getShortcut()); //fix #6876 821 GuiHelper.runInEDTAndWait(new Runnable() { 822 @Override public void run() { 823 b.setEnabled(true); 824 } 825 }); 826 } else { 827 Main.unregisterShortcut(mode.getShortcut()); 828 GuiHelper.runInEDTAndWait(new Runnable() { 829 @Override public void run() { 830 b.setEnabled(false); 831 } 832 }); 833 } 834 } 835 AudioPlayer.reset(); 836 repaint(); 837 } 838 839 /** 840 * Replies the currently active layer 841 * 842 * @return the currently active layer (may be null) 843 */ 844 public Layer getActiveLayer() { 845 return activeLayer; 846 } 847 848 /** 849 * Replies the current edit layer, if any 850 * 851 * @return the current edit layer. May be null. 852 */ 853 public OsmDataLayer getEditLayer() { 854 return editLayer; 855 } 856 857 /** 858 * replies true if the list of layers managed by this map view contain layer 859 * 860 * @param layer the layer 861 * @return true if the list of layers managed by this map view contain layer 862 */ 863 public boolean hasLayer(Layer layer) { 864 return layers.contains(layer); 865 } 866 867 public boolean addTemporaryLayer(MapViewPaintable mvp) { 868 if (temporaryLayers.contains(mvp)) return false; 869 return temporaryLayers.add(mvp); 870 } 871 872 public boolean removeTemporaryLayer(MapViewPaintable mvp) { 873 return temporaryLayers.remove(mvp); 874 } 875 876 @Override 877 public void propertyChange(PropertyChangeEvent evt) { 878 if (evt.getPropertyName().equals(Layer.VISIBLE_PROP)) { 879 repaint(); 880 } else if (evt.getPropertyName().equals(Layer.OPACITY_PROP)) { 881 Layer l = (Layer)evt.getSource(); 882 if (l.isVisible()) { 883 changedLayer = l; 884 repaint(); 885 } 886 } else if (evt.getPropertyName().equals(OsmDataLayer.REQUIRES_SAVE_TO_DISK_PROP) 887 || evt.getPropertyName().equals(OsmDataLayer.REQUIRES_UPLOAD_TO_SERVER_PROP)) { 888 OsmDataLayer layer = (OsmDataLayer)evt.getSource(); 889 if (layer == getEditLayer()) { 890 refreshTitle(); 891 } 892 } 893 } 894 895 protected void refreshTitle() { 896 if (Main.parent != null) { 897 boolean dirty = editLayer != null && 898 (editLayer.requiresSaveToFile() || (editLayer.requiresUploadToServer() && !editLayer.isUploadDiscouraged())); 899 ((JFrame) Main.parent).setTitle((dirty ? "* " : "") + tr("Java OpenStreetMap Editor")); 900 ((JFrame) Main.parent).getRootPane().putClientProperty("Window.documentModified", dirty); 901 } 902 } 903 904 @Override 905 public void preferenceChanged(PreferenceChangeEvent e) { 906 synchronized (this) { 907 paintPreferencesChanged = true; 908 } 909 } 910 911 private SelectionChangedListener repaintSelectionChangedListener = new SelectionChangedListener(){ 912 @Override public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 913 repaint(); 914 } 915 }; 916 917 public void destroy() { 918 Main.pref.removePreferenceChangeListener(this); 919 DataSet.removeSelectionListener(repaintSelectionChangedListener); 920 MultipolygonCache.getInstance().clear(this); 921 if (mapMover != null) { 922 mapMover.destroy(); 923 } 924 activeLayer = null; 925 changedLayer = null; 926 editLayer = null; 927 layers.clear(); 928 nonChangedLayers.clear(); 929 temporaryLayers.clear(); 930 } 931 932 @Override 933 public void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue) { 934 if (layer == getEditLayer()) { 935 refreshTitle(); 936 } 937 } 938 939 /** 940 * Get a string representation of all layers suitable for the {@code source} changeset tag. 941 */ 942 public String getLayerInformationForSourceTag() { 943 final Collection<String> layerInfo = new ArrayList<>(); 944 if (!getLayersOfType(GpxLayer.class).isEmpty()) { 945 // no i18n for international values 946 layerInfo.add("survey"); 947 } 948 for (final GeoImageLayer i : getLayersOfType(GeoImageLayer.class)) { 949 layerInfo.add(i.getName()); 950 } 951 for (final ImageryLayer i : getLayersOfType(ImageryLayer.class)) { 952 layerInfo.add(ImageryInfo.ImageryType.BING.equals(i.getInfo().getImageryType()) ? "Bing" : i.getName()); 953 } 954 return Utils.join("; ", layerInfo); 955 } 956}