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