source: josm/trunk/src/org/openstreetmap/josm/gui/layer/Layer.java @ 12718

Last change on this file since 12718 was 12718, checked in by Don-vip, 3 weeks ago

see #13036 - see #15229 - see #15182 - make Commands depends only on a DataSet, not a Layer. This removes a lot of GUI dependencies

  • Property svn:eol-style set to native
File size: 21.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.awt.Component;
8import java.awt.event.ActionEvent;
9import java.beans.PropertyChangeListener;
10import java.beans.PropertyChangeSupport;
11import java.io.File;
12import java.util.List;
13import java.util.Optional;
14
15import javax.swing.AbstractAction;
16import javax.swing.Action;
17import javax.swing.Icon;
18import javax.swing.JOptionPane;
19import javax.swing.JSeparator;
20import javax.swing.SwingUtilities;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.actions.GpxExportAction;
24import org.openstreetmap.josm.actions.SaveAction;
25import org.openstreetmap.josm.actions.SaveActionBase;
26import org.openstreetmap.josm.actions.SaveAsAction;
27import org.openstreetmap.josm.data.ProjectionBounds;
28import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
29import org.openstreetmap.josm.data.preferences.AbstractProperty;
30import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
31import org.openstreetmap.josm.data.preferences.ColorProperty;
32import org.openstreetmap.josm.data.projection.Projection;
33import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
34import org.openstreetmap.josm.tools.Destroyable;
35import org.openstreetmap.josm.tools.ImageProvider;
36import org.openstreetmap.josm.tools.Utils;
37
38/**
39 * A layer encapsulates the gui component of one dataset and its representation.
40 *
41 * Some layers may display data directly imported from OSM server. Other only
42 * display background images. Some can be edited, some not. Some are static and
43 * other changes dynamically (auto-updated).
44 *
45 * Layers can be visible or not. Most actions the user can do applies only on
46 * selected layers. The available actions depend on the selected layers too.
47 *
48 * All layers are managed by the MapView. They are displayed in a list to the
49 * right of the screen.
50 *
51 * @author imi
52 */
53public abstract class Layer extends AbstractMapViewPaintable implements Destroyable, ProjectionChangeListener {
54
55    /**
56     * Action related to a single layer.
57     */
58    public interface LayerAction {
59
60        /**
61         * Determines if this action supports a given list of layers.
62         * @param layers list of layers
63         * @return {@code true} if this action supports the given list of layers, {@code false} otherwise
64         */
65        boolean supportLayers(List<Layer> layers);
66
67        /**
68         * Creates and return the menu component.
69         * @return the menu component
70         */
71        Component createMenuComponent();
72    }
73
74    /**
75     * Action related to several layers.
76     * @since 10600 (functional interface)
77     */
78    @FunctionalInterface
79    public interface MultiLayerAction {
80
81        /**
82         * Returns the action for a given list of layers.
83         * @param layers list of layers
84         * @return the action for the given list of layers
85         */
86        Action getMultiLayerAction(List<Layer> layers);
87    }
88
89    /**
90     * Special class that can be returned by getMenuEntries when JSeparator needs to be created
91     */
92    public static class SeparatorLayerAction extends AbstractAction implements LayerAction {
93        /** Unique instance */
94        public static final SeparatorLayerAction INSTANCE = new SeparatorLayerAction();
95
96        @Override
97        public void actionPerformed(ActionEvent e) {
98            throw new UnsupportedOperationException();
99        }
100
101        @Override
102        public Component createMenuComponent() {
103            return new JSeparator();
104        }
105
106        @Override
107        public boolean supportLayers(List<Layer> layers) {
108            return false;
109        }
110    }
111
112    /**
113     * The visibility property for this layer. May be <code>true</code> (visible) or <code>false</code> (hidden).
114     */
115    public static final String VISIBLE_PROP = Layer.class.getName() + ".visible";
116    /**
117     * The opacity of this layer. A number between 0 and 1
118     */
119    public static final String OPACITY_PROP = Layer.class.getName() + ".opacity";
120    /**
121     * The name property of the layer.
122     * You can listen to name changes by listening to changes to this property.
123     */
124    public static final String NAME_PROP = Layer.class.getName() + ".name";
125    /**
126     * Property that defines the filter state.
127     * This is currently not used.
128     */
129    public static final String FILTER_STATE_PROP = Layer.class.getName() + ".filterstate";
130
131    /**
132     * keeps track of property change listeners
133     */
134    protected PropertyChangeSupport propertyChangeSupport;
135
136    /**
137     * The visibility state of the layer.
138     */
139    private boolean visible = true;
140
141    /**
142     * The opacity of the layer.
143     */
144    private double opacity = 1;
145
146    /**
147     * The layer should be handled as a background layer in automatic handling
148     */
149    private boolean background;
150
151    /**
152     * The name of this layer.
153     */
154    private String name;
155
156    /**
157     * This is set if user renamed this layer.
158     */
159    private boolean renamed;
160
161    /**
162     * If a file is associated with this layer, this variable should be set to it.
163     */
164    private File associatedFile;
165
166    private final ValueChangeListener<Object> invalidateListener = change -> invalidate();
167    private boolean isDestroyed;
168
169    /**
170     * Create the layer and fill in the necessary components.
171     * @param name Layer name
172     */
173    public Layer(String name) {
174        this.propertyChangeSupport = new PropertyChangeSupport(this);
175        setName(name);
176    }
177
178    /**
179     * Initialization code, that depends on Main.map.mapView.
180     *
181     * It is always called in the event dispatching thread.
182     * Note that Main.map is null as long as no layer has been added, so do
183     * not execute code in the constructor, that assumes Main.map.mapView is
184     * not null.
185     *
186     * If you need to execute code when this layer is added to the map view, use
187     * {@link #attachToMapView(org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent)}
188     */
189    public void hookUpMapView() {
190    }
191
192    /**
193     * Return a representative small image for this layer. The image must not
194     * be larger than 64 pixel in any dimension.
195     * @return layer icon
196     */
197    public abstract Icon getIcon();
198
199    /**
200     * Gets the color property to use for this layer.
201     * @return The color property.
202     * @since 10824
203     */
204    public AbstractProperty<Color> getColorProperty() {
205        ColorProperty base = getBaseColorProperty();
206        if (base != null) {
207            // cannot cache this - name may change.
208            return base.getChildColor("layer " + 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 ColorProperty 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     * Check changed status of layer
505     *
506     * @return True if layer was changed since last paint
507     * @deprecated This is not supported by multiple map views.
508     * Fire an {@link #invalidate()} to trigger a repaint.
509     */
510    @Deprecated
511    public boolean isChanged() {
512        return false;
513    }
514
515    /**
516     * allows to check whether a projection is supported or not
517     * @param proj projection
518     *
519     * @return True if projection is supported for this layer
520     */
521    public boolean isProjectionSupported(Projection proj) {
522        return proj != null;
523    }
524
525    /**
526     * Specify user information about projections
527     *
528     * @return User readable text telling about supported projections
529     */
530    public String nameSupportedProjections() {
531        return tr("All projections are supported");
532    }
533
534    /**
535     * The action to save a layer
536     */
537    public static class LayerSaveAction extends AbstractAction {
538        private final transient Layer layer;
539
540        /**
541         * Create a new action that saves the layer
542         * @param layer The layer to save.
543         */
544        public LayerSaveAction(Layer layer) {
545            putValue(SMALL_ICON, ImageProvider.get("save"));
546            putValue(SHORT_DESCRIPTION, tr("Save the current data."));
547            putValue(NAME, tr("Save"));
548            setEnabled(true);
549            this.layer = layer;
550        }
551
552        @Override
553        public void actionPerformed(ActionEvent e) {
554            SaveAction.getInstance().doSave(layer);
555        }
556    }
557
558    /**
559     * Action to save the layer in a new file
560     */
561    public static class LayerSaveAsAction extends AbstractAction {
562        private final transient Layer layer;
563
564        /**
565         * Create a new save as action
566         * @param layer The layer that should be saved.
567         */
568        public LayerSaveAsAction(Layer layer) {
569            putValue(SMALL_ICON, ImageProvider.get("save_as"));
570            putValue(SHORT_DESCRIPTION, tr("Save the current data to a new file."));
571            putValue(NAME, tr("Save As..."));
572            setEnabled(true);
573            this.layer = layer;
574        }
575
576        @Override
577        public void actionPerformed(ActionEvent e) {
578            SaveAsAction.getInstance().doSave(layer);
579        }
580    }
581
582    /**
583     * Action that exports the layer as gpx file
584     */
585    public static class LayerGpxExportAction extends AbstractAction {
586        private final transient Layer layer;
587
588        /**
589         * Create a new gpx export action for the given layer.
590         * @param layer The layer
591         */
592        public LayerGpxExportAction(Layer layer) {
593            putValue(SMALL_ICON, ImageProvider.get("exportgpx"));
594            putValue(SHORT_DESCRIPTION, tr("Export the data to GPX file."));
595            putValue(NAME, tr("Export to GPX..."));
596            setEnabled(true);
597            this.layer = layer;
598        }
599
600        @Override
601        public void actionPerformed(ActionEvent e) {
602            new GpxExportAction().export(layer);
603        }
604    }
605
606    /* --------------------------------------------------------------------------------- */
607    /* interface ProjectionChangeListener                                                */
608    /* --------------------------------------------------------------------------------- */
609    @Override
610    public void projectionChanged(Projection oldValue, Projection newValue) {
611        if (!isProjectionSupported(newValue)) {
612            final String message = "<html><body><p>" +
613                    tr("The layer {0} does not support the new projection {1}.",
614                            Utils.escapeReservedCharactersHTML(getName()), newValue.toCode()) + "</p>" +
615                    "<p style='width: 450px;'>" + tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" +
616                    tr("Change the projection again or remove the layer.");
617
618            // run later to not block loading the UI.
619            SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(Main.parent,
620                    message,
621                    tr("Warning"),
622                    JOptionPane.WARNING_MESSAGE));
623        }
624    }
625
626    /**
627     * Initializes the layer after a successful load of data from a file
628     * @since 5459
629     */
630    public void onPostLoadFromFile() {
631        // To be overriden if needed
632    }
633
634    /**
635     * Replies the savable state of this layer (i.e if it can be saved through a "File-&gt;Save" dialog).
636     * @return true if this layer can be saved to a file
637     * @since 5459
638     */
639    public boolean isSavable() {
640        return false;
641    }
642
643    /**
644     * Checks whether it is ok to launch a save (whether we have data, there is no conflict etc.)
645     * @return <code>true</code>, if it is safe to save.
646     * @since 5459
647     */
648    public boolean checkSaveConditions() {
649        return true;
650    }
651
652    /**
653     * Creates a new "Save" dialog for this layer and makes it visible.<br>
654     * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
655     * @return The output {@code File}
656     * @see SaveActionBase#createAndOpenSaveFileChooser
657     * @since 5459
658     */
659    public File createAndOpenSaveFileChooser() {
660        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Layer"), "lay");
661    }
662
663    /**
664     * Gets the strategy that specifies where this layer should be inserted in a layer list.
665     * @return That strategy.
666     * @since 10008
667     */
668    public LayerPositionStrategy getDefaultLayerPosition() {
669        if (isBackgroundLayer()) {
670            return LayerPositionStrategy.BEFORE_FIRST_BACKGROUND_LAYER;
671        } else {
672            return LayerPositionStrategy.AFTER_LAST_VALIDATION_LAYER;
673        }
674    }
675
676    /**
677     * Gets the {@link ProjectionBounds} for this layer to be visible to the user. This can be the exact bounds, the UI handles padding. Return
678     * <code>null</code> if you cannot provide this information. The default implementation uses the bounds from
679     * {@link #visitBoundingBox(BoundingXYVisitor)}.
680     * @return The bounds for this layer.
681     * @since 10371
682     */
683    public ProjectionBounds getViewProjectionBounds() {
684        BoundingXYVisitor v = new BoundingXYVisitor();
685        visitBoundingBox(v);
686        return v.getBounds();
687    }
688
689    @Override
690    public String toString() {
691        return getClass().getSimpleName() + " [name=" + name + ", associatedFile=" + associatedFile + ']';
692    }
693}
Note: See TracBrowser for help on using the repository browser.