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

Last change on this file since 11553 was 11553, checked in by Don-vip, 7 years ago

refactor handling of null values - use Java 8 Optional where possible

  • Property svn:eol-style set to native
File size: 19.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 public static final String VISIBLE_PROP = Layer.class.getName() + ".visible";
113 public static final String OPACITY_PROP = Layer.class.getName() + ".opacity";
114 public static final String NAME_PROP = Layer.class.getName() + ".name";
115 public static final String FILTER_STATE_PROP = Layer.class.getName() + ".filterstate";
116
117 /**
118 * keeps track of property change listeners
119 */
120 protected PropertyChangeSupport propertyChangeSupport;
121
122 /**
123 * The visibility state of the layer.
124 */
125 private boolean visible = true;
126
127 /**
128 * The opacity of the layer.
129 */
130 private double opacity = 1;
131
132 /**
133 * The layer should be handled as a background layer in automatic handling
134 */
135 private boolean background;
136
137 /**
138 * The name of this layer.
139 */
140 private String name;
141
142 /**
143 * This is set if user renamed this layer.
144 */
145 private boolean renamed;
146
147 /**
148 * If a file is associated with this layer, this variable should be set to it.
149 */
150 private File associatedFile;
151
152 private final ValueChangeListener<Object> invalidateListener = change -> invalidate();
153
154 /**
155 * Create the layer and fill in the necessary components.
156 * @param name Layer name
157 */
158 public Layer(String name) {
159 this.propertyChangeSupport = new PropertyChangeSupport(this);
160 setName(name);
161 }
162
163 /**
164 * Initialization code, that depends on Main.map.mapView.
165 *
166 * It is always called in the event dispatching thread.
167 * Note that Main.map is null as long as no layer has been added, so do
168 * not execute code in the constructor, that assumes Main.map.mapView is
169 * not null.
170 *
171 * If you need to execute code when this layer is added to the map view, use
172 * {@link #attachToMapView(org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent)}
173 */
174 public void hookUpMapView() {
175 }
176
177 /**
178 * Return a representative small image for this layer. The image must not
179 * be larger than 64 pixel in any dimension.
180 * @return layer icon
181 */
182 public abstract Icon getIcon();
183
184 /**
185 * Gets the color property to use for this layer.
186 * @return The color property.
187 * @since 10824
188 */
189 public AbstractProperty<Color> getColorProperty() {
190 ColorProperty base = getBaseColorProperty();
191 if (base != null) {
192 // cannot cache this - name may change.
193 return base.getChildColor("layer " + getName());
194 } else {
195 return null;
196 }
197 }
198
199 /**
200 * Gets the color property that stores the default color for this layer.
201 * @return The property or <code>null</code> if this layer is not colored.
202 * @since 10824
203 */
204 protected ColorProperty getBaseColorProperty() {
205 return null;
206 }
207
208 private void addColorPropertyListener() {
209 AbstractProperty<Color> colorProperty = getColorProperty();
210 if (colorProperty != null) {
211 colorProperty.addListener(invalidateListener);
212 }
213 }
214
215 private void removeColorPropertyListener() {
216 AbstractProperty<Color> colorProperty = getColorProperty();
217 if (colorProperty != null) {
218 colorProperty.removeListener(invalidateListener);
219 }
220 }
221
222 /**
223 * @return A small tooltip hint about some statistics for this layer.
224 */
225 public abstract String getToolTipText();
226
227 /**
228 * Merges the given layer into this layer. Throws if the layer types are
229 * incompatible.
230 * @param from The layer that get merged into this one. After the merge,
231 * the other layer is not usable anymore and passing to one others
232 * mergeFrom should be one of the last things to do with a layer.
233 */
234 public abstract void mergeFrom(Layer from);
235
236 /**
237 * @param other The other layer that is tested to be mergable with this.
238 * @return Whether the other layer can be merged into this layer.
239 */
240 public abstract boolean isMergable(Layer other);
241
242 public abstract void visitBoundingBox(BoundingXYVisitor v);
243
244 public abstract Object getInfoComponent();
245
246 /**
247 * Determines if info dialog can be resized (false by default).
248 * @return {@code true} if the info dialog can be resized, {@code false} otherwise
249 * @since 6708
250 */
251 public boolean isInfoResizable() {
252 return false;
253 }
254
255 /**
256 * Returns list of actions. Action can implement LayerAction interface when it needs to be represented by other
257 * menu component than JMenuItem or when it supports multiple layers. Actions that support multiple layers should also
258 * have correct equals implementation.
259 *
260 * Use {@link SeparatorLayerAction#INSTANCE} instead of new JSeparator
261 * @return menu actions for this layer
262 */
263 public abstract Action[] getMenuEntries();
264
265 /**
266 * Called, when the layer is removed from the mapview and is going to be destroyed.
267 *
268 * This is because the Layer constructor can not add itself safely as listener
269 * to the layerlist dialog, because there may be no such dialog yet (loaded
270 * via command line parameter).
271 */
272 @Override
273 public void destroy() {
274 // Override in subclasses if needed
275 removeColorPropertyListener();
276 }
277
278 public File getAssociatedFile() {
279 return associatedFile;
280 }
281
282 public void setAssociatedFile(File file) {
283 associatedFile = file;
284 }
285
286 /**
287 * Replies the name of the layer
288 *
289 * @return the name of the layer
290 */
291 public String getName() {
292 return name;
293 }
294
295 /**
296 * Sets the name of the layer
297 *
298 * @param name the name. If null, the name is set to the empty string.
299 */
300 public final void setName(String name) {
301 if (this.name != null) {
302 removeColorPropertyListener();
303 }
304 String oldValue = this.name;
305 this.name = Optional.ofNullable(name).orElse("");
306 if (!this.name.equals(oldValue)) {
307 propertyChangeSupport.firePropertyChange(NAME_PROP, oldValue, this.name);
308 }
309
310 // re-add listener
311 addColorPropertyListener();
312 invalidate();
313 }
314
315 /**
316 * Rename layer and set renamed flag to mark it as renamed (has user given name).
317 *
318 * @param name the name. If null, the name is set to the empty string.
319 */
320 public final void rename(String name) {
321 renamed = true;
322 setName(name);
323 }
324
325 /**
326 * Replies true if this layer was renamed by user
327 *
328 * @return true if this layer was renamed by user
329 */
330 public boolean isRenamed() {
331 return renamed;
332 }
333
334 /**
335 * Replies true if this layer is a background layer
336 *
337 * @return true if this layer is a background layer
338 */
339 public boolean isBackgroundLayer() {
340 return background;
341 }
342
343 /**
344 * Sets whether this layer is a background layer
345 *
346 * @param background true, if this layer is a background layer
347 */
348 public void setBackgroundLayer(boolean background) {
349 this.background = background;
350 }
351
352 /**
353 * Sets the visibility of this layer. Emits property change event for
354 * property {@link #VISIBLE_PROP}.
355 *
356 * @param visible true, if the layer is visible; false, otherwise.
357 */
358 public void setVisible(boolean visible) {
359 boolean oldValue = isVisible();
360 this.visible = visible;
361 if (visible && opacity == 0) {
362 setOpacity(1);
363 } else if (oldValue != isVisible()) {
364 fireVisibleChanged(oldValue, isVisible());
365 }
366 }
367
368 /**
369 * Replies true if this layer is visible. False, otherwise.
370 * @return true if this layer is visible. False, otherwise.
371 */
372 public boolean isVisible() {
373 return visible && opacity != 0;
374 }
375
376 /**
377 * Gets the opacity of the layer, in range 0...1
378 * @return The opacity
379 */
380 public double getOpacity() {
381 return opacity;
382 }
383
384 /**
385 * Sets the opacity of the layer, in range 0...1
386 * @param opacity The opacity
387 * @throws IllegalArgumentException if the opacity is out of range
388 */
389 public void setOpacity(double opacity) {
390 if (!(opacity >= 0 && opacity <= 1))
391 throw new IllegalArgumentException("Opacity value must be between 0 and 1");
392 double oldOpacity = getOpacity();
393 boolean oldVisible = isVisible();
394 this.opacity = opacity;
395 if (!Utils.equalsEpsilon(oldOpacity, getOpacity())) {
396 fireOpacityChanged(oldOpacity, getOpacity());
397 }
398 if (oldVisible != isVisible()) {
399 fireVisibleChanged(oldVisible, isVisible());
400 }
401 }
402
403 /**
404 * Sets new state to the layer after applying {@link ImageProcessor}.
405 */
406 public void setFilterStateChanged() {
407 fireFilterStateChanged();
408 }
409
410 /**
411 * Toggles the visibility state of this layer.
412 */
413 public void toggleVisible() {
414 setVisible(!isVisible());
415 }
416
417 /**
418 * Adds a {@link PropertyChangeListener}
419 *
420 * @param listener the listener
421 */
422 public void addPropertyChangeListener(PropertyChangeListener listener) {
423 propertyChangeSupport.addPropertyChangeListener(listener);
424 }
425
426 /**
427 * Removes a {@link PropertyChangeListener}
428 *
429 * @param listener the listener
430 */
431 public void removePropertyChangeListener(PropertyChangeListener listener) {
432 propertyChangeSupport.removePropertyChangeListener(listener);
433 }
434
435 /**
436 * fires a property change for the property {@link #VISIBLE_PROP}
437 *
438 * @param oldValue the old value
439 * @param newValue the new value
440 */
441 protected void fireVisibleChanged(boolean oldValue, boolean newValue) {
442 propertyChangeSupport.firePropertyChange(VISIBLE_PROP, oldValue, newValue);
443 }
444
445 /**
446 * fires a property change for the property {@link #OPACITY_PROP}
447 *
448 * @param oldValue the old value
449 * @param newValue the new value
450 */
451 protected void fireOpacityChanged(double oldValue, double newValue) {
452 propertyChangeSupport.firePropertyChange(OPACITY_PROP, oldValue, newValue);
453 }
454
455 /**
456 * fires a property change for the property {@link #FILTER_STATE_PROP}.
457 */
458 protected void fireFilterStateChanged() {
459 propertyChangeSupport.firePropertyChange(FILTER_STATE_PROP, null, null);
460 }
461
462 /**
463 * Check changed status of layer
464 *
465 * @return True if layer was changed since last paint
466 * @deprecated This is not supported by multiple map views.
467 * Fire an {@link #invalidate()} to trigger a repaint.
468 * Let this method return false if you only use invalidation events.
469 */
470 @Deprecated
471 public boolean isChanged() {
472 return true;
473 }
474
475 /**
476 * allows to check whether a projection is supported or not
477 * @param proj projection
478 *
479 * @return True if projection is supported for this layer
480 */
481 public boolean isProjectionSupported(Projection proj) {
482 return proj != null;
483 }
484
485 /**
486 * Specify user information about projections
487 *
488 * @return User readable text telling about supported projections
489 */
490 public String nameSupportedProjections() {
491 return tr("All projections are supported");
492 }
493
494 /**
495 * The action to save a layer
496 */
497 public static class LayerSaveAction extends AbstractAction {
498 private final transient Layer layer;
499
500 public LayerSaveAction(Layer layer) {
501 putValue(SMALL_ICON, ImageProvider.get("save"));
502 putValue(SHORT_DESCRIPTION, tr("Save the current data."));
503 putValue(NAME, tr("Save"));
504 setEnabled(true);
505 this.layer = layer;
506 }
507
508 @Override
509 public void actionPerformed(ActionEvent e) {
510 SaveAction.getInstance().doSave(layer);
511 }
512 }
513
514 public static class LayerSaveAsAction extends AbstractAction {
515 private final transient Layer layer;
516
517 public LayerSaveAsAction(Layer layer) {
518 putValue(SMALL_ICON, ImageProvider.get("save_as"));
519 putValue(SHORT_DESCRIPTION, tr("Save the current data to a new file."));
520 putValue(NAME, tr("Save As..."));
521 setEnabled(true);
522 this.layer = layer;
523 }
524
525 @Override
526 public void actionPerformed(ActionEvent e) {
527 SaveAsAction.getInstance().doSave(layer);
528 }
529 }
530
531 public static class LayerGpxExportAction extends AbstractAction {
532 private final transient Layer layer;
533
534 public LayerGpxExportAction(Layer layer) {
535 putValue(SMALL_ICON, ImageProvider.get("exportgpx"));
536 putValue(SHORT_DESCRIPTION, tr("Export the data to GPX file."));
537 putValue(NAME, tr("Export to GPX..."));
538 setEnabled(true);
539 this.layer = layer;
540 }
541
542 @Override
543 public void actionPerformed(ActionEvent e) {
544 new GpxExportAction().export(layer);
545 }
546 }
547
548 /* --------------------------------------------------------------------------------- */
549 /* interface ProjectionChangeListener */
550 /* --------------------------------------------------------------------------------- */
551 @Override
552 public void projectionChanged(Projection oldValue, Projection newValue) {
553 if (!isProjectionSupported(newValue)) {
554 final String message = "<html><body><p>" +
555 tr("The layer {0} does not support the new projection {1}.", getName(), newValue.toCode()) + "</p>" +
556 "<p style='width: 450px;'>" + tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" +
557 tr("Change the projection again or remove the layer.");
558
559 // run later to not block loading the UI.
560 SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(Main.parent,
561 message,
562 tr("Warning"),
563 JOptionPane.WARNING_MESSAGE));
564 }
565 }
566
567 /**
568 * Initializes the layer after a successful load of data from a file
569 * @since 5459
570 */
571 public void onPostLoadFromFile() {
572 // To be overriden if needed
573 }
574
575 /**
576 * Replies the savable state of this layer (i.e if it can be saved through a "File-&gt;Save" dialog).
577 * @return true if this layer can be saved to a file
578 * @since 5459
579 */
580 public boolean isSavable() {
581 return false;
582 }
583
584 /**
585 * Checks whether it is ok to launch a save (whether we have data, there is no conflict etc.)
586 * @return <code>true</code>, if it is safe to save.
587 * @since 5459
588 */
589 public boolean checkSaveConditions() {
590 return true;
591 }
592
593 /**
594 * Creates a new "Save" dialog for this layer and makes it visible.<br>
595 * When the user has chosen a file, checks the file extension, and confirms overwrite if needed.
596 * @return The output {@code File}
597 * @see SaveActionBase#createAndOpenSaveFileChooser
598 * @since 5459
599 */
600 public File createAndOpenSaveFileChooser() {
601 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Layer"), "lay");
602 }
603
604 /**
605 * Gets the strategy that specifies where this layer should be inserted in a layer list.
606 * @return That strategy.
607 * @since 10008
608 */
609 public LayerPositionStrategy getDefaultLayerPosition() {
610 if (isBackgroundLayer()) {
611 return LayerPositionStrategy.BEFORE_FIRST_BACKGROUND_LAYER;
612 } else {
613 return LayerPositionStrategy.AFTER_LAST_VALIDATION_LAYER;
614 }
615 }
616
617 /**
618 * Gets the {@link ProjectionBounds} for this layer to be visible to the user. This can be the exact bounds, the UI handles padding. Return
619 * <code>null</code> if you cannot provide this information. The default implementation uses the bounds from
620 * {@link #visitBoundingBox(BoundingXYVisitor)}.
621 * @return The bounds for this layer.
622 * @since 10371
623 */
624 public ProjectionBounds getViewProjectionBounds() {
625 BoundingXYVisitor v = new BoundingXYVisitor();
626 visitBoundingBox(v);
627 return v.getBounds();
628 }
629
630 @Override
631 public String toString() {
632 return getClass().getSimpleName() + " [name=" + name + ", associatedFile=" + associatedFile + ']';
633 }
634}
Note: See TracBrowser for help on using the repository browser.