001    // License: GPL. See LICENSE file for details.
002    
003    package org.openstreetmap.josm.gui.layer;
004    
005    import static org.openstreetmap.josm.tools.I18n.tr;
006    
007    import java.awt.Color;
008    import java.awt.Component;
009    import java.awt.Graphics2D;
010    import java.awt.event.ActionEvent;
011    import java.beans.PropertyChangeListener;
012    import java.beans.PropertyChangeSupport;
013    import java.io.File;
014    import java.util.List;
015    
016    import javax.swing.AbstractAction;
017    import javax.swing.Action;
018    import javax.swing.Icon;
019    import javax.swing.JOptionPane;
020    import javax.swing.JSeparator;
021    
022    import org.openstreetmap.josm.Main;
023    import org.openstreetmap.josm.actions.GpxExportAction;
024    import org.openstreetmap.josm.actions.SaveAction;
025    import org.openstreetmap.josm.actions.SaveActionBase;
026    import org.openstreetmap.josm.actions.SaveAsAction;
027    import org.openstreetmap.josm.data.Bounds;
028    import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
029    import org.openstreetmap.josm.data.projection.Projection;
030    import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
031    import org.openstreetmap.josm.gui.MapView;
032    import org.openstreetmap.josm.tools.Destroyable;
033    import org.openstreetmap.josm.tools.ImageProvider;
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     */
050    abstract public class Layer implements Destroyable, MapViewPaintable, ProjectionChangeListener {
051    
052        public interface LayerAction {
053            boolean supportLayers(List<Layer> layers);
054            Component createMenuComponent();
055        }
056    
057        public interface MultiLayerAction {
058            Action getMultiLayerAction(List<Layer> layers);
059        }
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            @Override
069            public void actionPerformed(ActionEvent e) {
070                throw new UnsupportedOperationException();
071            }
072            @Override
073            public Component createMenuComponent() {
074                return new JSeparator();
075            }
076            @Override
077            public boolean supportLayers(List<Layer> layers) {
078                return false;
079            }
080        }
081    
082        static public final String VISIBLE_PROP = Layer.class.getName() + ".visible";
083        static public final String OPACITY_PROP = Layer.class.getName() + ".opacity";
084        static public final String NAME_PROP = Layer.class.getName() + ".name";
085    
086        static public final int ICON_SIZE = 16;
087    
088        /** keeps track of property change listeners */
089        protected PropertyChangeSupport propertyChangeSupport;
090    
091        /**
092         * The visibility state of the layer.
093         *
094         */
095        private boolean visible = true;
096    
097        /**
098         * The opacity of the layer.
099         *
100         */
101        private double opacity = 1;
102    
103        /**
104         * The layer should be handled as a background layer in automatic handling
105         *
106         */
107        private boolean background = false;
108    
109        /**
110         * The name of this layer.
111         *
112         */
113        private  String name;
114    
115        /**
116         * If a file is associated with this layer, this variable should be set to it.
117         */
118        private File associatedFile;
119    
120        /**
121         * Create the layer and fill in the necessary components.
122         */
123        public Layer(String name) {
124            this.propertyChangeSupport = new PropertyChangeSupport(this);
125            setName(name);
126        }
127    
128        /**
129         * Initialization code, that depends on Main.map.mapView.
130         *
131         * It is always called in the event dispatching thread.
132         * Note that Main.map is null as long as no layer has been added, so do
133         * not execute code in the constructor, that assumes Main.map.mapView is
134         * not null. Instead override this method.
135         */
136        public void hookUpMapView() {
137        }
138    
139        /**
140         * Paint the dataset using the engine set.
141         * @param mv The object that can translate GeoPoints to screen coordinates.
142         */
143        @Override
144        abstract public void paint(Graphics2D g, MapView mv, Bounds box);
145        /**
146         * Return a representative small image for this layer. The image must not
147         * be larger than 64 pixel in any dimension.
148         */
149        abstract public Icon getIcon();
150    
151        /**
152         * Return a Color for this layer. Return null when no color specified.
153         * @param ignoreCustom Custom color should return null, as no default color
154         *      is used. When this is true, then even for custom coloring the base
155         *      color is returned - mainly for layer internal use.
156         */
157        public Color getColor(boolean ignoreCustom) {
158            return null;
159        }
160    
161        /**
162         * @return A small tooltip hint about some statistics for this layer.
163         */
164        abstract public String getToolTipText();
165    
166        /**
167         * Merges the given layer into this layer. Throws if the layer types are
168         * incompatible.
169         * @param from The layer that get merged into this one. After the merge,
170         *      the other layer is not usable anymore and passing to one others
171         *      mergeFrom should be one of the last things to do with a layer.
172         */
173        abstract public void mergeFrom(Layer from);
174    
175        /**
176         * @param other The other layer that is tested to be mergable with this.
177         * @return Whether the other layer can be merged into this layer.
178         */
179        abstract public boolean isMergable(Layer other);
180    
181        abstract public void visitBoundingBox(BoundingXYVisitor v);
182    
183        abstract public Object getInfoComponent();
184    
185        /**
186         * Returns list of actions. Action can implement LayerAction interface when it needs to be represented by other
187         * menu component than JMenuItem or when it supports multiple layers. Actions that support multiple layers should also
188         * have correct equals implementation.
189         *
190         * Use SeparatorLayerAction.INSTANCE instead of new JSeparator
191         *
192         */
193        abstract public Action[] getMenuEntries();
194    
195        /**
196         * Called, when the layer is removed from the mapview and is going to be
197         * destroyed.
198         *
199         * This is because the Layer constructor can not add itself safely as listener
200         * to the layerlist dialog, because there may be no such dialog yet (loaded
201         * via command line parameter).
202         */
203        @Override
204        public void destroy() {}
205    
206        public File getAssociatedFile() { return associatedFile; }
207        public void setAssociatedFile(File file) { associatedFile = file; }
208    
209        /**
210         * Replies the name of the layer
211         *
212         * @return the name of the layer
213         */
214        public String getName() {
215            return name;
216        }
217    
218        /**
219         * Sets the name of the layer
220         *
221         *@param name the name. If null, the name is set to the empty string.
222         *
223         */
224        public void setName(String name) {
225            if (name == null) {
226                name = "";
227            }
228            String oldValue = this.name;
229            this.name = name;
230            if (!this.name.equals(oldValue)) {
231                propertyChangeSupport.firePropertyChange(NAME_PROP, oldValue, this.name);
232            }
233        }
234    
235        /**
236         * Replies true if this layer is a background layer
237         *
238         * @return true if this layer is a background layer
239         */
240        public boolean isBackgroundLayer() {
241            return background;
242        }
243    
244        /**
245         * Sets whether this layer is a background layer
246         *
247         * @param background true, if this layer is a background layer
248         */
249        public void setBackgroundLayer(boolean background) {
250            this.background = background;
251        }
252    
253        /**
254         * Sets the visibility of this layer. Emits property change event for
255         * property {@link #VISIBLE_PROP}.
256         *
257         * @param visible true, if the layer is visible; false, otherwise.
258         */
259        public void setVisible(boolean visible) {
260            boolean oldValue = isVisible();
261            this.visible  = visible;
262            if (visible && opacity == 0) {
263                setOpacity(1);
264            } else if (oldValue != isVisible()) {
265                fireVisibleChanged(oldValue, isVisible());
266            }
267        }
268    
269        /**
270         * Replies true if this layer is visible. False, otherwise.
271         * @return  true if this layer is visible. False, otherwise.
272         */
273        public boolean isVisible() {
274            return visible && opacity != 0;
275        }
276    
277        public double getOpacity() {
278            return opacity;
279        }
280    
281        public void setOpacity(double opacity) {
282            if (!(opacity >= 0 && opacity <= 1))
283                throw new IllegalArgumentException("Opacity value must be between 0 and 1");
284            double oldOpacity = getOpacity();
285            boolean oldVisible = isVisible();
286            this.opacity = opacity;
287            if (oldOpacity != getOpacity()) {
288                fireOpacityChanged(oldOpacity, getOpacity());
289            }
290            if (oldVisible != isVisible()) {
291                fireVisibleChanged(oldVisible, isVisible());
292            }
293        }
294    
295        /**
296         * Toggles the visibility state of this layer.
297         */
298        public void toggleVisible() {
299            setVisible(!isVisible());
300        }
301    
302        /**
303         * Adds a {@link PropertyChangeListener}
304         *
305         * @param listener the listener
306         */
307        public void addPropertyChangeListener(PropertyChangeListener listener) {
308            propertyChangeSupport.addPropertyChangeListener(listener);
309        }
310    
311        /**
312         * Removes a {@link PropertyChangeListener}
313         *
314         * @param listener the listener
315         */
316        public void removePropertyChangeListener(PropertyChangeListener listener) {
317            propertyChangeSupport.removePropertyChangeListener(listener);
318        }
319    
320        /**
321         * fires a property change for the property {@link #VISIBLE_PROP}
322         *
323         * @param oldValue the old value
324         * @param newValue the new value
325         */
326        protected void fireVisibleChanged(boolean oldValue, boolean newValue) {
327            propertyChangeSupport.firePropertyChange(VISIBLE_PROP, oldValue, newValue);
328        }
329    
330        /**
331         * fires a property change for the property {@link #OPACITY_PROP}
332         *
333         * @param oldValue the old value
334         * @param newValue the new value
335         */
336        protected void fireOpacityChanged(double oldValue, double newValue) {
337            propertyChangeSupport.firePropertyChange(OPACITY_PROP, oldValue, newValue);
338        }
339    
340        /**
341         * Check changed status of layer
342         *
343         * @return True if layer was changed since last paint
344         */
345        public boolean isChanged() {
346            return true;
347        }
348    
349        /**
350         * allows to check whether a projection is supported or not
351         *
352         * @return True if projection is supported for this layer
353         */
354        public boolean isProjectionSupported(Projection proj) {
355            return true;
356        }
357    
358        /**
359         * Specify user information about projections
360         *
361         * @return User readable text telling about supported projections
362         */
363        public String nameSupportedProjections() {
364            return tr("All projections are supported");
365        }
366    
367        /**
368         * The action to save a layer
369         *
370         */
371        public static class LayerSaveAction extends AbstractAction {
372            private Layer layer;
373            public LayerSaveAction(Layer layer) {
374                putValue(SMALL_ICON, ImageProvider.get("save"));
375                putValue(SHORT_DESCRIPTION, tr("Save the current data."));
376                putValue(NAME, tr("Save"));
377                setEnabled(true);
378                this.layer = layer;
379            }
380    
381            public void actionPerformed(ActionEvent e) {
382                SaveAction.getInstance().doSave(layer);
383            }
384        }
385    
386        public static class LayerSaveAsAction extends AbstractAction {
387            private Layer layer;
388            public LayerSaveAsAction(Layer layer) {
389                putValue(SMALL_ICON, ImageProvider.get("save_as"));
390                putValue(SHORT_DESCRIPTION, tr("Save the current data to a new file."));
391                putValue(NAME, tr("Save As..."));
392                setEnabled(true);
393                this.layer = layer;
394            }
395    
396            public void actionPerformed(ActionEvent e) {
397                SaveAsAction.getInstance().doSave(layer);
398            }
399        }
400    
401        public static class LayerGpxExportAction extends AbstractAction {
402            private Layer layer;
403            public LayerGpxExportAction(Layer layer) {
404                putValue(SMALL_ICON, ImageProvider.get("exportgpx"));
405                putValue(SHORT_DESCRIPTION, tr("Export the data to GPX file."));
406                putValue(NAME, tr("Export to GPX..."));
407                setEnabled(true);
408                this.layer = layer;
409            }
410    
411            public void actionPerformed(ActionEvent e) {
412                new GpxExportAction().export(layer);
413            }
414        }
415    
416        /* --------------------------------------------------------------------------------- */
417        /* interface ProjectionChangeListener                                                */
418        /* --------------------------------------------------------------------------------- */
419        @Override
420        public void projectionChanged(Projection oldValue, Projection newValue) {
421            if(!isProjectionSupported(newValue)) {
422                JOptionPane.showMessageDialog(Main.parent,
423                        tr("The layer {0} does not support the new projection {1}.\n{2}\n"
424                                + "Change the projection again or remove the layer.",
425                                getName(), newValue.toCode(), nameSupportedProjections()),
426                                tr("Warning"),
427                                JOptionPane.WARNING_MESSAGE);
428            }
429        }
430    
431        /**
432         * Initializes the layer after a successful load of data from a file
433         * @since 5459
434         */
435        public void onPostLoadFromFile() {
436            // To be overriden if needed
437        }
438        
439        /**
440         * Replies the savable state of this layer (i.e if it can be saved through a "File->Save" dialog).
441         * @return true if this layer can be saved to a file
442         * @since 5459
443         */
444        public boolean isSavable() {
445            return false;
446        }
447        
448        /**
449         * Checks whether it is ok to launch a save (whether we have data, there is no conflict etc.)
450         * @return <code>true</code>, if it is safe to save.
451         * @since 5459
452         */
453        public boolean checkSaveConditions() {
454            return true;
455        }
456        
457        /**
458         * Creates a new "Save" dialog for this layer and makes it visible.<br/>
459         * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
460         * @return The output {@code File}
461         * @since 5459
462         * @see SaveActionBase#createAndOpenSaveFileChooser
463         */
464        public File createAndOpenSaveFileChooser() {
465            return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Layer"), "lay");
466        }
467    }