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

Last change on this file since 12170 was 12170, checked in by michael2402, 7 years ago

"See #13175, see #14120: Remove isChanged() method call in map view - all layers should invalidate correctly now."

  • 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 final 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.