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

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

New nicer map slider.

Patch submitted by: michael2402

Closes: #12644

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