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