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; 013import java.util.Optional; 014 015import javax.swing.AbstractAction; 016import javax.swing.Action; 017import javax.swing.Icon; 018import javax.swing.JOptionPane; 019import javax.swing.JSeparator; 020import javax.swing.SwingUtilities; 021 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.preferences.AbstractProperty; 029import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener; 030import org.openstreetmap.josm.data.preferences.NamedColorProperty; 031import org.openstreetmap.josm.data.projection.Projection; 032import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 033import org.openstreetmap.josm.gui.MainApplication; 034import org.openstreetmap.josm.tools.Destroyable; 035import org.openstreetmap.josm.tools.ImageProcessor; 036import org.openstreetmap.josm.tools.ImageProvider; 037import org.openstreetmap.josm.tools.Utils; 038 039/** 040 * A layer encapsulates the gui component of one dataset and its representation. 041 * 042 * Some layers may display data directly imported from OSM server. Other only 043 * display background images. Some can be edited, some not. Some are static and 044 * other changes dynamically (auto-updated). 045 * 046 * Layers can be visible or not. Most actions the user can do applies only on 047 * selected layers. The available actions depend on the selected layers too. 048 * 049 * All layers are managed by the MapView. They are displayed in a list to the 050 * right of the screen. 051 * 052 * @author imi 053 */ 054public abstract class Layer extends AbstractMapViewPaintable implements Destroyable, ProjectionChangeListener { 055 056 /** 057 * Action related to a single layer. 058 */ 059 public interface LayerAction { 060 061 /** 062 * Determines if this action supports a given list of layers. 063 * @param layers list of layers 064 * @return {@code true} if this action supports the given list of layers, {@code false} otherwise 065 */ 066 boolean supportLayers(List<Layer> layers); 067 068 /** 069 * Creates and return the menu component. 070 * @return the menu component 071 */ 072 Component createMenuComponent(); 073 } 074 075 /** 076 * Action related to several layers. 077 * @since 10600 (functional interface) 078 */ 079 @FunctionalInterface 080 public interface MultiLayerAction { 081 082 /** 083 * Returns the action for a given list of layers. 084 * @param layers list of layers 085 * @return the action for the given list of layers 086 */ 087 Action getMultiLayerAction(List<Layer> layers); 088 } 089 090 /** 091 * Special class that can be returned by getMenuEntries when JSeparator needs to be created 092 */ 093 public static class SeparatorLayerAction extends AbstractAction implements LayerAction { 094 /** Unique instance */ 095 public static final SeparatorLayerAction INSTANCE = new SeparatorLayerAction(); 096 097 @Override 098 public void actionPerformed(ActionEvent e) { 099 throw new UnsupportedOperationException(); 100 } 101 102 @Override 103 public Component createMenuComponent() { 104 return new JSeparator(); 105 } 106 107 @Override 108 public boolean supportLayers(List<Layer> layers) { 109 return false; 110 } 111 } 112 113 /** 114 * The visibility property for this layer. May be <code>true</code> (visible) or <code>false</code> (hidden). 115 */ 116 public static final String VISIBLE_PROP = Layer.class.getName() + ".visible"; 117 /** 118 * The opacity of this layer. A number between 0 and 1 119 */ 120 public static final String OPACITY_PROP = Layer.class.getName() + ".opacity"; 121 /** 122 * The name property of the layer. 123 * You can listen to name changes by listening to changes to this property. 124 */ 125 public static final String NAME_PROP = Layer.class.getName() + ".name"; 126 /** 127 * Property that defines the filter state. 128 * This is currently not used. 129 */ 130 public static final String FILTER_STATE_PROP = Layer.class.getName() + ".filterstate"; 131 132 /** 133 * keeps track of property change listeners 134 */ 135 protected PropertyChangeSupport propertyChangeSupport; 136 137 /** 138 * The visibility state of the layer. 139 */ 140 private boolean visible = true; 141 142 /** 143 * The opacity of the layer. 144 */ 145 private double opacity = 1; 146 147 /** 148 * The layer should be handled as a background layer in automatic handling 149 */ 150 private boolean background; 151 152 /** 153 * The name of this layer. 154 */ 155 private String name; 156 157 /** 158 * This is set if user renamed this layer. 159 */ 160 private boolean renamed; 161 162 /** 163 * If a file is associated with this layer, this variable should be set to it. 164 */ 165 private File associatedFile; 166 167 private final ValueChangeListener<Object> invalidateListener = change -> invalidate(); 168 private boolean isDestroyed; 169 170 /** 171 * Create the layer and fill in the necessary components. 172 * @param name Layer name 173 */ 174 public Layer(String name) { 175 this.propertyChangeSupport = new PropertyChangeSupport(this); 176 setName(name); 177 } 178 179 /** 180 * Initialization code, that depends on Main.map.mapView. 181 * 182 * It is always called in the event dispatching thread. 183 * Note that Main.map is null as long as no layer has been added, so do 184 * not execute code in the constructor, that assumes Main.map.mapView is 185 * not null. 186 * 187 * If you need to execute code when this layer is added to the map view, use 188 * {@link #attachToMapView(org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent)} 189 */ 190 public void hookUpMapView() { 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 * Gets the color property to use for this layer. 202 * @return The color property. 203 * @since 10824 204 */ 205 public AbstractProperty<Color> getColorProperty() { 206 NamedColorProperty base = getBaseColorProperty(); 207 if (base != null) { 208 return base.getChildColor(NamedColorProperty.COLOR_CATEGORY_LAYER, getName(), base.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 NamedColorProperty 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 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 * allows to check whether a projection is supported or not 505 * @param proj projection 506 * 507 * @return True if projection is supported for this layer 508 */ 509 public boolean isProjectionSupported(Projection proj) { 510 return proj != null; 511 } 512 513 /** 514 * Specify user information about projections 515 * 516 * @return User readable text telling about supported projections 517 */ 518 public String nameSupportedProjections() { 519 return tr("All projections are supported"); 520 } 521 522 /** 523 * The action to save a layer 524 */ 525 public static class LayerSaveAction extends AbstractAction { 526 private final transient Layer layer; 527 528 /** 529 * Create a new action that saves the layer 530 * @param layer The layer to save. 531 */ 532 public LayerSaveAction(Layer layer) { 533 new ImageProvider("save").getResource().attachImageIcon(this, true); 534 putValue(SHORT_DESCRIPTION, tr("Save the current data.")); 535 putValue(NAME, tr("Save")); 536 setEnabled(true); 537 this.layer = layer; 538 } 539 540 @Override 541 public void actionPerformed(ActionEvent e) { 542 SaveAction.getInstance().doSave(layer); 543 } 544 } 545 546 /** 547 * Action to save the layer in a new file 548 */ 549 public static class LayerSaveAsAction extends AbstractAction { 550 private final transient Layer layer; 551 552 /** 553 * Create a new save as action 554 * @param layer The layer that should be saved. 555 */ 556 public LayerSaveAsAction(Layer layer) { 557 new ImageProvider("save_as").getResource().attachImageIcon(this, true); 558 putValue(SHORT_DESCRIPTION, tr("Save the current data to a new file.")); 559 putValue(NAME, tr("Save As...")); 560 setEnabled(true); 561 this.layer = layer; 562 } 563 564 @Override 565 public void actionPerformed(ActionEvent e) { 566 SaveAsAction.getInstance().doSave(layer); 567 } 568 } 569 570 /** 571 * Action that exports the layer as gpx file 572 */ 573 public static class LayerGpxExportAction extends AbstractAction { 574 private final transient Layer layer; 575 576 /** 577 * Create a new gpx export action for the given layer. 578 * @param layer The layer 579 */ 580 public LayerGpxExportAction(Layer layer) { 581 new ImageProvider("exportgpx").getResource().attachImageIcon(this, true); 582 putValue(SHORT_DESCRIPTION, tr("Export the data to GPX file.")); 583 putValue(NAME, tr("Export to GPX...")); 584 setEnabled(true); 585 this.layer = layer; 586 } 587 588 @Override 589 public void actionPerformed(ActionEvent e) { 590 new GpxExportAction().export(layer); 591 } 592 } 593 594 /* --------------------------------------------------------------------------------- */ 595 /* interface ProjectionChangeListener */ 596 /* --------------------------------------------------------------------------------- */ 597 @Override 598 public void projectionChanged(Projection oldValue, Projection newValue) { 599 if (!isProjectionSupported(newValue)) { 600 final String message = "<html><body><p>" + 601 tr("The layer {0} does not support the new projection {1}.", 602 Utils.escapeReservedCharactersHTML(getName()), newValue.toCode()) + "</p>" + 603 "<p style='width: 450px;'>" + tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" + 604 tr("Change the projection again or remove the layer."); 605 606 // run later to not block loading the UI. 607 SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(MainApplication.getMainFrame(), 608 message, 609 tr("Warning"), 610 JOptionPane.WARNING_MESSAGE)); 611 } 612 } 613 614 /** 615 * Initializes the layer after a successful load of data from a file 616 * @since 5459 617 */ 618 public void onPostLoadFromFile() { 619 // To be overriden if needed 620 } 621 622 /** 623 * Replies the savable state of this layer (i.e if it can be saved through a "File->Save" dialog). 624 * @return true if this layer can be saved to a file 625 * @since 5459 626 */ 627 public boolean isSavable() { 628 return false; 629 } 630 631 /** 632 * Checks whether it is ok to launch a save (whether we have data, there is no conflict etc.) 633 * @return <code>true</code>, if it is safe to save. 634 * @since 5459 635 */ 636 public boolean checkSaveConditions() { 637 return true; 638 } 639 640 /** 641 * Creates a new "Save" dialog for this layer and makes it visible.<br> 642 * When the user has chosen a file, checks the file extension, and confirms overwrite if needed. 643 * @return The output {@code File} 644 * @see SaveActionBase#createAndOpenSaveFileChooser 645 * @since 5459 646 */ 647 public File createAndOpenSaveFileChooser() { 648 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Layer"), "lay"); 649 } 650 651 /** 652 * Gets the strategy that specifies where this layer should be inserted in a layer list. 653 * @return That strategy. 654 * @since 10008 655 */ 656 public LayerPositionStrategy getDefaultLayerPosition() { 657 if (isBackgroundLayer()) { 658 return LayerPositionStrategy.BEFORE_FIRST_BACKGROUND_LAYER; 659 } else { 660 return LayerPositionStrategy.AFTER_LAST_VALIDATION_LAYER; 661 } 662 } 663 664 /** 665 * Gets the {@link ProjectionBounds} for this layer to be visible to the user. This can be the exact bounds, the UI handles padding. Return 666 * <code>null</code> if you cannot provide this information. The default implementation uses the bounds from 667 * {@link #visitBoundingBox(BoundingXYVisitor)}. 668 * @return The bounds for this layer. 669 * @since 10371 670 */ 671 public ProjectionBounds getViewProjectionBounds() { 672 BoundingXYVisitor v = new BoundingXYVisitor(); 673 visitBoundingBox(v); 674 return v.getBounds(); 675 } 676 677 @Override 678 public String toString() { 679 return getClass().getSimpleName() + " [name=" + name + ", associatedFile=" + associatedFile + ']'; 680 } 681}