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

Last change on this file since 11905 was 11905, checked in by Don-vip, 2 years ago

sonar - squid:S2301 - Public methods should not contain selector arguments

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