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