001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import java.awt.AlphaComposite;
005import java.awt.Color;
006import java.awt.Dimension;
007import java.awt.Graphics;
008import java.awt.Graphics2D;
009import java.awt.Point;
010import java.awt.Rectangle;
011import java.awt.event.ComponentAdapter;
012import java.awt.event.ComponentEvent;
013import java.awt.event.KeyEvent;
014import java.awt.event.MouseAdapter;
015import java.awt.event.MouseEvent;
016import java.awt.event.MouseMotionListener;
017import java.awt.geom.Area;
018import java.awt.image.BufferedImage;
019import java.beans.PropertyChangeEvent;
020import java.beans.PropertyChangeListener;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.IdentityHashMap;
026import java.util.LinkedHashSet;
027import java.util.List;
028import java.util.Set;
029import java.util.TreeSet;
030import java.util.concurrent.CopyOnWriteArrayList;
031import java.util.concurrent.atomic.AtomicBoolean;
032
033import javax.swing.AbstractButton;
034import javax.swing.JComponent;
035import javax.swing.JPanel;
036
037import org.openstreetmap.josm.Main;
038import org.openstreetmap.josm.actions.mapmode.MapMode;
039import org.openstreetmap.josm.data.Bounds;
040import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
041import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
042import org.openstreetmap.josm.data.ProjectionBounds;
043import org.openstreetmap.josm.data.SelectionChangedListener;
044import org.openstreetmap.josm.data.ViewportData;
045import org.openstreetmap.josm.data.coor.EastNorth;
046import org.openstreetmap.josm.data.imagery.ImageryInfo;
047import org.openstreetmap.josm.data.osm.DataSet;
048import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
049import org.openstreetmap.josm.data.osm.visitor.paint.Rendering;
050import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
051import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;
052import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
053import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
054import org.openstreetmap.josm.gui.layer.GpxLayer;
055import org.openstreetmap.josm.gui.layer.ImageryLayer;
056import org.openstreetmap.josm.gui.layer.Layer;
057import org.openstreetmap.josm.gui.layer.LayerManager;
058import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
059import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
060import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
061import org.openstreetmap.josm.gui.layer.MainLayerManager;
062import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
063import org.openstreetmap.josm.gui.layer.MapViewGraphics;
064import org.openstreetmap.josm.gui.layer.MapViewPaintable;
065import org.openstreetmap.josm.gui.layer.MapViewPaintable.LayerPainter;
066import org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent;
067import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationEvent;
068import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationListener;
069import org.openstreetmap.josm.gui.layer.OsmDataLayer;
070import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
071import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker;
072import org.openstreetmap.josm.tools.AudioPlayer;
073import org.openstreetmap.josm.tools.Shortcut;
074import org.openstreetmap.josm.tools.Utils;
075import org.openstreetmap.josm.tools.bugreport.BugReport;
076
077/**
078 * This is a component used in the {@link MapFrame} for browsing the map. It use is to
079 * provide the MapMode's enough capabilities to operate.<br><br>
080 *
081 * {@code MapView} holds meta-data about the data set currently displayed, as scale level,
082 * center point viewed, what scrolling mode or editing mode is selected or with
083 * what projection the map is viewed etc..<br><br>
084 *
085 * {@code MapView} is able to administrate several layers.
086 *
087 * @author imi
088 */
089public class MapView extends NavigatableComponent
090implements PropertyChangeListener, PreferenceChangedListener,
091LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {
092
093    /**
094     * An invalidation listener that simply calls repaint() for now.
095     * @author Michael Zangl
096     * @since 10271
097     */
098    private class LayerInvalidatedListener implements PaintableInvalidationListener {
099        private boolean ignoreRepaint;
100
101        private final Set<MapViewPaintable> invalidatedLayers = Collections.newSetFromMap(new IdentityHashMap<MapViewPaintable, Boolean>());
102
103        @Override
104        public void paintableInvalidated(PaintableInvalidationEvent event) {
105            invalidate(event.getLayer());
106        }
107
108        /**
109         * Invalidate contents and repaint map view
110         * @param mapViewPaintable invalidated layer
111         */
112        public synchronized void invalidate(MapViewPaintable mapViewPaintable) {
113            ignoreRepaint = true;
114            invalidatedLayers.add(mapViewPaintable);
115            repaint();
116        }
117
118        /**
119         * Temporary until all {@link MapViewPaintable}s support this.
120         * @param p The paintable.
121         */
122        public synchronized void addTo(MapViewPaintable p) {
123            if (p instanceof AbstractMapViewPaintable) {
124                ((AbstractMapViewPaintable) p).addInvalidationListener(this);
125            }
126        }
127
128        /**
129         * Temporary until all {@link MapViewPaintable}s support this.
130         * @param p The paintable.
131         */
132        public synchronized void removeFrom(MapViewPaintable p) {
133            if (p instanceof AbstractMapViewPaintable) {
134                ((AbstractMapViewPaintable) p).removeInvalidationListener(this);
135            }
136            invalidatedLayers.remove(p);
137        }
138
139        /**
140         * Attempts to trace repaints that did not originate from this listener. Good to find missed {@link MapView#repaint()}s in code.
141         */
142        protected synchronized void traceRandomRepaint() {
143            if (!ignoreRepaint) {
144                System.err.println("Repaint:");
145                Thread.dumpStack();
146            }
147            ignoreRepaint = false;
148        }
149
150        /**
151         * Retrieves a set of all layers that have been marked as invalid since the last call to this method.
152         * @return The layers
153         */
154        protected synchronized Set<MapViewPaintable> collectInvalidatedLayers() {
155            Set<MapViewPaintable> layers = Collections.newSetFromMap(new IdentityHashMap<MapViewPaintable, Boolean>());
156            layers.addAll(invalidatedLayers);
157            invalidatedLayers.clear();
158            return layers;
159        }
160    }
161
162    /**
163     * A layer painter that issues a warning when being called.
164     * @author Michael Zangl
165     * @since 10474
166     */
167    private static class WarningLayerPainter implements LayerPainter {
168        boolean warningPrinted;
169        private final Layer layer;
170
171        WarningLayerPainter(Layer layer) {
172            this.layer = layer;
173        }
174
175        @Override
176        public void paint(MapViewGraphics graphics) {
177            if (!warningPrinted) {
178                Main.debug("A layer triggered a repaint while being added: " + layer);
179                warningPrinted = true;
180            }
181        }
182
183        @Override
184        public void detachFromMapView(MapViewEvent event) {
185            // ignored
186        }
187    }
188
189    public boolean viewportFollowing;
190
191    /**
192     * A list of all layers currently loaded. If we support multiple map views, this list may be different for each of them.
193     */
194    private final MainLayerManager layerManager;
195
196    /**
197     * The play head marker: there is only one of these so it isn't in any specific layer
198     */
199    public transient PlayHeadMarker playHeadMarker;
200
201    /**
202     * The last event performed by mouse.
203     */
204    public MouseEvent lastMEvent = new MouseEvent(this, 0, 0, 0, 0, 0, 0, false); // In case somebody reads it before first mouse move
205
206    /**
207     * Temporary layers (selection rectangle, etc.) that are never cached and
208     * drawn on top of regular layers.
209     * Access must be synchronized.
210     */
211    private final transient Set<MapViewPaintable> temporaryLayers = new LinkedHashSet<>();
212
213    private transient BufferedImage nonChangedLayersBuffer;
214    private transient BufferedImage offscreenBuffer;
215    // Layers that wasn't changed since last paint
216    private final transient List<Layer> nonChangedLayers = new ArrayList<>();
217    private int lastViewID;
218    private final AtomicBoolean paintPreferencesChanged = new AtomicBoolean(true);
219    private Rectangle lastClipBounds = new Rectangle();
220    private transient MapMover mapMover;
221
222    /**
223     * The listener that listens to invalidations of all layers.
224     */
225    private final LayerInvalidatedListener invalidatedListener = new LayerInvalidatedListener();
226
227    /**
228     * This is a map of all Layers that have been added to this view.
229     */
230    private final HashMap<Layer, LayerPainter> registeredLayers = new HashMap<>();
231
232    /**
233     * Constructs a new {@code MapView}.
234     * @param layerManager The layers to display.
235     * @param contentPane Ignored. Main content pane is used.
236     * @param viewportData the initial viewport of the map. Can be null, then
237     * the viewport is derived from the layer data.
238     * @since 10279
239     */
240    public MapView(MainLayerManager layerManager, final JPanel contentPane, final ViewportData viewportData) {
241        this.layerManager = layerManager;
242        initialViewport = viewportData;
243        layerManager.addLayerChangeListener(this, true);
244        layerManager.addActiveLayerChangeListener(this);
245        Main.pref.addPreferenceChangeListener(this);
246
247        addComponentListener(new ComponentAdapter() {
248            @Override
249            public void componentResized(ComponentEvent e) {
250                removeComponentListener(this);
251
252                mapMover = new MapMover(MapView.this, contentPane);
253            }
254        });
255
256        // listend to selection changes to redraw the map
257        DataSet.addSelectionListener(repaintSelectionChangedListener);
258
259        //store the last mouse action
260        this.addMouseMotionListener(new MouseMotionListener() {
261            @Override
262            public void mouseDragged(MouseEvent e) {
263                mouseMoved(e);
264            }
265
266            @Override
267            public void mouseMoved(MouseEvent e) {
268                lastMEvent = e;
269            }
270        });
271        this.addMouseListener(new MouseAdapter() {
272            @Override
273            public void mousePressed(MouseEvent me) {
274                // focus the MapView component when mouse is pressed inside it
275                requestFocus();
276            }
277        });
278
279        setFocusTraversalKeysEnabled(!Shortcut.findShortcut(KeyEvent.VK_TAB, 0).isPresent());
280
281        for (JComponent c : getMapNavigationComponents(this)) {
282            add(c);
283        }
284        setTransferHandler(new OsmTransferHandler());
285    }
286
287    /**
288     * Adds the map navigation components to a
289     * @param forMapView The map view to get the components for.
290     * @return A list containing the correctly positioned map navigation components.
291     */
292    public static List<? extends JComponent> getMapNavigationComponents(MapView forMapView) {
293        MapSlider zoomSlider = new MapSlider(forMapView);
294        Dimension size = zoomSlider.getPreferredSize();
295        zoomSlider.setSize(size);
296        zoomSlider.setLocation(3, 0);
297        zoomSlider.setFocusTraversalKeysEnabled(!Shortcut.findShortcut(KeyEvent.VK_TAB, 0).isPresent());
298
299        MapScaler scaler = new MapScaler(forMapView);
300        scaler.setPreferredLineLength(size.width - 10);
301        scaler.setSize(scaler.getPreferredSize());
302        scaler.setLocation(3, size.height);
303
304        return Arrays.asList(zoomSlider, scaler);
305    }
306
307    // remebered geometry of the component
308    private Dimension oldSize;
309    private Point oldLoc;
310
311    /**
312     * Call this method to keep map position on screen during next repaint
313     */
314    public void rememberLastPositionOnScreen() {
315        oldSize = getSize();
316        oldLoc = getLocationOnScreen();
317    }
318
319    @Override
320    public void layerAdded(LayerAddEvent e) {
321        try {
322            Layer layer = e.getAddedLayer();
323            registeredLayers.put(layer, new WarningLayerPainter(layer));
324            // Layers may trigger a redraw during this call if they open dialogs.
325            LayerPainter painter = layer.attachToMapView(new MapViewEvent(this, false));
326            if (!registeredLayers.containsKey(layer)) {
327                // The layer may have removed itself during attachToMapView()
328                Main.warn("Layer was removed during attachToMapView()");
329            } else {
330                registeredLayers.put(layer, painter);
331
332                ProjectionBounds viewProjectionBounds = layer.getViewProjectionBounds();
333                if (viewProjectionBounds != null) {
334                    scheduleZoomTo(new ViewportData(viewProjectionBounds));
335                }
336
337                layer.addPropertyChangeListener(this);
338                Main.addProjectionChangeListener(layer);
339                invalidatedListener.addTo(layer);
340                AudioPlayer.reset();
341
342                repaint();
343            }
344        } catch (RuntimeException t) {
345            throw BugReport.intercept(t).put("layer", e.getAddedLayer());
346        }
347    }
348
349    /**
350     * Replies true if the active data layer (edit layer) is drawable.
351     *
352     * @return true if the active data layer (edit layer) is drawable, false otherwise
353     */
354    public boolean isActiveLayerDrawable() {
355         return layerManager.getEditLayer() != null;
356    }
357
358    /**
359     * Replies true if the active data layer (edit layer) is visible.
360     *
361     * @return true if the active data layer (edit layer) is visible, false otherwise
362     */
363    public boolean isActiveLayerVisible() {
364        OsmDataLayer e = layerManager.getEditLayer();
365        return e != null && e.isVisible();
366    }
367
368    @Override
369    public void layerRemoving(LayerRemoveEvent e) {
370        Layer layer = e.getRemovedLayer();
371
372        LayerPainter painter = registeredLayers.remove(layer);
373        if (painter == null) {
374            Main.error("The painter for layer " + layer + " was not registered.");
375            return;
376        }
377        painter.detachFromMapView(new MapViewEvent(this, false));
378        Main.removeProjectionChangeListener(layer);
379        layer.removePropertyChangeListener(this);
380        invalidatedListener.removeFrom(layer);
381        layer.destroy();
382        AudioPlayer.reset();
383
384        repaint();
385    }
386
387    private boolean virtualNodesEnabled;
388
389    public void setVirtualNodesEnabled(boolean enabled) {
390        if (virtualNodesEnabled != enabled) {
391            virtualNodesEnabled = enabled;
392            repaint();
393        }
394    }
395
396    /**
397     * Checks if virtual nodes should be drawn. Default is <code>false</code>
398     * @return The virtual nodes property.
399     * @see Rendering#render(DataSet, boolean, Bounds)
400     */
401    public boolean isVirtualNodesEnabled() {
402        return virtualNodesEnabled;
403    }
404
405    /**
406     * Moves the layer to the given new position. No event is fired, but repaints
407     * according to the new Z-Order of the layers.
408     *
409     * @param layer     The layer to move
410     * @param pos       The new position of the layer
411     */
412    public void moveLayer(Layer layer, int pos) {
413        layerManager.moveLayer(layer, pos);
414    }
415
416    @Override
417    public void layerOrderChanged(LayerOrderChangeEvent e) {
418        AudioPlayer.reset();
419        repaint();
420    }
421
422    private void paintLayer(Layer layer, Graphics2D g, Bounds box) {
423        try {
424            LayerPainter painter = registeredLayers.get(layer);
425            if (painter == null) {
426                throw new IllegalArgumentException("Cannot paint layer, it is not registered.");
427            }
428            MapViewRectangle clipBounds = getState().getViewArea(g.getClipBounds());
429            MapViewGraphics paintGraphics = new MapViewGraphics(this, g, clipBounds);
430
431            if (layer.getOpacity() < 1) {
432                g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) layer.getOpacity()));
433            }
434            painter.paint(paintGraphics);
435            g.setPaintMode();
436        } catch (RuntimeException t) {
437            BugReport.intercept(t).put("layer", layer).put("bounds", box).warn();
438        }
439    }
440
441    /**
442     * Draw the component.
443     */
444    @Override
445    public void paint(Graphics g) {
446        try {
447            if (!prepareToDraw()) {
448                return;
449            }
450        } catch (RuntimeException e) {
451            BugReport.intercept(e).put("center", this::getCenter).warn();
452            return;
453        }
454
455        List<Layer> visibleLayers = layerManager.getVisibleLayersInZOrder();
456
457        int nonChangedLayersCount = 0;
458        Set<MapViewPaintable> invalidated = invalidatedListener.collectInvalidatedLayers();
459        for (Layer l: visibleLayers) {
460            // `isChanged` for backward compatibility, see https://josm.openstreetmap.de/ticket/13175#comment:7
461            // Layers that still implement it (plugins) will use it to tell the MapView that they have been changed.
462            // This is why the MapView still uses it in addition to the invalidation events.
463            if (l.isChanged() || invalidated.contains(l)) {
464                break;
465            } else {
466                nonChangedLayersCount++;
467            }
468        }
469
470        boolean canUseBuffer = !paintPreferencesChanged.getAndSet(false)
471                && nonChangedLayers.size() <= nonChangedLayersCount
472                && lastViewID == getViewID()
473                && lastClipBounds.contains(g.getClipBounds())
474                && nonChangedLayers.equals(visibleLayers.subList(0, nonChangedLayers.size()));
475
476        if (null == offscreenBuffer || offscreenBuffer.getWidth() != getWidth() || offscreenBuffer.getHeight() != getHeight()) {
477            offscreenBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR);
478        }
479
480        Graphics2D tempG = offscreenBuffer.createGraphics();
481        tempG.setClip(g.getClip());
482        Bounds box = getLatLonBounds(g.getClipBounds());
483
484        if (!canUseBuffer || nonChangedLayersBuffer == null) {
485            if (null == nonChangedLayersBuffer
486                    || nonChangedLayersBuffer.getWidth() != getWidth() || nonChangedLayersBuffer.getHeight() != getHeight()) {
487                nonChangedLayersBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR);
488            }
489            Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
490            g2.setClip(g.getClip());
491            g2.setColor(PaintColors.getBackgroundColor());
492            g2.fillRect(0, 0, getWidth(), getHeight());
493
494            for (int i = 0; i < nonChangedLayersCount; i++) {
495                paintLayer(visibleLayers.get(i), g2, box);
496            }
497        } else {
498            // Maybe there were more unchanged layers then last time - draw them to buffer
499            if (nonChangedLayers.size() != nonChangedLayersCount) {
500                Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
501                g2.setClip(g.getClip());
502                for (int i = nonChangedLayers.size(); i < nonChangedLayersCount; i++) {
503                    paintLayer(visibleLayers.get(i), g2, box);
504                }
505            }
506        }
507
508        nonChangedLayers.clear();
509        nonChangedLayers.addAll(visibleLayers.subList(0, nonChangedLayersCount));
510        lastViewID = getViewID();
511        lastClipBounds = g.getClipBounds();
512
513        tempG.drawImage(nonChangedLayersBuffer, 0, 0, null);
514
515        for (int i = nonChangedLayersCount; i < visibleLayers.size(); i++) {
516            paintLayer(visibleLayers.get(i), tempG, box);
517        }
518
519        try {
520            drawTemporaryLayers(tempG, box);
521        } catch (RuntimeException e) {
522            BugReport.intercept(e).put("temporaryLayers", temporaryLayers).warn();
523        }
524
525        // draw world borders
526        try {
527            drawWorldBorders(tempG);
528        } catch (RuntimeException e) {
529            // getProjection() needs to be inside lambda to catch errors.
530            BugReport.intercept(e).put("bounds", () -> getProjection().getWorldBoundsLatLon()).warn();
531        }
532
533        if (Main.isDisplayingMapView() && Main.map.filterDialog != null) {
534            Main.map.filterDialog.drawOSDText(tempG);
535        }
536
537        if (playHeadMarker != null) {
538            playHeadMarker.paint(tempG, this);
539        }
540
541        try {
542            g.drawImage(offscreenBuffer, 0, 0, null);
543        } catch (ClassCastException e) {
544            // See #11002 and duplicate tickets. On Linux with Java >= 8 Many users face this error here:
545            //
546            // java.lang.ClassCastException: sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData
547            //   at sun.java2d.xr.XRPMBlitLoops.cacheToTmpSurface(XRPMBlitLoops.java:145)
548            //   at sun.java2d.xr.XrSwToPMBlit.Blit(XRPMBlitLoops.java:353)
549            //   at sun.java2d.pipe.DrawImage.blitSurfaceData(DrawImage.java:959)
550            //   at sun.java2d.pipe.DrawImage.renderImageCopy(DrawImage.java:577)
551            //   at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:67)
552            //   at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:1014)
553            //   at sun.java2d.pipe.ValidatePipe.copyImage(ValidatePipe.java:186)
554            //   at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3318)
555            //   at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3296)
556            //   at org.openstreetmap.josm.gui.MapView.paint(MapView.java:834)
557            //
558            // It seems to be this JDK bug, but Oracle does not seem to be fixing it:
559            // https://bugs.openjdk.java.net/browse/JDK-7172749
560            //
561            // According to bug reports it can happen for a variety of reasons such as:
562            // - long period of time
563            // - change of screen resolution
564            // - addition/removal of a secondary monitor
565            //
566            // But the application seems to work fine after, so let's just log the error
567            Main.error(e);
568        }
569        super.paint(g);
570    }
571
572    private void drawTemporaryLayers(Graphics2D tempG, Bounds box) {
573        synchronized (temporaryLayers) {
574            for (MapViewPaintable mvp : temporaryLayers) {
575                try {
576                    mvp.paint(tempG, this, box);
577                } catch (RuntimeException e) {
578                    throw BugReport.intercept(e).put("mvp", mvp);
579                }
580            }
581        }
582    }
583
584    private void drawWorldBorders(Graphics2D tempG) {
585        tempG.setColor(Color.WHITE);
586        Bounds b = getProjection().getWorldBoundsLatLon();
587
588        int w = getWidth();
589        int h = getHeight();
590
591        // Work around OpenJDK having problems when drawing out of bounds
592        final Area border = getState().getArea(b);
593        // Make the viewport 1px larger in every direction to prevent an
594        // additional 1px border when zooming in
595        final Area viewport = new Area(new Rectangle(-1, -1, w + 2, h + 2));
596        border.intersect(viewport);
597        tempG.draw(border);
598    }
599
600    /**
601     * Sets up the viewport to prepare for drawing the view.
602     * @return <code>true</code> if the view can be drawn, <code>false</code> otherwise.
603     */
604    public boolean prepareToDraw() {
605        updateLocationState();
606        if (initialViewport != null) {
607            zoomTo(initialViewport);
608            initialViewport = null;
609        }
610
611        if (getCenter() == null)
612            return false; // no data loaded yet.
613
614        // if the position was remembered, we need to adjust center once before repainting
615        if (oldLoc != null && oldSize != null) {
616            Point l1 = getLocationOnScreen();
617            final EastNorth newCenter = new EastNorth(
618                    getCenter().getX()+ (l1.x-oldLoc.x - (oldSize.width-getWidth())/2.0)*getScale(),
619                    getCenter().getY()+ (oldLoc.y-l1.y + (oldSize.height-getHeight())/2.0)*getScale()
620                    );
621            oldLoc = null; oldSize = null;
622            zoomTo(newCenter);
623        }
624
625        return true;
626    }
627
628    @Override
629    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
630        if (Main.map != null) {
631            /* This only makes the buttons look disabled. Disabling the actions as well requires
632             * the user to re-select the tool after i.e. moving a layer. While testing I found
633             * that I switch layers and actions at the same time and it was annoying to mind the
634             * order. This way it works as visual clue for new users */
635            // FIXME: This does not belong here.
636            for (final AbstractButton b: Main.map.allMapModeButtons) {
637                MapMode mode = (MapMode) b.getAction();
638                final boolean activeLayerSupported = mode.layerIsSupported(layerManager.getActiveLayer());
639                if (activeLayerSupported) {
640                    Main.registerActionShortcut(mode, mode.getShortcut()); //fix #6876
641                } else {
642                    Main.unregisterShortcut(mode.getShortcut());
643                }
644                b.setEnabled(activeLayerSupported);
645            }
646        }
647        AudioPlayer.reset();
648        repaint();
649    }
650
651    /**
652     * Adds a new temporary layer.
653     * <p>
654     * A temporary layer is a layer that is painted above all normal layers. Layers are painted in the order they are added.
655     *
656     * @param mvp The layer to paint.
657     * @return <code>true</code> if the layer was added.
658     */
659    public boolean addTemporaryLayer(MapViewPaintable mvp) {
660        synchronized (temporaryLayers) {
661            boolean added = temporaryLayers.add(mvp);
662            if (added) {
663                invalidatedListener.addTo(mvp);
664            }
665            return added;
666        }
667    }
668
669    /**
670     * Removes a layer previously added as temporary layer.
671     * @param mvp The layer to remove.
672     * @return <code>true</code> if that layer was removed.
673     */
674    public boolean removeTemporaryLayer(MapViewPaintable mvp) {
675        synchronized (temporaryLayers) {
676            boolean removed = temporaryLayers.remove(mvp);
677            if (removed) {
678                invalidatedListener.removeFrom(mvp);
679            }
680            return removed;
681        }
682    }
683
684    /**
685     * Gets a list of temporary layers.
686     * @return The layers in the order they are added.
687     */
688    public List<MapViewPaintable> getTemporaryLayers() {
689        synchronized (temporaryLayers) {
690            return Collections.unmodifiableList(new ArrayList<>(temporaryLayers));
691        }
692    }
693
694    @Override
695    public void propertyChange(PropertyChangeEvent evt) {
696        if (evt.getPropertyName().equals(Layer.VISIBLE_PROP)) {
697            repaint();
698        } else if (evt.getPropertyName().equals(Layer.OPACITY_PROP) ||
699                evt.getPropertyName().equals(Layer.FILTER_STATE_PROP)) {
700            Layer l = (Layer) evt.getSource();
701            if (l.isVisible()) {
702                invalidatedListener.invalidate(l);
703            }
704        }
705    }
706
707    @Override
708    public void preferenceChanged(PreferenceChangeEvent e) {
709        paintPreferencesChanged.set(true);
710    }
711
712    private final transient SelectionChangedListener repaintSelectionChangedListener = newSelection -> repaint();
713
714    /**
715     * Destroy this map view panel. Should be called once when it is not needed any more.
716     */
717    public void destroy() {
718        layerManager.removeLayerChangeListener(this, true);
719        layerManager.removeActiveLayerChangeListener(this);
720        Main.pref.removePreferenceChangeListener(this);
721        DataSet.removeSelectionListener(repaintSelectionChangedListener);
722        MultipolygonCache.getInstance().clear(this);
723        if (mapMover != null) {
724            mapMover.destroy();
725        }
726        nonChangedLayers.clear();
727        synchronized (temporaryLayers) {
728            temporaryLayers.clear();
729        }
730        nonChangedLayersBuffer = null;
731        offscreenBuffer = null;
732    }
733
734    /**
735     * Get a string representation of all layers suitable for the {@code source} changeset tag.
736     * @return A String of sources separated by ';'
737     */
738    public String getLayerInformationForSourceTag() {
739        final Set<String> layerInfo = new TreeSet<>();
740        if (!layerManager.getLayersOfType(GpxLayer.class).isEmpty()) {
741            // no i18n for international values
742            layerInfo.add("survey");
743        }
744        for (final GeoImageLayer i : layerManager.getLayersOfType(GeoImageLayer.class)) {
745            if (i.isVisible()) {
746                layerInfo.add(i.getName());
747            }
748        }
749        for (final ImageryLayer i : layerManager.getLayersOfType(ImageryLayer.class)) {
750            if (i.isVisible()) {
751                layerInfo.add(ImageryInfo.ImageryType.BING.equals(i.getInfo().getImageryType()) ? "Bing" : i.getName());
752            }
753        }
754        return Utils.join("; ", layerInfo);
755    }
756
757    /**
758     * This is a listener that gets informed whenever repaint is called for this MapView.
759     * <p>
760     * This is the only safe method to find changes to the map view, since many components call MapView.repaint() directly.
761     * @author Michael Zangl
762     * @since 10600 (functional interface)
763     */
764    @FunctionalInterface
765    public interface RepaintListener {
766        /**
767         * Called when any repaint method is called (using default arguments if required).
768         * @param tm see {@link JComponent#repaint(long, int, int, int, int)}
769         * @param x see {@link JComponent#repaint(long, int, int, int, int)}
770         * @param y see {@link JComponent#repaint(long, int, int, int, int)}
771         * @param width see {@link JComponent#repaint(long, int, int, int, int)}
772         * @param height see {@link JComponent#repaint(long, int, int, int, int)}
773         */
774        void repaint(long tm, int x, int y, int width, int height);
775    }
776
777    private final transient CopyOnWriteArrayList<RepaintListener> repaintListeners = new CopyOnWriteArrayList<>();
778
779    /**
780     * Adds a listener that gets informed whenever repaint() is called for this class.
781     * @param l The listener.
782     */
783    public void addRepaintListener(RepaintListener l) {
784        repaintListeners.add(l);
785    }
786
787    /**
788     * Removes a registered repaint listener.
789     * @param l The listener.
790     */
791    public void removeRepaintListener(RepaintListener l) {
792        repaintListeners.remove(l);
793    }
794
795    @Override
796    public void repaint(long tm, int x, int y, int width, int height) {
797        // This is the main repaint method, all other methods are convenience methods and simply call this method.
798        // This is just an observation, not a must, but seems to be true for all implementations I found so far.
799        if (repaintListeners != null) {
800            // Might get called early in super constructor
801            for (RepaintListener l : repaintListeners) {
802                l.repaint(tm, x, y, width, height);
803            }
804        }
805        super.repaint(tm, x, y, width, height);
806    }
807
808    @Override
809    public void repaint() {
810        if (Main.isTraceEnabled()) {
811            invalidatedListener.traceRandomRepaint();
812        }
813        super.repaint();
814    }
815
816    /**
817     * Returns the layer manager.
818     * @return the layer manager
819     * @since 10282
820     */
821    public final MainLayerManager getLayerManager() {
822        return layerManager;
823    }
824
825    /**
826     * Schedule a zoom to the given position on the next redraw.
827     * Temporary, may be removed without warning.
828     * @param viewportData the viewport to zoom to
829     * @since 10394
830     */
831    public void scheduleZoomTo(ViewportData viewportData) {
832        initialViewport = viewportData;
833    }
834}