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