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.projection.Projection; 029import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 030import org.openstreetmap.josm.gui.MainApplication; 031import org.openstreetmap.josm.tools.Destroyable; 032import org.openstreetmap.josm.tools.ImageProcessor; 033import org.openstreetmap.josm.tools.ImageProvider; 034import org.openstreetmap.josm.tools.Utils; 035 036/** 037 * A layer encapsulates the gui component of one dataset and its representation. 038 * 039 * Some layers may display data directly imported from OSM server. Other only 040 * display background images. Some can be edited, some not. Some are static and 041 * other changes dynamically (auto-updated). 042 * 043 * Layers can be visible or not. Most actions the user can do applies only on 044 * selected layers. The available actions depend on the selected layers too. 045 * 046 * All layers are managed by the MapView. They are displayed in a list to the 047 * right of the screen. 048 * 049 * @author imi 050 */ 051public abstract class Layer extends AbstractMapViewPaintable implements Destroyable, ProjectionChangeListener { 052 053 /** 054 * Action related to a single layer. 055 */ 056 public interface LayerAction { 057 058 /** 059 * Determines if this action supports a given list of layers. 060 * @param layers list of layers 061 * @return {@code true} if this action supports the given list of layers, {@code false} otherwise 062 */ 063 boolean supportLayers(List<Layer> layers); 064 065 /** 066 * Creates and return the menu component. 067 * @return the menu component 068 */ 069 Component createMenuComponent(); 070 } 071 072 /** 073 * Action related to several layers. 074 * @since 10600 (functional interface) 075 */ 076 @FunctionalInterface 077 public interface MultiLayerAction { 078 079 /** 080 * Returns the action for a given list of layers. 081 * @param layers list of layers 082 * @return the action for the given list of layers 083 */ 084 Action getMultiLayerAction(List<Layer> layers); 085 } 086 087 /** 088 * Special class that can be returned by getMenuEntries when JSeparator needs to be created 089 */ 090 public static class SeparatorLayerAction extends AbstractAction implements LayerAction { 091 /** Unique instance */ 092 public static final SeparatorLayerAction INSTANCE = new SeparatorLayerAction(); 093 094 @Override 095 public void actionPerformed(ActionEvent e) { 096 throw new UnsupportedOperationException(); 097 } 098 099 @Override 100 public Component createMenuComponent() { 101 return new JSeparator(); 102 } 103 104 @Override 105 public boolean supportLayers(List<Layer> layers) { 106 return false; 107 } 108 } 109 110 /** 111 * The visibility property for this layer. May be <code>true</code> (visible) or <code>false</code> (hidden). 112 */ 113 public static final String VISIBLE_PROP = Layer.class.getName() + ".visible"; 114 /** 115 * The opacity of this layer. A number between 0 and 1 116 */ 117 public static final String OPACITY_PROP = Layer.class.getName() + ".opacity"; 118 /** 119 * The name property of the layer. 120 * You can listen to name changes by listening to changes to this property. 121 */ 122 public static final String NAME_PROP = Layer.class.getName() + ".name"; 123 /** 124 * Property that defines the filter state. 125 * This is currently not used. 126 */ 127 public static final String FILTER_STATE_PROP = Layer.class.getName() + ".filterstate"; 128 129 /** 130 * keeps track of property change listeners 131 */ 132 protected PropertyChangeSupport propertyChangeSupport; 133 134 /** 135 * The visibility state of the layer. 136 */ 137 private boolean visible = true; 138 139 /** 140 * The opacity of the layer. 141 */ 142 private double opacity = 1; 143 144 /** 145 * The layer should be handled as a background layer in automatic handling 146 */ 147 private boolean background; 148 149 /** 150 * The name of this layer. 151 */ 152 private String name; 153 154 /** 155 * This is set if user renamed this layer. 156 */ 157 private boolean renamed; 158 159 /** 160 * If a file is associated with this layer, this variable should be set to it. 161 */ 162 private File associatedFile; 163 164 private boolean isDestroyed; 165 166 /** 167 * Create the layer and fill in the necessary components. 168 * @param name Layer name 169 */ 170 protected Layer(String name) { 171 this.propertyChangeSupport = new PropertyChangeSupport(this); 172 setName(name); 173 } 174 175 /** 176 * Initialization code, that depends on Main.map.mapView. 177 * 178 * It is always called in the event dispatching thread. 179 * Note that Main.map is null as long as no layer has been added, so do 180 * not execute code in the constructor, that assumes Main.map.mapView is 181 * not null. 182 * 183 * If you need to execute code when this layer is added to the map view, use 184 * {@link #attachToMapView(org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent)} 185 */ 186 public void hookUpMapView() { 187 } 188 189 /** 190 * Return a representative small image for this layer. The image must not 191 * be larger than 64 pixel in any dimension. 192 * @return layer icon 193 */ 194 public abstract Icon getIcon(); 195 196 /** 197 * Determines whether the layer has / can handle colors. 198 * @return whether the layer has / can handle colors. 199 * @since 15496 200 */ 201 public boolean hasColor() { 202 return false; 203 } 204 205 /** 206 * Return the current color of the layer 207 * @return null when not present or not supported 208 * @since 15496 209 */ 210 public Color getColor() { 211 return null; 212 } 213 214 /** 215 * Sets the color for this layer. Nothing happens if not supported by the layer 216 * @param color the color to be set, <code>null</code> for default 217 * @since 15496 218 */ 219 public void setColor(Color color) { 220 } 221 222 /** 223 * Returns a small tooltip hint about some statistics for this layer. 224 * @return A small tooltip hint about some statistics for this layer. 225 */ 226 public abstract String getToolTipText(); 227 228 /** 229 * Merges the given layer into this layer. Throws if the layer types are 230 * incompatible. 231 * @param from The layer that get merged into this one. After the merge, 232 * the other layer is not usable anymore and passing to one others 233 * mergeFrom should be one of the last things to do with a layer. 234 */ 235 public abstract void mergeFrom(Layer from); 236 237 /** 238 * Determines if the other layer can be merged into this layer. 239 * @param other The other layer that is tested to be mergable with this. 240 * @return Whether the other layer can be merged into this layer. 241 */ 242 public abstract boolean isMergable(Layer other); 243 244 /** 245 * Visits the content bounds of this layer. The behavior of this method depends on the layer, 246 * but each implementation should attempt to cover the relevant content of the layer in this method. 247 * @param v The visitor that gets notified about the contents of this layer. 248 */ 249 public abstract void visitBoundingBox(BoundingXYVisitor v); 250 251 /** 252 * Gets the layer information to display to the user. 253 * This is used if the user requests information about this layer. 254 * It should display a description of the layer content. 255 * @return Either a String or a {@link Component} describing the layer. 256 */ 257 public abstract Object getInfoComponent(); 258 259 /** 260 * Determines if info dialog can be resized (false by default). 261 * @return {@code true} if the info dialog can be resized, {@code false} otherwise 262 * @since 6708 263 */ 264 public boolean isInfoResizable() { 265 return false; 266 } 267 268 /** 269 * Returns list of actions. Action can implement LayerAction interface when it needs to be represented by other 270 * menu component than JMenuItem or when it supports multiple layers. Actions that support multiple layers should also 271 * have correct equals implementation. 272 * 273 * Use {@link SeparatorLayerAction#INSTANCE} instead of new JSeparator 274 * @return menu actions for this layer 275 */ 276 public abstract Action[] getMenuEntries(); 277 278 /** 279 * Called, when the layer is removed from the mapview and is going to be destroyed. 280 * 281 * This is because the Layer constructor can not add itself safely as listener 282 * to the layerlist dialog, because there may be no such dialog yet (loaded 283 * via command line parameter). 284 */ 285 @Override 286 public synchronized void destroy() { 287 if (isDestroyed) { 288 throw new IllegalStateException("The layer has already been destroyed: " + this); 289 } 290 isDestroyed = true; 291 // Override in subclasses if needed 292 } 293 294 /** 295 * Gets the associated file for this layer. 296 * @return The file or <code>null</code> if it is unset. 297 * @see #setAssociatedFile(File) 298 */ 299 public File getAssociatedFile() { 300 return associatedFile; 301 } 302 303 /** 304 * Sets the associated file for this layer. 305 * 306 * The associated file might be the one that the user opened. 307 * @param file The file, may be <code>null</code> 308 */ 309 public void setAssociatedFile(File file) { 310 associatedFile = file; 311 } 312 313 /** 314 * Replies a label for this layer useful for UI elements. Defaults to the layer name 315 * @return a label for this layer 316 * @since 17626 317 */ 318 public String getLabel() { 319 return getName(); 320 } 321 322 /** 323 * Replies the name of the layer 324 * 325 * @return the name of the layer 326 */ 327 public String getName() { 328 return name; 329 } 330 331 /** 332 * Sets the name of the layer 333 * 334 * @param name the name. If null, the name is set to the empty string. 335 */ 336 public void setName(String name) { 337 String oldValue = this.name; 338 this.name = Optional.ofNullable(name).orElse(""); 339 if (!this.name.equals(oldValue)) { 340 propertyChangeSupport.firePropertyChange(NAME_PROP, oldValue, this.name); 341 } 342 invalidate(); 343 } 344 345 /** 346 * Rename layer and set renamed flag to mark it as renamed (has user given name). 347 * 348 * @param name the name. If null, the name is set to the empty string. 349 */ 350 public final void rename(String name) { 351 renamed = true; 352 setName(name); 353 } 354 355 /** 356 * Replies true if this layer was renamed by user 357 * 358 * @return true if this layer was renamed by user 359 */ 360 public boolean isRenamed() { 361 return renamed; 362 } 363 364 /** 365 * Replies true if this layer is a background layer 366 * 367 * @return true if this layer is a background layer 368 */ 369 public boolean isBackgroundLayer() { 370 return background; 371 } 372 373 /** 374 * Sets whether this layer is a background layer 375 * 376 * @param background true, if this layer is a background layer 377 */ 378 public void setBackgroundLayer(boolean background) { 379 this.background = background; 380 } 381 382 /** 383 * Sets the visibility of this layer. Emits property change event for 384 * property {@link #VISIBLE_PROP}. 385 * 386 * @param visible true, if the layer is visible; false, otherwise. 387 */ 388 public void setVisible(boolean visible) { 389 boolean oldValue = isVisible(); 390 this.visible = visible; 391 if (visible && opacity == 0) { 392 setOpacity(1); 393 } else if (oldValue != isVisible()) { 394 fireVisibleChanged(oldValue, isVisible()); 395 } 396 } 397 398 /** 399 * Replies true if this layer is visible. False, otherwise. 400 * @return true if this layer is visible. False, otherwise. 401 */ 402 public boolean isVisible() { 403 return visible && opacity != 0; 404 } 405 406 /** 407 * Gets the opacity of the layer, in range 0...1 408 * @return The opacity 409 */ 410 public double getOpacity() { 411 return opacity; 412 } 413 414 /** 415 * Sets the opacity of the layer, in range 0...1 416 * @param opacity The opacity 417 * @throws IllegalArgumentException if the opacity is out of range 418 */ 419 public void setOpacity(double opacity) { 420 if (!(opacity >= 0 && opacity <= 1)) 421 throw new IllegalArgumentException("Opacity value must be between 0 and 1"); 422 double oldOpacity = getOpacity(); 423 boolean oldVisible = isVisible(); 424 this.opacity = opacity; 425 if (!Utils.equalsEpsilon(oldOpacity, getOpacity())) { 426 fireOpacityChanged(oldOpacity, getOpacity()); 427 } 428 if (oldVisible != isVisible()) { 429 fireVisibleChanged(oldVisible, isVisible()); 430 } 431 } 432 433 /** 434 * Sets new state to the layer after applying {@link ImageProcessor}. 435 */ 436 public void setFilterStateChanged() { 437 fireFilterStateChanged(); 438 } 439 440 /** 441 * Toggles the visibility state of this layer. 442 */ 443 public void toggleVisible() { 444 setVisible(!isVisible()); 445 } 446 447 /** 448 * Adds a {@link PropertyChangeListener} 449 * 450 * @param listener the listener 451 */ 452 public void addPropertyChangeListener(PropertyChangeListener listener) { 453 propertyChangeSupport.addPropertyChangeListener(listener); 454 } 455 456 /** 457 * Removes a {@link PropertyChangeListener} 458 * 459 * @param listener the listener 460 */ 461 public void removePropertyChangeListener(PropertyChangeListener listener) { 462 propertyChangeSupport.removePropertyChangeListener(listener); 463 } 464 465 /** 466 * fires a property change for the property {@link #VISIBLE_PROP} 467 * 468 * @param oldValue the old value 469 * @param newValue the new value 470 */ 471 protected void fireVisibleChanged(boolean oldValue, boolean newValue) { 472 propertyChangeSupport.firePropertyChange(VISIBLE_PROP, oldValue, newValue); 473 } 474 475 /** 476 * fires a property change for the property {@link #OPACITY_PROP} 477 * 478 * @param oldValue the old value 479 * @param newValue the new value 480 */ 481 protected void fireOpacityChanged(double oldValue, double newValue) { 482 propertyChangeSupport.firePropertyChange(OPACITY_PROP, oldValue, newValue); 483 } 484 485 /** 486 * fires a property change for the property {@link #FILTER_STATE_PROP}. 487 */ 488 protected void fireFilterStateChanged() { 489 propertyChangeSupport.firePropertyChange(FILTER_STATE_PROP, null, null); 490 } 491 492 /** 493 * allows to check whether a projection is supported or not 494 * @param proj projection 495 * 496 * @return True if projection is supported for this layer 497 */ 498 public boolean isProjectionSupported(Projection proj) { 499 return proj != null; 500 } 501 502 /** 503 * Specify user information about projections 504 * 505 * @return User readable text telling about supported projections 506 */ 507 public String nameSupportedProjections() { 508 return tr("All projections are supported"); 509 } 510 511 /** 512 * The action to save a layer 513 */ 514 public static class LayerSaveAction extends AbstractAction { 515 private final transient Layer layer; 516 517 /** 518 * Create a new action that saves the layer 519 * @param layer The layer to save. 520 */ 521 public LayerSaveAction(Layer layer) { 522 new ImageProvider("save").getResource().attachImageIcon(this, true); 523 putValue(SHORT_DESCRIPTION, tr("Save the current data.")); 524 putValue(NAME, tr("Save")); 525 setEnabled(true); 526 this.layer = layer; 527 } 528 529 @Override 530 public void actionPerformed(ActionEvent e) { 531 SaveAction.getInstance().doSave(layer, true); 532 } 533 } 534 535 /** 536 * Action to save the layer in a new file 537 */ 538 public static class LayerSaveAsAction extends AbstractAction { 539 private final transient Layer layer; 540 541 /** 542 * Create a new save as action 543 * @param layer The layer that should be saved. 544 */ 545 public LayerSaveAsAction(Layer layer) { 546 new ImageProvider("save_as").getResource().attachImageIcon(this, true); 547 putValue(SHORT_DESCRIPTION, tr("Save the current data to a new file.")); 548 putValue(NAME, tr("Save As...")); 549 setEnabled(true); 550 this.layer = layer; 551 } 552 553 @Override 554 public void actionPerformed(ActionEvent e) { 555 SaveAsAction.getInstance().doSave(layer); 556 } 557 } 558 559 /** 560 * Action that exports the layer as gpx file 561 */ 562 public static class LayerGpxExportAction extends AbstractAction { 563 private final transient Layer layer; 564 565 /** 566 * Create a new gpx export action for the given layer. 567 * @param layer The layer 568 */ 569 public LayerGpxExportAction(Layer layer) { 570 new ImageProvider("exportgpx").getResource().attachImageIcon(this, true); 571 putValue(SHORT_DESCRIPTION, tr("Export the data to GPX file.")); 572 putValue(NAME, tr("Export to GPX...")); 573 setEnabled(true); 574 this.layer = layer; 575 } 576 577 @Override 578 public void actionPerformed(ActionEvent e) { 579 new GpxExportAction().export(layer); 580 } 581 } 582 583 /* --------------------------------------------------------------------------------- */ 584 /* interface ProjectionChangeListener */ 585 /* --------------------------------------------------------------------------------- */ 586 @Override 587 public void projectionChanged(Projection oldValue, Projection newValue) { 588 if (!isProjectionSupported(newValue)) { 589 final String message = "<html><body><p>" + 590 tr("The layer {0} does not support the new projection {1}.", 591 Utils.escapeReservedCharactersHTML(getName()), newValue.toCode()) + "</p>" + 592 "<p style='width: 450px;'>" + tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" + 593 tr("Change the projection again or remove the layer."); 594 595 // run later to not block loading the UI. 596 SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(MainApplication.getMainFrame(), 597 message, 598 tr("Warning"), 599 JOptionPane.WARNING_MESSAGE)); 600 } 601 } 602 603 /** 604 * Initializes the layer after a successful load of data from a file 605 * @since 5459 606 */ 607 public void onPostLoadFromFile() { 608 // To be overridden if needed 609 } 610 611 /** 612 * Replies the savable state of this layer (i.e if it can be saved through a "File->Save" dialog). 613 * @return true if this layer can be saved to a file 614 * @since 5459 615 */ 616 public boolean isSavable() { 617 return false; 618 } 619 620 /** 621 * Checks whether it is ok to launch a save (whether we have data, there is no conflict etc.) 622 * @return <code>true</code>, if it is safe to save. 623 * @since 5459 624 */ 625 public boolean checkSaveConditions() { 626 return true; 627 } 628 629 /** 630 * Creates a new "Save" dialog for this layer and makes it visible.<br> 631 * When the user has chosen a file, checks the file extension, and confirms overwrite if needed. 632 * @return The output {@code File} 633 * @see SaveActionBase#createAndOpenSaveFileChooser 634 * @since 5459 635 */ 636 public File createAndOpenSaveFileChooser() { 637 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save Layer"), "lay"); 638 } 639 640 /** 641 * Gets the strategy that specifies where this layer should be inserted in a layer list. 642 * @return That strategy. 643 * @since 10008 644 */ 645 public LayerPositionStrategy getDefaultLayerPosition() { 646 if (isBackgroundLayer()) { 647 return LayerPositionStrategy.BEFORE_FIRST_BACKGROUND_LAYER; 648 } else { 649 return LayerPositionStrategy.AFTER_LAST_VALIDATION_LAYER; 650 } 651 } 652 653 /** 654 * Gets the {@link ProjectionBounds} for this layer to be visible to the user. This can be the exact bounds, the UI handles padding. Return 655 * <code>null</code> if you cannot provide this information. The default implementation uses the bounds from 656 * {@link #visitBoundingBox(BoundingXYVisitor)}. 657 * @return The bounds for this layer. 658 * @since 10371 659 */ 660 public ProjectionBounds getViewProjectionBounds() { 661 BoundingXYVisitor v = new BoundingXYVisitor(); 662 visitBoundingBox(v); 663 return v.getBounds(); 664 } 665 666 /** 667 * Get the source for the layer 668 * @return The string for the changeset source tag or {@code null} 669 * @since 15371 670 */ 671 public String getChangesetSourceTag() { 672 return null; 673 } 674 675 @Override 676 public String toString() { 677 return getClass().getSimpleName() + " [name=" + name + ", associatedFile=" + associatedFile + ']'; 678 } 679}