source: josm/trunk/src/org/openstreetmap/josm/gui/MapView.java@ 9870

Last change on this file since 9870 was 9818, checked in by wiktorn, 8 years ago

Snap scale to mercator zoom levels.

See #12350

Patch submitted by: kolesar

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