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