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