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

Last change on this file since 10063 was 10055, checked in by Don-vip, 8 years ago

fix #12652 - Do not ...snip... bug report messages after 6000 characters (patch by michael2402, modified)

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