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