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

Last change on this file since 8602 was 8573, checked in by bastiK, 9 years ago

fixed #11647 - Regression: relation list empty (patch by michael2402)

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