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

Last change on this file since 8811 was 8646, checked in by wiktorn, 9 years ago

Move listeners notification outside synchronized sections.

Addresses: #11689

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