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