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