001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Component;
008import java.awt.event.ActionEvent;
009import java.beans.PropertyChangeListener;
010import java.beans.PropertyChangeSupport;
011import java.io.File;
012import java.util.List;
013
014import javax.swing.AbstractAction;
015import javax.swing.Action;
016import javax.swing.Icon;
017import javax.swing.JOptionPane;
018import javax.swing.JSeparator;
019import javax.swing.SwingUtilities;
020
021import org.openstreetmap.josm.Main;
022import org.openstreetmap.josm.actions.GpxExportAction;
023import org.openstreetmap.josm.actions.SaveAction;
024import org.openstreetmap.josm.actions.SaveActionBase;
025import org.openstreetmap.josm.actions.SaveAsAction;
026import org.openstreetmap.josm.data.ProjectionBounds;
027import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
028import org.openstreetmap.josm.data.preferences.AbstractProperty;
029import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
030import org.openstreetmap.josm.data.preferences.ColorProperty;
031import org.openstreetmap.josm.data.projection.Projection;
032import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
033import org.openstreetmap.josm.tools.Destroyable;
034import org.openstreetmap.josm.tools.ImageProvider;
035import org.openstreetmap.josm.tools.Utils;
036
037/**
038 * A layer encapsulates the gui component of one dataset and its representation.
039 *
040 * Some layers may display data directly imported from OSM server. Other only
041 * display background images. Some can be edited, some not. Some are static and
042 * other changes dynamically (auto-updated).
043 *
044 * Layers can be visible or not. Most actions the user can do applies only on
045 * selected layers. The available actions depend on the selected layers too.
046 *
047 * All layers are managed by the MapView. They are displayed in a list to the
048 * right of the screen.
049 *
050 * @author imi
051 */
052public abstract class Layer extends AbstractMapViewPaintable implements Destroyable, ProjectionChangeListener {
053
054    /**
055     * Action related to a single layer.
056     */
057    public interface LayerAction {
058
059        /**
060         * Determines if this action supports a given list of layers.
061         * @param layers list of layers
062         * @return {@code true} if this action supports the given list of layers, {@code false} otherwise
063         */
064        boolean supportLayers(List<Layer> layers);
065
066        /**
067         * Creates and return the menu component.
068         * @return the menu component
069         */
070        Component createMenuComponent();
071    }
072
073    /**
074     * Action related to several layers.
075     * @since 10600 (functional interface)
076     */
077    @FunctionalInterface
078    public interface MultiLayerAction {
079
080        /**
081         * Returns the action for a given list of layers.
082         * @param layers list of layers
083         * @return the action for the given list of layers
084         */
085        Action getMultiLayerAction(List<Layer> layers);
086    }
087
088    /**
089     * Special class that can be returned by getMenuEntries when JSeparator needs to be created
090     */
091    public static class SeparatorLayerAction extends AbstractAction implements LayerAction {
092        /** Unique instance */
093        public static final SeparatorLayerAction INSTANCE = new SeparatorLayerAction();
094
095        @Override
096        public void actionPerformed(ActionEvent e) {
097            throw new UnsupportedOperationException();
098        }
099
100        @Override
101        public Component createMenuComponent() {
102            return new JSeparator();
103        }
104
105        @Override
106        public boolean supportLayers(List<Layer> layers) {
107            return false;
108        }
109    }
110
111    public static final String VISIBLE_PROP = Layer.class.getName() + ".visible";
112    public static final String OPACITY_PROP = Layer.class.getName() + ".opacity";
113    public static final String NAME_PROP = Layer.class.getName() + ".name";
114    public static final String FILTER_STATE_PROP = Layer.class.getName() + ".filterstate";
115
116    /**
117     * keeps track of property change listeners
118     */
119    protected PropertyChangeSupport propertyChangeSupport;
120
121    /**
122     * The visibility state of the layer.
123     */
124    private boolean visible = true;
125
126    /**
127     * The opacity of the layer.
128     */
129    private double opacity = 1;
130
131    /**
132     * The layer should be handled as a background layer in automatic handling
133     */
134    private boolean background;
135
136    /**
137     * The name of this layer.
138     */
139    private String name;
140
141    /**
142     * This is set if user renamed this layer.
143     */
144    private boolean renamed;
145
146    /**
147     * If a file is associated with this layer, this variable should be set to it.
148     */
149    private File associatedFile;
150
151    private final ValueChangeListener<Object> invalidateListener = change -> invalidate();
152
153    /**
154     * Create the layer and fill in the necessary components.
155     * @param name Layer name
156     */
157    public Layer(String name) {
158        this.propertyChangeSupport = new PropertyChangeSupport(this);
159        setName(name);
160    }
161
162    /**
163     * Initialization code, that depends on Main.map.mapView.
164     *
165     * It is always called in the event dispatching thread.
166     * Note that Main.map is null as long as no layer has been added, so do
167     * not execute code in the constructor, that assumes Main.map.mapView is
168     * not null.
169     *
170     * If you need to execute code when this layer is added to the map view, use
171     * {@link #attachToMapView(org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent)}
172     */
173    public void hookUpMapView() {
174    }
175
176    /**
177     * Return a representative small image for this layer. The image must not
178     * be larger than 64 pixel in any dimension.
179     * @return layer icon
180     */
181    public abstract Icon getIcon();
182
183    /**
184     * Gets the color property to use for this layer.
185     * @return The color property.
186     * @since 10824
187     */
188    public AbstractProperty<Color> getColorProperty() {
189        ColorProperty base = getBaseColorProperty();
190        if (base != null) {
191            // cannot cache this - name may change.
192            return base.getChildColor("layer " + getName());
193        } else {
194            return null;
195        }
196    }
197
198    /**
199     * Gets the color property that stores the default color for this layer.
200     * @return The property or <code>null</code> if this layer is not colored.
201     * @since 10824
202     */
203    protected ColorProperty getBaseColorProperty() {
204        return null;
205    }
206
207    private void addColorPropertyListener() {
208        AbstractProperty<Color> colorProperty = getColorProperty();
209        if (colorProperty != null) {
210            colorProperty.addListener(invalidateListener);
211        }
212    }
213
214    private void removeColorPropertyListener() {
215        AbstractProperty<Color> colorProperty = getColorProperty();
216        if (colorProperty != null) {
217            colorProperty.removeListener(invalidateListener);
218        }
219    }
220
221    /**
222     * @return A small tooltip hint about some statistics for this layer.
223     */
224    public abstract String getToolTipText();
225
226    /**
227     * Merges the given layer into this layer. Throws if the layer types are
228     * incompatible.
229     * @param from The layer that get merged into this one. After the merge,
230     *      the other layer is not usable anymore and passing to one others
231     *      mergeFrom should be one of the last things to do with a layer.
232     */
233    public abstract void mergeFrom(Layer from);
234
235    /**
236     * @param other The other layer that is tested to be mergable with this.
237     * @return Whether the other layer can be merged into this layer.
238     */
239    public abstract boolean isMergable(Layer other);
240
241    public abstract void visitBoundingBox(BoundingXYVisitor v);
242
243    public abstract Object getInfoComponent();
244
245    /**
246     * Determines if info dialog can be resized (false by default).
247     * @return {@code true} if the info dialog can be resized, {@code false} otherwise
248     * @since 6708
249     */
250    public boolean isInfoResizable() {
251        return false;
252    }
253
254    /**
255     * Returns list of actions. Action can implement LayerAction interface when it needs to be represented by other
256     * menu component than JMenuItem or when it supports multiple layers. Actions that support multiple layers should also
257     * have correct equals implementation.
258     *
259     * Use {@link SeparatorLayerAction#INSTANCE} instead of new JSeparator
260     * @return menu actions for this layer
261     */
262    public abstract Action[] getMenuEntries();
263
264    /**
265     * Called, when the layer is removed from the mapview and is going to be destroyed.
266     *
267     * This is because the Layer constructor can not add itself safely as listener
268     * to the layerlist dialog, because there may be no such dialog yet (loaded
269     * via command line parameter).
270     */
271    @Override
272    public void destroy() {
273        // Override in subclasses if needed
274        removeColorPropertyListener();
275    }
276
277    public File getAssociatedFile() {
278        return associatedFile;
279    }
280
281    public void setAssociatedFile(File file) {
282        associatedFile = file;
283    }
284
285    /**
286     * Replies the name of the layer
287     *
288     * @return the name of the layer
289     */
290    public String getName() {
291        return name;
292    }
293
294    /**
295     * Sets the name of the layer
296     *
297     * @param name the name. If null, the name is set to the empty string.
298     */
299    public final void setName(String name) {
300        if (this.name != null) {
301            removeColorPropertyListener();
302        }
303        if (name == null) {
304            name = "";
305        }
306
307        String oldValue = this.name;
308        this.name = name;
309        if (!this.name.equals(oldValue)) {
310            propertyChangeSupport.firePropertyChange(NAME_PROP, oldValue, this.name);
311        }
312
313        // re-add listener
314        addColorPropertyListener();
315        invalidate();
316    }
317
318    /**
319     * Rename layer and set renamed flag to mark it as renamed (has user given name).
320     *
321     * @param name the name. If null, the name is set to the empty string.
322     */
323    public final void rename(String name) {
324        renamed = true;
325        setName(name);
326    }
327
328    /**
329     * Replies true if this layer was renamed by user
330     *
331     * @return true if this layer was renamed by user
332     */
333    public boolean isRenamed() {
334        return renamed;
335    }
336
337    /**
338     * Replies true if this layer is a background layer
339     *
340     * @return true if this layer is a background layer
341     */
342    public boolean isBackgroundLayer() {
343        return background;
344    }
345
346    /**
347     * Sets whether this layer is a background layer
348     *
349     * @param background true, if this layer is a background layer
350     */
351    public void setBackgroundLayer(boolean background) {
352        this.background = background;
353    }
354
355    /**
356     * Sets the visibility of this layer. Emits property change event for
357     * property {@link #VISIBLE_PROP}.
358     *
359     * @param visible true, if the layer is visible; false, otherwise.
360     */
361    public void setVisible(boolean visible) {
362        boolean oldValue = isVisible();
363        this.visible = visible;
364        if (visible && opacity == 0) {
365            setOpacity(1);
366        } else if (oldValue != isVisible()) {
367            fireVisibleChanged(oldValue, isVisible());
368        }
369    }
370
371    /**
372     * Replies true if this layer is visible. False, otherwise.
373     * @return  true if this layer is visible. False, otherwise.
374     */
375    public boolean isVisible() {
376        return visible && opacity != 0;
377    }
378
379    /**
380     * Gets the opacity of the layer, in range 0...1
381     * @return The opacity
382     */
383    public double getOpacity() {
384        return opacity;
385    }
386
387    /**
388     * Sets the opacity of the layer, in range 0...1
389     * @param opacity The opacity
390     * @throws IllegalArgumentException if the opacity is out of range
391     */
392    public void setOpacity(double opacity) {
393        if (!(opacity >= 0 && opacity <= 1))
394            throw new IllegalArgumentException("Opacity value must be between 0 and 1");
395        double oldOpacity = getOpacity();
396        boolean oldVisible = isVisible();
397        this.opacity = opacity;
398        if (!Utils.equalsEpsilon(oldOpacity, getOpacity())) {
399            fireOpacityChanged(oldOpacity, getOpacity());
400        }
401        if (oldVisible != isVisible()) {
402            fireVisibleChanged(oldVisible, isVisible());
403        }
404    }
405
406    /**
407     * Sets new state to the layer after applying {@link ImageProcessor}.
408     */
409    public void setFilterStateChanged() {
410        fireFilterStateChanged();
411    }
412
413    /**
414     * Toggles the visibility state of this layer.
415     */
416    public void toggleVisible() {
417        setVisible(!isVisible());
418    }
419
420    /**
421     * Adds a {@link PropertyChangeListener}
422     *
423     * @param listener the listener
424     */
425    public void addPropertyChangeListener(PropertyChangeListener listener) {
426        propertyChangeSupport.addPropertyChangeListener(listener);
427    }
428
429    /**
430     * Removes a {@link PropertyChangeListener}
431     *
432     * @param listener the listener
433     */
434    public void removePropertyChangeListener(PropertyChangeListener listener) {
435        propertyChangeSupport.removePropertyChangeListener(listener);
436    }
437
438    /**
439     * fires a property change for the property {@link #VISIBLE_PROP}
440     *
441     * @param oldValue the old value
442     * @param newValue the new value
443     */
444    protected void fireVisibleChanged(boolean oldValue, boolean newValue) {
445        propertyChangeSupport.firePropertyChange(VISIBLE_PROP, oldValue, newValue);
446    }
447
448    /**
449     * fires a property change for the property {@link #OPACITY_PROP}
450     *
451     * @param oldValue the old value
452     * @param newValue the new value
453     */
454    protected void fireOpacityChanged(double oldValue, double newValue) {
455        propertyChangeSupport.firePropertyChange(OPACITY_PROP, oldValue, newValue);
456    }
457
458    /**
459     * fires a property change for the property {@link #FILTER_STATE_PROP}.
460     */
461    protected void fireFilterStateChanged() {
462        propertyChangeSupport.firePropertyChange(FILTER_STATE_PROP, null, null);
463    }
464
465    /**
466     * Check changed status of layer
467     *
468     * @return True if layer was changed since last paint
469     * @deprecated This is not supported by multiple map views.
470     * Fire an {@link #invalidate()} to trigger a repaint.
471     * Let this method return false if you only use invalidation events.
472     */
473    @Deprecated
474    public boolean isChanged() {
475        return true;
476    }
477
478    /**
479     * allows to check whether a projection is supported or not
480     * @param proj projection
481     *
482     * @return True if projection is supported for this layer
483     */
484    public boolean isProjectionSupported(Projection proj) {
485        return proj != null;
486    }
487
488    /**
489     * Specify user information about projections
490     *
491     * @return User readable text telling about supported projections
492     */
493    public String nameSupportedProjections() {
494        return tr("All projections are supported");
495    }
496
497    /**
498     * The action to save a layer
499     */
500    public static class LayerSaveAction extends AbstractAction {
501        private final transient Layer layer;
502
503        public LayerSaveAction(Layer layer) {
504            putValue(SMALL_ICON, ImageProvider.get("save"));
505            putValue(SHORT_DESCRIPTION, tr("Save the current data."));
506            putValue(NAME, tr("Save"));
507            setEnabled(true);
508            this.layer = layer;
509        }
510
511        @Override
512        public void actionPerformed(ActionEvent e) {
513            SaveAction.getInstance().doSave(layer);
514        }
515    }
516
517    public static class LayerSaveAsAction extends AbstractAction {
518        private final transient Layer layer;
519
520        public LayerSaveAsAction(Layer layer) {
521            putValue(SMALL_ICON, ImageProvider.get("save_as"));
522            putValue(SHORT_DESCRIPTION, tr("Save the current data to a new file."));
523            putValue(NAME, tr("Save As..."));
524            setEnabled(true);
525            this.layer = layer;
526        }
527
528        @Override
529        public void actionPerformed(ActionEvent e) {
530            SaveAsAction.getInstance().doSave(layer);
531        }
532    }
533
534    public static class LayerGpxExportAction extends AbstractAction {
535        private final transient Layer layer;
536
537        public LayerGpxExportAction(Layer layer) {
538            putValue(SMALL_ICON, ImageProvider.get("exportgpx"));
539            putValue(SHORT_DESCRIPTION, tr("Export the data to GPX file."));
540            putValue(NAME, tr("Export to GPX..."));
541            setEnabled(true);
542            this.layer = layer;
543        }
544
545        @Override
546        public void actionPerformed(ActionEvent e) {
547            new GpxExportAction().export(layer);
548        }
549    }
550
551    /* --------------------------------------------------------------------------------- */
552    /* interface ProjectionChangeListener                                                */
553    /* --------------------------------------------------------------------------------- */
554    @Override
555    public void projectionChanged(Projection oldValue, Projection newValue) {
556        if (!isProjectionSupported(newValue)) {
557            final String message = "<html><body><p>" +
558                    tr("The layer {0} does not support the new projection {1}.", getName(), newValue.toCode()) + "</p>" +
559                    "<p style='width: 450px;'>" + tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" +
560                    tr("Change the projection again or remove the layer.");
561
562            // run later to not block loading the UI.
563            SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(Main.parent,
564                    message,
565                    tr("Warning"),
566                    JOptionPane.WARNING_MESSAGE));
567        }
568    }
569
570    /**
571     * Initializes the layer after a successful load of data from a file
572     * @since 5459
573     */
574    public void onPostLoadFromFile() {
575        // To be overriden if needed
576    }
577
578    /**
579     * Replies the savable state of this layer (i.e if it can be saved through a "File-&gt;Save" dialog).
580     * @return true if this layer can be saved to a file
581     * @since 5459
582     */
583    public boolean isSavable() {
584        return false;
585    }
586
587    /**
588     * Checks whether it is ok to launch a save (whether we have data, there is no conflict etc.)
589     * @return <code>true</code>, if it is safe to save.
590     * @since 5459
591     */
592    public boolean checkSaveConditions() {
593        return true;
594    }
595
596    /**
597     * Creates a new "Save" dialog for this layer and makes it visible.<br>
598     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
599     * @return The output {@code File}
600     * @see SaveActionBase#createAndOpenSaveFileChooser
601     * @since 5459
602     */
603    public File createAndOpenSaveFileChooser() {
604        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Layer"), "lay");
605    }
606
607    /**
608     * Gets the strategy that specifies where this layer should be inserted in a layer list.
609     * @return That strategy.
610     * @since 10008
611     */
612    public LayerPositionStrategy getDefaultLayerPosition() {
613        if (isBackgroundLayer()) {
614            return LayerPositionStrategy.BEFORE_FIRST_BACKGROUND_LAYER;
615        } else {
616            return LayerPositionStrategy.AFTER_LAST_VALIDATION_LAYER;
617        }
618    }
619
620    /**
621     * Gets the {@link ProjectionBounds} for this layer to be visible to the user. This can be the exact bounds, the UI handles padding. Return
622     * <code>null</code> if you cannot provide this information. The default implementation uses the bounds from
623     * {@link #visitBoundingBox(BoundingXYVisitor)}.
624     * @return The bounds for this layer.
625     * @since 10371
626     */
627    public ProjectionBounds getViewProjectionBounds() {
628        BoundingXYVisitor v = new BoundingXYVisitor();
629        visitBoundingBox(v);
630        return v.getBounds();
631    }
632
633    @Override
634    public String toString() {
635        return getClass().getSimpleName() + " [name=" + name + ", associatedFile=" + associatedFile + ']';
636    }
637}