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