001// License: GPL. See LICENSE file for details.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.AlphaComposite;
007import java.awt.Color;
008import java.awt.Dimension;
009import java.awt.Graphics;
010import java.awt.Graphics2D;
011import java.awt.Point;
012import java.awt.Rectangle;
013import java.awt.event.ComponentAdapter;
014import java.awt.event.ComponentEvent;
015import java.awt.event.KeyEvent;
016import java.awt.event.MouseAdapter;
017import java.awt.event.MouseEvent;
018import java.awt.event.MouseMotionListener;
019import java.awt.geom.Area;
020import java.awt.geom.GeneralPath;
021import java.awt.image.BufferedImage;
022import java.beans.PropertyChangeEvent;
023import java.beans.PropertyChangeListener;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.concurrent.CopyOnWriteArrayList;
031
032import javax.swing.AbstractButton;
033import javax.swing.ActionMap;
034import javax.swing.InputMap;
035import javax.swing.JFrame;
036import javax.swing.JPanel;
037
038import org.openstreetmap.josm.Main;
039import org.openstreetmap.josm.actions.mapmode.MapMode;
040import org.openstreetmap.josm.data.Bounds;
041import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
042import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
043import org.openstreetmap.josm.data.SelectionChangedListener;
044import org.openstreetmap.josm.data.ViewportData;
045import org.openstreetmap.josm.data.coor.EastNorth;
046import org.openstreetmap.josm.data.coor.LatLon;
047import org.openstreetmap.josm.data.imagery.ImageryInfo;
048import org.openstreetmap.josm.data.osm.DataSet;
049import org.openstreetmap.josm.data.osm.OsmPrimitive;
050import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
051import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
052import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
053import org.openstreetmap.josm.gui.layer.GpxLayer;
054import org.openstreetmap.josm.gui.layer.ImageryLayer;
055import org.openstreetmap.josm.gui.layer.Layer;
056import org.openstreetmap.josm.gui.layer.MapViewPaintable;
057import org.openstreetmap.josm.gui.layer.OsmDataLayer;
058import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
059import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
060import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker;
061import org.openstreetmap.josm.gui.util.GuiHelper;
062import org.openstreetmap.josm.tools.AudioPlayer;
063import org.openstreetmap.josm.tools.BugReportExceptionHandler;
064import org.openstreetmap.josm.tools.Shortcut;
065import org.openstreetmap.josm.tools.Utils;
066
067/**
068 * This is a component used in the {@link MapFrame} for browsing the map. It use is to
069 * provide the MapMode's enough capabilities to operate.<br><br>
070 *
071 * {@code MapView} holds meta-data about the data set currently displayed, as scale level,
072 * center point viewed, what scrolling mode or editing mode is selected or with
073 * what projection the map is viewed etc..<br><br>
074 *
075 * {@code MapView} is able to administrate several layers.
076 *
077 * @author imi
078 */
079public class MapView extends NavigatableComponent implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.LayerStateChangeListener {
080
081    /**
082     * Interface to notify listeners of a layer change.
083     * @author imi
084     */
085    public interface LayerChangeListener {
086
087        /**
088         * Notifies this listener that the active layer has changed.
089         * @param oldLayer The previous active layer
090         * @param newLayer The new activer layer
091         */
092        void activeLayerChange(Layer oldLayer, Layer newLayer);
093
094        /**
095         * Notifies this listener that a layer has been added.
096         * @param newLayer The new added layer
097         */
098        void layerAdded(Layer newLayer);
099
100        /**
101         * Notifies this listener that a layer has been removed.
102         * @param oldLayer The old removed layer
103         */
104        void layerRemoved(Layer oldLayer);
105    }
106
107    public interface EditLayerChangeListener {
108        void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer);
109    }
110
111    public boolean viewportFollowing = false;
112
113    /**
114     * the layer listeners
115     */
116    private static final CopyOnWriteArrayList<LayerChangeListener> layerChangeListeners = new CopyOnWriteArrayList<>();
117    private static final CopyOnWriteArrayList<EditLayerChangeListener> editLayerChangeListeners = new CopyOnWriteArrayList<>();
118
119    /**
120     * Removes a layer change listener
121     *
122     * @param listener the listener. Ignored if null or already registered.
123     */
124    public static void removeLayerChangeListener(LayerChangeListener listener) {
125        layerChangeListeners.remove(listener);
126    }
127
128    public static void removeEditLayerChangeListener(EditLayerChangeListener listener) {
129        editLayerChangeListeners.remove(listener);
130    }
131
132    /**
133     * Adds a layer change listener
134     *
135     * @param listener the listener. Ignored if null or already registered.
136     */
137    public static void addLayerChangeListener(LayerChangeListener listener) {
138        if (listener != null) {
139            layerChangeListeners.addIfAbsent(listener);
140        }
141    }
142
143    /**
144     * Adds an edit layer change listener
145     *
146     * @param listener the listener. Ignored if null or already registered.
147     * @param initialFire Fire an edit-layer-changed-event right after adding
148     * the listener in case there is an edit layer present
149     */
150    public static void addEditLayerChangeListener(EditLayerChangeListener listener, boolean initialFire) {
151        addEditLayerChangeListener(listener);
152        if (initialFire && Main.isDisplayingMapView() && Main.map.mapView.getEditLayer() != null) {
153            fireEditLayerChanged(null, Main.map.mapView.getEditLayer());
154        }
155    }
156
157    /**
158     * Adds an edit layer change listener
159     *
160     * @param listener the listener. Ignored if null or already registered.
161     */
162    public static void addEditLayerChangeListener(EditLayerChangeListener listener) {
163        if (listener != null) {
164            editLayerChangeListeners.addIfAbsent(listener);
165        }
166    }
167
168    protected static void fireActiveLayerChanged(Layer oldLayer, Layer newLayer) {
169        for (LayerChangeListener l : layerChangeListeners) {
170            l.activeLayerChange(oldLayer, newLayer);
171        }
172    }
173
174    protected static void fireLayerAdded(Layer newLayer) {
175        for (MapView.LayerChangeListener l : MapView.layerChangeListeners) {
176            l.layerAdded(newLayer);
177        }
178    }
179
180    protected static void fireLayerRemoved(Layer layer) {
181        for (MapView.LayerChangeListener l : MapView.layerChangeListeners) {
182            l.layerRemoved(layer);
183        }
184    }
185
186    protected static void fireEditLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
187        for (EditLayerChangeListener l : editLayerChangeListeners) {
188            l.editLayerChanged(oldLayer, newLayer);
189        }
190    }
191
192    /**
193     * A list of all layers currently loaded.
194     */
195    private final List<Layer> layers = new ArrayList<>();
196    /**
197     * The play head marker: there is only one of these so it isn't in any specific layer
198     */
199    public PlayHeadMarker playHeadMarker = null;
200
201    /**
202     * The layer from the layers list that is currently active.
203     */
204    private Layer activeLayer;
205
206    private OsmDataLayer editLayer;
207
208    /**
209     * The last event performed by mouse.
210     */
211    public MouseEvent lastMEvent = new MouseEvent(this, 0, 0, 0, 0, 0, 0, false); // In case somebody reads it before first mouse move
212
213    private final List<MapViewPaintable> temporaryLayers = new LinkedList<>();
214
215    private BufferedImage nonChangedLayersBuffer;
216    private BufferedImage offscreenBuffer;
217    // Layers that wasn't changed since last paint
218    private final List<Layer> nonChangedLayers = new ArrayList<>();
219    private Layer changedLayer;
220    private int lastViewID;
221    private boolean paintPreferencesChanged = true;
222    private Rectangle lastClipBounds = new Rectangle();
223    private MapMover mapMover;
224
225    /**
226     * Constructs a new {@code MapView}.
227     * @param contentPane The content pane used to register shortcuts in its
228     * {@link InputMap} and {@link ActionMap}
229     * @param viewportData the initial viewport of the map. Can be null, then
230     * the viewport is derived from the layer data.
231     */
232    public MapView(final JPanel contentPane, final ViewportData viewportData) {
233        initialViewport = viewportData;
234        Main.pref.addPreferenceChangeListener(this);
235        final boolean unregisterTab = Shortcut.findShortcut(KeyEvent.VK_TAB, 0)!=null;
236
237        addComponentListener(new ComponentAdapter(){
238            @Override public void componentResized(ComponentEvent e) {
239                removeComponentListener(this);
240
241                MapSlider zoomSlider = new MapSlider(MapView.this);
242                add(zoomSlider);
243                zoomSlider.setBounds(3, 0, 114, 30);
244                zoomSlider.setFocusTraversalKeysEnabled(!unregisterTab);
245
246                MapScaler scaler = new MapScaler(MapView.this);
247                add(scaler);
248                scaler.setLocation(10,30);
249
250                mapMover = new MapMover(MapView.this, contentPane);
251            }
252        });
253
254        // listend to selection changes to redraw the map
255        DataSet.addSelectionListener(repaintSelectionChangedListener);
256
257        //store the last mouse action
258        this.addMouseMotionListener(new MouseMotionListener() {
259            @Override public void mouseDragged(MouseEvent e) {
260                mouseMoved(e);
261            }
262            @Override public void mouseMoved(MouseEvent e) {
263                lastMEvent = e;
264            }
265        });
266        this.addMouseListener(new MouseAdapter() {
267            @Override
268            public void mousePressed(MouseEvent me) {
269                // focus the MapView component when mouse is pressed inside it
270                requestFocus();
271            }
272        });
273
274        if (Shortcut.findShortcut(KeyEvent.VK_TAB, 0)!=null) {
275            setFocusTraversalKeysEnabled(false);
276        }
277    }
278
279    // remebered geometry of the component
280    private Dimension oldSize = null;
281    private Point oldLoc = null;
282
283    /*
284     * Call this method to keep map position on screen during next repaint
285     */
286    public void rememberLastPositionOnScreen() {
287        oldSize = getSize();
288        oldLoc  = getLocationOnScreen();
289    }
290
291    /**
292     * Adds a GPX layer. A GPX layer is added below the lowest data layer.
293     *
294     * @param layer the GPX layer
295     */
296    protected void addGpxLayer(GpxLayer layer) {
297        if (layers.isEmpty()) {
298            layers.add(layer);
299            return;
300        }
301        for (int i=layers.size()-1; i>= 0; i--) {
302            if (layers.get(i) instanceof OsmDataLayer) {
303                if (i == layers.size()-1) {
304                    layers.add(layer);
305                } else {
306                    layers.add(i+1, layer);
307                }
308                return;
309            }
310        }
311        layers.add(0, layer);
312    }
313
314    /**
315     * Add a layer to the current MapView. The layer will be added at topmost
316     * position.
317     * @param layer The layer to add
318     */
319    public void addLayer(Layer layer) {
320        if (layer instanceof MarkerLayer && playHeadMarker == null) {
321            playHeadMarker = PlayHeadMarker.create();
322        }
323
324        if (layer instanceof GpxLayer) {
325            addGpxLayer((GpxLayer)layer);
326        } else if (layers.isEmpty()) {
327            layers.add(layer);
328        } else if (layer.isBackgroundLayer()) {
329            int i = 0;
330            for (; i < layers.size(); i++) {
331                if (layers.get(i).isBackgroundLayer()) {
332                    break;
333                }
334            }
335            layers.add(i, layer);
336        } else {
337            layers.add(0, layer);
338        }
339        fireLayerAdded(layer);
340        boolean isOsmDataLayer = layer instanceof OsmDataLayer;
341        if (isOsmDataLayer) {
342            ((OsmDataLayer)layer).addLayerStateChangeListener(this);
343        }
344        boolean callSetActiveLayer = isOsmDataLayer || activeLayer == null;
345        if (callSetActiveLayer) {
346            // autoselect the new layer
347            setActiveLayer(layer); // also repaints this MapView
348        }
349        layer.addPropertyChangeListener(this);
350        Main.addProjectionChangeListener(layer);
351        AudioPlayer.reset();
352        if (!callSetActiveLayer) {
353            repaint();
354        }
355    }
356
357    @Override
358    protected DataSet getCurrentDataSet() {
359        if (editLayer != null)
360            return editLayer.data;
361        else
362            return null;
363    }
364
365    /**
366     * Replies true if the active layer is drawable.
367     *
368     * @return true if the active layer is drawable, false otherwise
369     */
370    public boolean isActiveLayerDrawable() {
371        return editLayer != null;
372    }
373
374    /**
375     * Replies true if the active layer is visible.
376     *
377     * @return true if the active layer is visible, false otherwise
378     */
379    public boolean isActiveLayerVisible() {
380        return isActiveLayerDrawable() && editLayer.isVisible();
381    }
382
383    /**
384     * Determines the next active data layer according to the following
385     * rules:
386     * <ul>
387     *   <li>if there is at least one {@link OsmDataLayer} the first one
388     *     becomes active</li>
389     *   <li>otherwise, the top most layer of any type becomes active</li>
390     * </ul>
391     *
392     * @return the next active data layer
393     */
394    protected Layer determineNextActiveLayer(List<Layer> layersList) {
395        // First look for data layer
396        for (Layer layer:layersList) {
397            if (layer instanceof OsmDataLayer)
398                return layer;
399        }
400
401        // Then any layer
402        if (!layersList.isEmpty())
403            return layersList.get(0);
404
405        // and then give up
406        return null;
407
408    }
409
410    /**
411     * Remove the layer from the mapview. If the layer was in the list before,
412     * an LayerChange event is fired.
413     * @param layer The layer to remove
414     */
415    public void removeLayer(Layer layer) {
416        List<Layer> layersList = new ArrayList<>(layers);
417
418        if (!layersList.remove(layer))
419            return;
420
421        setEditLayer(layersList);
422
423        if (layer == activeLayer) {
424            setActiveLayer(determineNextActiveLayer(layersList), false);
425        }
426
427        if (layer instanceof OsmDataLayer) {
428            ((OsmDataLayer)layer).removeLayerPropertyChangeListener(this);
429        }
430
431        layers.remove(layer);
432        Main.removeProjectionChangeListener(layer);
433        fireLayerRemoved(layer);
434        layer.removePropertyChangeListener(this);
435        layer.destroy();
436        AudioPlayer.reset();
437        repaint();
438    }
439
440    private boolean virtualNodesEnabled = false;
441
442    public void setVirtualNodesEnabled(boolean enabled) {
443        if(virtualNodesEnabled != enabled) {
444            virtualNodesEnabled = enabled;
445            repaint();
446        }
447    }
448    public boolean isVirtualNodesEnabled() {
449        return virtualNodesEnabled;
450    }
451
452    /**
453     * Moves the layer to the given new position. No event is fired, but repaints
454     * according to the new Z-Order of the layers.
455     *
456     * @param layer     The layer to move
457     * @param pos       The new position of the layer
458     */
459    public void moveLayer(Layer layer, int pos) {
460        int curLayerPos = layers.indexOf(layer);
461        if (curLayerPos == -1)
462            throw new IllegalArgumentException(tr("Layer not in list."));
463        if (pos == curLayerPos)
464            return; // already in place.
465        layers.remove(curLayerPos);
466        if (pos >= layers.size()) {
467            layers.add(layer);
468        } else {
469            layers.add(pos, layer);
470        }
471        setEditLayer(layers);
472        AudioPlayer.reset();
473        repaint();
474    }
475
476    public int getLayerPos(Layer layer) {
477        int curLayerPos = layers.indexOf(layer);
478        if (curLayerPos == -1)
479            throw new IllegalArgumentException(tr("Layer not in list."));
480        return curLayerPos;
481    }
482
483    /**
484     * Creates a list of the visible layers in Z-Order, the layer with the lowest Z-Order
485     * first, layer with the highest Z-Order last.
486     *
487     * @return a list of the visible in Z-Order, the layer with the lowest Z-Order
488     * first, layer with the highest Z-Order last.
489     */
490    protected List<Layer> getVisibleLayersInZOrder() {
491        List<Layer> ret = new ArrayList<>();
492        for (Layer l: layers) {
493            if (l.isVisible()) {
494                ret.add(l);
495            }
496        }
497        // sort according to position in the list of layers, with one exception:
498        // an active data layer always becomes a higher Z-Order than all other
499        // data layers
500        //
501        Collections.sort(
502                ret,
503                new Comparator<Layer>() {
504                    @Override public int compare(Layer l1, Layer l2) {
505                        if (l1 instanceof OsmDataLayer && l2 instanceof OsmDataLayer) {
506                            if (l1 == getActiveLayer()) return -1;
507                            if (l2 == getActiveLayer()) return 1;
508                            return Integer.valueOf(layers.indexOf(l1)).compareTo(layers.indexOf(l2));
509                        } else
510                            return Integer.valueOf(layers.indexOf(l1)).compareTo(layers.indexOf(l2));
511                    }
512                }
513        );
514        Collections.reverse(ret);
515        return ret;
516    }
517
518    private void paintLayer(Layer layer, Graphics2D g, Bounds box) {
519        if (layer.getOpacity() < 1) {
520            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,(float)layer.getOpacity()));
521        }
522        layer.paint(g, this, box);
523        g.setPaintMode();
524    }
525
526    /**
527     * Draw the component.
528     */
529    @Override public void paint(Graphics g) {
530        if (initialViewport != null) {
531            zoomTo(initialViewport);
532            initialViewport = null;
533        }
534        if (BugReportExceptionHandler.exceptionHandlingInProgress())
535            return;
536
537        if (center == null)
538            return; // no data loaded yet.
539
540        // if the position was remembered, we need to adjust center once before repainting
541        if (oldLoc != null && oldSize != null) {
542            Point l1  = getLocationOnScreen();
543            final EastNorth newCenter = new EastNorth(
544                    center.getX()+ (l1.x-oldLoc.x - (oldSize.width-getWidth())/2.0)*getScale(),
545                    center.getY()+ (oldLoc.y-l1.y + (oldSize.height-getHeight())/2.0)*getScale()
546                    );
547            oldLoc = null; oldSize = null;
548            zoomTo(newCenter);
549        }
550
551        List<Layer> visibleLayers = getVisibleLayersInZOrder();
552
553        int nonChangedLayersCount = 0;
554        for (Layer l: visibleLayers) {
555            if (l.isChanged() || l == changedLayer) {
556                break;
557            } else {
558                nonChangedLayersCount++;
559            }
560        }
561
562        boolean canUseBuffer;
563
564        synchronized (this) {
565            canUseBuffer = !paintPreferencesChanged;
566            paintPreferencesChanged = false;
567        }
568        canUseBuffer = canUseBuffer && nonChangedLayers.size() <= nonChangedLayersCount &&
569        lastViewID == getViewID() && lastClipBounds.contains(g.getClipBounds());
570        if (canUseBuffer) {
571            for (int i=0; i<nonChangedLayers.size(); i++) {
572                if (visibleLayers.get(i) != nonChangedLayers.get(i)) {
573                    canUseBuffer = false;
574                    break;
575                }
576            }
577        }
578
579        if (null == offscreenBuffer || offscreenBuffer.getWidth() != getWidth() || offscreenBuffer.getHeight() != getHeight()) {
580            offscreenBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR);
581        }
582
583        Graphics2D tempG = offscreenBuffer.createGraphics();
584        tempG.setClip(g.getClip());
585        Bounds box = getLatLonBounds(g.getClipBounds());
586
587        if (!canUseBuffer || nonChangedLayersBuffer == null) {
588            if (null == nonChangedLayersBuffer || nonChangedLayersBuffer.getWidth() != getWidth() || nonChangedLayersBuffer.getHeight() != getHeight()) {
589                nonChangedLayersBuffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR);
590            }
591            Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
592            g2.setClip(g.getClip());
593            g2.setColor(PaintColors.getBackgroundColor());
594            g2.fillRect(0, 0, getWidth(), getHeight());
595
596            for (int i=0; i<nonChangedLayersCount; i++) {
597                paintLayer(visibleLayers.get(i),g2, box);
598            }
599        } else {
600            // Maybe there were more unchanged layers then last time - draw them to buffer
601            if (nonChangedLayers.size() != nonChangedLayersCount) {
602                Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
603                g2.setClip(g.getClip());
604                for (int i=nonChangedLayers.size(); i<nonChangedLayersCount; i++) {
605                    paintLayer(visibleLayers.get(i),g2, box);
606                }
607            }
608        }
609
610        nonChangedLayers.clear();
611        changedLayer = null;
612        for (int i=0; i<nonChangedLayersCount; i++) {
613            nonChangedLayers.add(visibleLayers.get(i));
614        }
615        lastViewID = getViewID();
616        lastClipBounds = g.getClipBounds();
617
618        tempG.drawImage(nonChangedLayersBuffer, 0, 0, null);
619
620        for (int i=nonChangedLayersCount; i<visibleLayers.size(); i++) {
621            paintLayer(visibleLayers.get(i),tempG, box);
622        }
623
624        for (MapViewPaintable mvp : temporaryLayers) {
625            mvp.paint(tempG, this, box);
626        }
627
628        // draw world borders
629        tempG.setColor(Color.WHITE);
630        Bounds b = getProjection().getWorldBoundsLatLon();
631        double lat = b.getMinLat();
632        double lon = b.getMinLon();
633
634        Point p = getPoint(b.getMin());
635
636        GeneralPath path = new GeneralPath();
637
638        path.moveTo(p.x, p.y);
639        double max = b.getMax().lat();
640        for(; lat <= max; lat += 1.0)
641        {
642            p = getPoint(new LatLon(lat >= max ? max : lat, lon));
643            path.lineTo(p.x, p.y);
644        }
645        lat = max; max = b.getMax().lon();
646        for(; lon <= max; lon += 1.0)
647        {
648            p = getPoint(new LatLon(lat, lon >= max ? max : lon));
649            path.lineTo(p.x, p.y);
650        }
651        lon = max; max = b.getMinLat();
652        for(; lat >= max; lat -= 1.0)
653        {
654            p = getPoint(new LatLon(lat <= max ? max : lat, lon));
655            path.lineTo(p.x, p.y);
656        }
657        lat = max; max = b.getMinLon();
658        for(; lon >= max; lon -= 1.0)
659        {
660            p = getPoint(new LatLon(lat, lon <= max ? max : lon));
661            path.lineTo(p.x, p.y);
662        }
663
664        int w = getWidth();
665        int h = getHeight();
666
667        // Work around OpenJDK having problems when drawing out of bounds
668        final Area border = new Area(path);
669        // Make the viewport 1px larger in every direction to prevent an
670        // additional 1px border when zooming in
671        final Area viewport = new Area(new Rectangle(-1, -1, w + 2, h + 2));
672        border.intersect(viewport);
673        tempG.draw(border);
674
675        if (Main.isDisplayingMapView() && Main.map.filterDialog != null) {
676            Main.map.filterDialog.drawOSDText(tempG);
677        }
678
679        if (playHeadMarker != null) {
680            playHeadMarker.paint(tempG, this);
681        }
682
683        g.drawImage(offscreenBuffer, 0, 0, null);
684        super.paint(g);
685    }
686
687    /**
688     * Set the new dimension to the view.
689     * 
690     * @deprecated use #zoomTo(BoundingXYVisitor)
691     */
692    @Deprecated
693    public void recalculateCenterScale(BoundingXYVisitor box) {
694        zoomTo(box);
695    }
696
697    /**
698     * @return An unmodifiable collection of all layers
699     */
700    public Collection<Layer> getAllLayers() {
701        return Collections.unmodifiableCollection(new ArrayList<>(layers));
702    }
703
704    /**
705     * @return An unmodifiable ordered list of all layers
706     */
707    public List<Layer> getAllLayersAsList() {
708        return Collections.unmodifiableList(new ArrayList<>(layers));
709    }
710
711    /**
712     * Replies an unmodifiable list of layers of a certain type.
713     *
714     * Example:
715     * <pre>
716     *     List&lt;WMSLayer&gt; wmsLayers = getLayersOfType(WMSLayer.class);
717     * </pre>
718     *
719     * @return an unmodifiable list of layers of a certain type.
720     */
721    public <T extends Layer> List<T>  getLayersOfType(Class<T> ofType) {
722        return new ArrayList<>(Utils.filteredCollection(getAllLayers(), ofType));
723    }
724
725    /**
726     * Replies the number of layers managed by this mav view
727     *
728     * @return the number of layers managed by this mav view
729     */
730    public int getNumLayers() {
731        return layers.size();
732    }
733
734    /**
735     * Replies true if there is at least one layer in this map view
736     *
737     * @return true if there is at least one layer in this map view
738     */
739    public boolean hasLayers() {
740        return getNumLayers() > 0;
741    }
742
743    private void setEditLayer(List<Layer> layersList) {
744        OsmDataLayer newEditLayer = layersList.contains(editLayer)?editLayer:null;
745        OsmDataLayer oldEditLayer = editLayer;
746
747        // Find new edit layer
748        if (activeLayer != editLayer || !layersList.contains(editLayer)) {
749            if (activeLayer instanceof OsmDataLayer && layersList.contains(activeLayer)) {
750                newEditLayer = (OsmDataLayer) activeLayer;
751            } else {
752                for (Layer layer:layersList) {
753                    if (layer instanceof OsmDataLayer) {
754                        newEditLayer = (OsmDataLayer) layer;
755                        break;
756                    }
757                }
758            }
759        }
760
761        // Set new edit layer
762        if (newEditLayer != editLayer) {
763            if (newEditLayer == null) {
764                getCurrentDataSet().setSelected();
765            }
766
767            editLayer = newEditLayer;
768            fireEditLayerChanged(oldEditLayer, newEditLayer);
769            refreshTitle();
770        }
771
772    }
773
774    /**
775     * Sets the active layer to <code>layer</code>. If <code>layer</code> is an instance
776     * of {@link OsmDataLayer} also sets {@link #editLayer} to <code>layer</code>.
777     *
778     * @param layer the layer to be activate; must be one of the layers in the list of layers
779     * @exception IllegalArgumentException thrown if layer is not in the lis of layers
780     */
781    public void setActiveLayer(Layer layer) {
782        setActiveLayer(layer, true);
783    }
784
785    private void setActiveLayer(Layer layer, boolean setEditLayer) {
786        if (layer != null && !layers.contains(layer))
787            throw new IllegalArgumentException(tr("Layer ''{0}'' must be in list of layers", layer.toString()));
788
789        if (layer == activeLayer)
790            return;
791
792        Layer old = activeLayer;
793        activeLayer = layer;
794        if (setEditLayer) {
795            setEditLayer(layers);
796        }
797        fireActiveLayerChanged(old, layer);
798
799        /* This only makes the buttons look disabled. Disabling the actions as well requires
800         * the user to re-select the tool after i.e. moving a layer. While testing I found
801         * that I switch layers and actions at the same time and it was annoying to mind the
802         * order. This way it works as visual clue for new users */
803        for (final AbstractButton b: Main.map.allMapModeButtons) {
804            MapMode mode = (MapMode)b.getAction();
805            if (mode.layerIsSupported(layer)) {
806                Main.registerActionShortcut(mode, mode.getShortcut()); //fix #6876
807                GuiHelper.runInEDTAndWait(new Runnable() {
808                    @Override public void run() {
809                        b.setEnabled(true);
810                    }
811                });
812            } else {
813                Main.unregisterShortcut(mode.getShortcut());
814                GuiHelper.runInEDTAndWait(new Runnable() {
815                    @Override public void run() {
816                        b.setEnabled(false);
817                    }
818                });
819            }
820        }
821        AudioPlayer.reset();
822        repaint();
823    }
824
825    /**
826     * Replies the currently active layer
827     *
828     * @return the currently active layer (may be null)
829     */
830    public Layer getActiveLayer() {
831        return activeLayer;
832    }
833
834    /**
835     * Replies the current edit layer, if any
836     *
837     * @return the current edit layer. May be null.
838     */
839    public OsmDataLayer getEditLayer() {
840        return editLayer;
841    }
842
843    /**
844     * replies true if the list of layers managed by this map view contain layer
845     *
846     * @param layer the layer
847     * @return true if the list of layers managed by this map view contain layer
848     */
849    public boolean hasLayer(Layer layer) {
850        return layers.contains(layer);
851    }
852
853    public boolean addTemporaryLayer(MapViewPaintable mvp) {
854        if (temporaryLayers.contains(mvp)) return false;
855        return temporaryLayers.add(mvp);
856    }
857
858    public boolean removeTemporaryLayer(MapViewPaintable mvp) {
859        return temporaryLayers.remove(mvp);
860    }
861
862    @Override
863    public void propertyChange(PropertyChangeEvent evt) {
864        if (evt.getPropertyName().equals(Layer.VISIBLE_PROP)) {
865            repaint();
866        } else if (evt.getPropertyName().equals(Layer.OPACITY_PROP)) {
867            Layer l = (Layer)evt.getSource();
868            if (l.isVisible()) {
869                changedLayer = l;
870                repaint();
871            }
872        } else if (evt.getPropertyName().equals(OsmDataLayer.REQUIRES_SAVE_TO_DISK_PROP)
873                || evt.getPropertyName().equals(OsmDataLayer.REQUIRES_UPLOAD_TO_SERVER_PROP)) {
874            OsmDataLayer layer = (OsmDataLayer)evt.getSource();
875            if (layer == getEditLayer()) {
876                refreshTitle();
877            }
878        }
879    }
880
881    protected void refreshTitle() {
882        if (Main.parent != null) {
883            boolean dirty = editLayer != null &&
884                    (editLayer.requiresSaveToFile() || (editLayer.requiresUploadToServer() && !editLayer.isUploadDiscouraged()));
885            ((JFrame) Main.parent).setTitle((dirty ? "* " : "") + tr("Java OpenStreetMap Editor"));
886            ((JFrame) Main.parent).getRootPane().putClientProperty("Window.documentModified", dirty);
887        }
888    }
889
890    @Override
891    public void preferenceChanged(PreferenceChangeEvent e) {
892        synchronized (this) {
893            paintPreferencesChanged = true;
894        }
895    }
896
897    private SelectionChangedListener repaintSelectionChangedListener = new SelectionChangedListener(){
898        @Override public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
899            repaint();
900        }
901    };
902
903    public void destroy() {
904        Main.pref.removePreferenceChangeListener(this);
905        DataSet.removeSelectionListener(repaintSelectionChangedListener);
906        MultipolygonCache.getInstance().clear(this);
907        if (mapMover != null) {
908            mapMover.destroy();
909        }
910        activeLayer = null;
911        changedLayer = null;
912        editLayer = null;
913        layers.clear();
914        nonChangedLayers.clear();
915        temporaryLayers.clear();
916    }
917
918    @Override
919    public void uploadDiscouragedChanged(OsmDataLayer layer, boolean newValue) {
920        if (layer == getEditLayer()) {
921            refreshTitle();
922        }
923    }
924
925    /**
926     * Get a string representation of all layers suitable for the {@code source} changeset tag.
927     */
928    public String getLayerInformationForSourceTag() {
929        final Collection<String> layerInfo = new ArrayList<>();
930        if (!getLayersOfType(GpxLayer.class).isEmpty()) {
931            // no i18n for international values
932            layerInfo.add("survey");
933        }
934        for (final GeoImageLayer i : getLayersOfType(GeoImageLayer.class)) {
935            layerInfo.add(i.getName());
936        }
937        for (final ImageryLayer i : getLayersOfType(ImageryLayer.class)) {
938            layerInfo.add(ImageryInfo.ImageryType.BING.equals(i.getInfo().getImageryType()) ? "Bing" : i.getName());
939        }
940        return Utils.join("; ", layerInfo);
941    }
942}