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