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    public 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     * @return whether the layer has / can handle colors.
198     * @since 15496
199     */
200    public boolean hasColor() {
201        return false;
202    }
203
204    /**
205     * Return the current color of the layer
206     * @return null when not present or not supported
207     * @since 15496
208     */
209    public Color getColor() {
210        return null;
211    }
212
213    /**
214     * Sets the color for this layer. Nothing happens if not supported by the layer
215     * @param color the color to be set, <code>null</code> for default
216     * @since 15496
217     */
218    public void setColor(Color color) {
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    /**
242     * Visits the content bounds of this layer. The behavior of this method depends on the layer,
243     * but each implementation should attempt to cover the relevant content of the layer in this method.
244     * @param v The visitor that gets notified about the contents of this layer.
245     */
246    public abstract void visitBoundingBox(BoundingXYVisitor v);
247
248    /**
249     * Gets the layer information to display to the user.
250     * This is used if the user requests information about this layer.
251     * It should display a description of the layer content.
252     * @return Either a String or a {@link Component} describing the layer.
253     */
254    public abstract Object getInfoComponent();
255
256    /**
257     * Determines if info dialog can be resized (false by default).
258     * @return {@code true} if the info dialog can be resized, {@code false} otherwise
259     * @since 6708
260     */
261    public boolean isInfoResizable() {
262        return false;
263    }
264
265    /**
266     * Returns list of actions. Action can implement LayerAction interface when it needs to be represented by other
267     * menu component than JMenuItem or when it supports multiple layers. Actions that support multiple layers should also
268     * have correct equals implementation.
269     *
270     * Use {@link SeparatorLayerAction#INSTANCE} instead of new JSeparator
271     * @return menu actions for this layer
272     */
273    public abstract Action[] getMenuEntries();
274
275    /**
276     * Called, when the layer is removed from the mapview and is going to be destroyed.
277     *
278     * This is because the Layer constructor can not add itself safely as listener
279     * to the layerlist dialog, because there may be no such dialog yet (loaded
280     * via command line parameter).
281     */
282    @Override
283    public synchronized void destroy() {
284        if (isDestroyed) {
285            throw new IllegalStateException("The layer has already been destroyed: " + this);
286        }
287        isDestroyed = true;
288        // Override in subclasses if needed
289    }
290
291    /**
292     * Gets the associated file for this layer.
293     * @return The file or <code>null</code> if it is unset.
294     * @see #setAssociatedFile(File)
295     */
296    public File getAssociatedFile() {
297        return associatedFile;
298    }
299
300    /**
301     * Sets the associated file for this layer.
302     *
303     * The associated file might be the one that the user opened.
304     * @param file The file, may be <code>null</code>
305     */
306    public void setAssociatedFile(File file) {
307        associatedFile = file;
308    }
309
310    /**
311     * Replies the name of the layer
312     *
313     * @return the name of the layer
314     */
315    public String getName() {
316        return name;
317    }
318
319    /**
320     * Sets the name of the layer
321     *
322     * @param name the name. If null, the name is set to the empty string.
323     */
324    public void setName(String name) {
325        String oldValue = this.name;
326        this.name = Optional.ofNullable(name).orElse("");
327        if (!this.name.equals(oldValue)) {
328            propertyChangeSupport.firePropertyChange(NAME_PROP, oldValue, this.name);
329        }
330        invalidate();
331    }
332
333    /**
334     * Rename layer and set renamed flag to mark it as renamed (has user given name).
335     *
336     * @param name the name. If null, the name is set to the empty string.
337     */
338    public final void rename(String name) {
339        renamed = true;
340        setName(name);
341    }
342
343    /**
344     * Replies true if this layer was renamed by user
345     *
346     * @return true if this layer was renamed by user
347     */
348    public boolean isRenamed() {
349        return renamed;
350    }
351
352    /**
353     * Replies true if this layer is a background layer
354     *
355     * @return true if this layer is a background layer
356     */
357    public boolean isBackgroundLayer() {
358        return background;
359    }
360
361    /**
362     * Sets whether this layer is a background layer
363     *
364     * @param background true, if this layer is a background layer
365     */
366    public void setBackgroundLayer(boolean background) {
367        this.background = background;
368    }
369
370    /**
371     * Sets the visibility of this layer. Emits property change event for
372     * property {@link #VISIBLE_PROP}.
373     *
374     * @param visible true, if the layer is visible; false, otherwise.
375     */
376    public void setVisible(boolean visible) {
377        boolean oldValue = isVisible();
378        this.visible = visible;
379        if (visible && opacity == 0) {
380            setOpacity(1);
381        } else if (oldValue != isVisible()) {
382            fireVisibleChanged(oldValue, isVisible());
383        }
384    }
385
386    /**
387     * Replies true if this layer is visible. False, otherwise.
388     * @return  true if this layer is visible. False, otherwise.
389     */
390    public boolean isVisible() {
391        return visible && opacity != 0;
392    }
393
394    /**
395     * Gets the opacity of the layer, in range 0...1
396     * @return The opacity
397     */
398    public double getOpacity() {
399        return opacity;
400    }
401
402    /**
403     * Sets the opacity of the layer, in range 0...1
404     * @param opacity The opacity
405     * @throws IllegalArgumentException if the opacity is out of range
406     */
407    public void setOpacity(double opacity) {
408        if (!(opacity >= 0 && opacity <= 1))
409            throw new IllegalArgumentException("Opacity value must be between 0 and 1");
410        double oldOpacity = getOpacity();
411        boolean oldVisible = isVisible();
412        this.opacity = opacity;
413        if (!Utils.equalsEpsilon(oldOpacity, getOpacity())) {
414            fireOpacityChanged(oldOpacity, getOpacity());
415        }
416        if (oldVisible != isVisible()) {
417            fireVisibleChanged(oldVisible, isVisible());
418        }
419    }
420
421    /**
422     * Sets new state to the layer after applying {@link ImageProcessor}.
423     */
424    public void setFilterStateChanged() {
425        fireFilterStateChanged();
426    }
427
428    /**
429     * Toggles the visibility state of this layer.
430     */
431    public void toggleVisible() {
432        setVisible(!isVisible());
433    }
434
435    /**
436     * Adds a {@link PropertyChangeListener}
437     *
438     * @param listener the listener
439     */
440    public void addPropertyChangeListener(PropertyChangeListener listener) {
441        propertyChangeSupport.addPropertyChangeListener(listener);
442    }
443
444    /**
445     * Removes a {@link PropertyChangeListener}
446     *
447     * @param listener the listener
448     */
449    public void removePropertyChangeListener(PropertyChangeListener listener) {
450        propertyChangeSupport.removePropertyChangeListener(listener);
451    }
452
453    /**
454     * fires a property change for the property {@link #VISIBLE_PROP}
455     *
456     * @param oldValue the old value
457     * @param newValue the new value
458     */
459    protected void fireVisibleChanged(boolean oldValue, boolean newValue) {
460        propertyChangeSupport.firePropertyChange(VISIBLE_PROP, oldValue, newValue);
461    }
462
463    /**
464     * fires a property change for the property {@link #OPACITY_PROP}
465     *
466     * @param oldValue the old value
467     * @param newValue the new value
468     */
469    protected void fireOpacityChanged(double oldValue, double newValue) {
470        propertyChangeSupport.firePropertyChange(OPACITY_PROP, oldValue, newValue);
471    }
472
473    /**
474     * fires a property change for the property {@link #FILTER_STATE_PROP}.
475     */
476    protected void fireFilterStateChanged() {
477        propertyChangeSupport.firePropertyChange(FILTER_STATE_PROP, null, null);
478    }
479
480    /**
481     * allows to check whether a projection is supported or not
482     * @param proj projection
483     *
484     * @return True if projection is supported for this layer
485     */
486    public boolean isProjectionSupported(Projection proj) {
487        return proj != null;
488    }
489
490    /**
491     * Specify user information about projections
492     *
493     * @return User readable text telling about supported projections
494     */
495    public String nameSupportedProjections() {
496        return tr("All projections are supported");
497    }
498
499    /**
500     * The action to save a layer
501     */
502    public static class LayerSaveAction extends AbstractAction {
503        private final transient Layer layer;
504
505        /**
506         * Create a new action that saves the layer
507         * @param layer The layer to save.
508         */
509        public LayerSaveAction(Layer layer) {
510            new ImageProvider("save").getResource().attachImageIcon(this, true);
511            putValue(SHORT_DESCRIPTION, tr("Save the current data."));
512            putValue(NAME, tr("Save"));
513            setEnabled(true);
514            this.layer = layer;
515        }
516
517        @Override
518        public void actionPerformed(ActionEvent e) {
519            SaveAction.getInstance().doSave(layer, true);
520        }
521    }
522
523    /**
524     * Action to save the layer in a new file
525     */
526    public static class LayerSaveAsAction extends AbstractAction {
527        private final transient Layer layer;
528
529        /**
530         * Create a new save as action
531         * @param layer The layer that should be saved.
532         */
533        public LayerSaveAsAction(Layer layer) {
534            new ImageProvider("save_as").getResource().attachImageIcon(this, true);
535            putValue(SHORT_DESCRIPTION, tr("Save the current data to a new file."));
536            putValue(NAME, tr("Save As..."));
537            setEnabled(true);
538            this.layer = layer;
539        }
540
541        @Override
542        public void actionPerformed(ActionEvent e) {
543            SaveAsAction.getInstance().doSave(layer);
544        }
545    }
546
547    /**
548     * Action that exports the layer as gpx file
549     */
550    public static class LayerGpxExportAction extends AbstractAction {
551        private final transient Layer layer;
552
553        /**
554         * Create a new gpx export action for the given layer.
555         * @param layer The layer
556         */
557        public LayerGpxExportAction(Layer layer) {
558            new ImageProvider("exportgpx").getResource().attachImageIcon(this, true);
559            putValue(SHORT_DESCRIPTION, tr("Export the data to GPX file."));
560            putValue(NAME, tr("Export to GPX..."));
561            setEnabled(true);
562            this.layer = layer;
563        }
564
565        @Override
566        public void actionPerformed(ActionEvent e) {
567            new GpxExportAction().export(layer);
568        }
569    }
570
571    /* --------------------------------------------------------------------------------- */
572    /* interface ProjectionChangeListener                                                */
573    /* --------------------------------------------------------------------------------- */
574    @Override
575    public void projectionChanged(Projection oldValue, Projection newValue) {
576        if (!isProjectionSupported(newValue)) {
577            final String message = "<html><body><p>" +
578                    tr("The layer {0} does not support the new projection {1}.",
579                            Utils.escapeReservedCharactersHTML(getName()), newValue.toCode()) + "</p>" +
580                    "<p style='width: 450px;'>" + tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" +
581                    tr("Change the projection again or remove the layer.");
582
583            // run later to not block loading the UI.
584            SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
585                    message,
586                    tr("Warning"),
587                    JOptionPane.WARNING_MESSAGE));
588        }
589    }
590
591    /**
592     * Initializes the layer after a successful load of data from a file
593     * @since 5459
594     */
595    public void onPostLoadFromFile() {
596        // To be overriden if needed
597    }
598
599    /**
600     * Replies the savable state of this layer (i.e if it can be saved through a "File-&gt;Save" dialog).
601     * @return true if this layer can be saved to a file
602     * @since 5459
603     */
604    public boolean isSavable() {
605        return false;
606    }
607
608    /**
609     * Checks whether it is ok to launch a save (whether we have data, there is no conflict etc.)
610     * @return <code>true</code>, if it is safe to save.
611     * @since 5459
612     */
613    public boolean checkSaveConditions() {
614        return true;
615    }
616
617    /**
618     * Creates a new "Save" dialog for this layer and makes it visible.<br>
619     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
620     * @return The output {@code File}
621     * @see SaveActionBase#createAndOpenSaveFileChooser
622     * @since 5459
623     */
624    public File createAndOpenSaveFileChooser() {
625        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Layer"), "lay");
626    }
627
628    /**
629     * Gets the strategy that specifies where this layer should be inserted in a layer list.
630     * @return That strategy.
631     * @since 10008
632     */
633    public LayerPositionStrategy getDefaultLayerPosition() {
634        if (isBackgroundLayer()) {
635            return LayerPositionStrategy.BEFORE_FIRST_BACKGROUND_LAYER;
636        } else {
637            return LayerPositionStrategy.AFTER_LAST_VALIDATION_LAYER;
638        }
639    }
640
641    /**
642     * Gets the {@link ProjectionBounds} for this layer to be visible to the user. This can be the exact bounds, the UI handles padding. Return
643     * <code>null</code> if you cannot provide this information. The default implementation uses the bounds from
644     * {@link #visitBoundingBox(BoundingXYVisitor)}.
645     * @return The bounds for this layer.
646     * @since 10371
647     */
648    public ProjectionBounds getViewProjectionBounds() {
649        BoundingXYVisitor v = new BoundingXYVisitor();
650        visitBoundingBox(v);
651        return v.getBounds();
652    }
653
654    /**
655     * Get the source for the layer
656     * @return The string for the changeset source tag or {@code null}
657     * @since 15371
658     */
659    public String getChangesetSourceTag() {
660        return null;
661    }
662
663    @Override
664    public String toString() {
665        return getClass().getSimpleName() + " [name=" + name + ", associatedFile=" + associatedFile + ']';
666    }
667}