001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer; 003 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.Collections; 007import java.util.IdentityHashMap; 008import java.util.Iterator; 009import java.util.List; 010import java.util.Set; 011import java.util.concurrent.CopyOnWriteArrayList; 012import java.util.function.Consumer; 013 014import org.openstreetmap.josm.gui.MainApplication; 015import org.openstreetmap.josm.gui.util.GuiHelper; 016import org.openstreetmap.josm.tools.JosmRuntimeException; 017import org.openstreetmap.josm.tools.Utils; 018import org.openstreetmap.josm.tools.bugreport.BugReport; 019 020/** 021 * This class handles the layer management. 022 * <p> 023 * This manager handles a list of layers with the first layer being the front layer. 024 * <h1>Threading</h1> 025 * Synchronization of the layer manager is done by synchronizing all read/write access. All changes are internally done in the EDT thread. 026 * 027 * Methods of this manager may be called from any thread in any order. 028 * Listeners are called while this layer manager is locked, so they should not block on other threads. 029 * 030 * @author Michael Zangl 031 * @since 10273 032 */ 033public class LayerManager { 034 /** 035 * Interface to notify listeners of a layer change. 036 */ 037 public interface LayerChangeListener { 038 /** 039 * Notifies this listener that a layer has been added. 040 * <p> 041 * Listeners are called in the EDT thread. You should not do blocking or long-running tasks in this method. 042 * @param e The new added layer event 043 */ 044 void layerAdded(LayerAddEvent e); 045 046 /** 047 * Notifies this listener that a layer was just removed. 048 * <p> 049 * Listeners are called in the EDT thread after the layer was removed. 050 * Use {@link LayerRemoveEvent#scheduleRemoval(Collection)} to remove more layers. 051 * You should not do blocking or long-running tasks in this method. 052 * @param e The layer to be removed (as event) 053 */ 054 void layerRemoving(LayerRemoveEvent e); 055 056 /** 057 * Notifies this listener that the order of layers was changed. 058 * <p> 059 * Listeners are called in the EDT thread. 060 * You should not do blocking or long-running tasks in this method. 061 * @param e The order change event. 062 */ 063 void layerOrderChanged(LayerOrderChangeEvent e); 064 } 065 066 /** 067 * Base class of layer manager events. 068 */ 069 protected static class LayerManagerEvent { 070 private final LayerManager source; 071 072 LayerManagerEvent(LayerManager source) { 073 this.source = source; 074 } 075 076 /** 077 * Returns the {@code LayerManager} at the origin of this event. 078 * @return the {@code LayerManager} at the origin of this event 079 */ 080 public LayerManager getSource() { 081 return source; 082 } 083 } 084 085 /** 086 * The event that is fired whenever a layer was added. 087 * @author Michael Zangl 088 */ 089 public static class LayerAddEvent extends LayerManagerEvent { 090 private final Layer addedLayer; 091 private final boolean requiresZoom; 092 093 LayerAddEvent(LayerManager source, Layer addedLayer, boolean requiresZoom) { 094 super(source); 095 this.addedLayer = addedLayer; 096 this.requiresZoom = requiresZoom; 097 } 098 099 /** 100 * Gets the layer that was added. 101 * @return The added layer. 102 */ 103 public Layer getAddedLayer() { 104 return addedLayer; 105 } 106 107 /** 108 * Determines if an initial zoom is required. 109 * @return {@code true} if a zoom is required when this layer is added 110 * @since 11774 111 */ 112 public final boolean isZoomRequired() { 113 return requiresZoom; 114 } 115 116 @Override 117 public String toString() { 118 return "LayerAddEvent [addedLayer=" + addedLayer + ']'; 119 } 120 } 121 122 /** 123 * The event that is fired before removing a layer. 124 * @author Michael Zangl 125 */ 126 public static class LayerRemoveEvent extends LayerManagerEvent { 127 private final Layer removedLayer; 128 private final boolean lastLayer; 129 private final Collection<Layer> scheduleForRemoval = new ArrayList<>(); 130 131 LayerRemoveEvent(LayerManager source, Layer removedLayer) { 132 super(source); 133 this.removedLayer = removedLayer; 134 this.lastLayer = source.getLayers().size() == 1; 135 } 136 137 /** 138 * Gets the layer that is about to be removed. 139 * @return The layer. 140 */ 141 public Layer getRemovedLayer() { 142 return removedLayer; 143 } 144 145 /** 146 * Check if the layer that was removed is the last layer in the list. 147 * @return <code>true</code> if this was the last layer. 148 * @since 10432 149 */ 150 public boolean isLastLayer() { 151 return lastLayer; 152 } 153 154 /** 155 * Schedule the removal of other layers after this layer has been deleted. 156 * <p> 157 * Duplicate removal requests are ignored. 158 * @param layers The layers to remove. 159 * @since 10507 160 */ 161 public void scheduleRemoval(Collection<? extends Layer> layers) { 162 for (Layer layer : layers) { 163 getSource().checkContainsLayer(layer); 164 } 165 scheduleForRemoval.addAll(layers); 166 } 167 168 @Override 169 public String toString() { 170 return "LayerRemoveEvent [removedLayer=" + removedLayer + ", lastLayer=" + lastLayer + ']'; 171 } 172 } 173 174 /** 175 * An event that is fired whenever the order of layers changed. 176 * <p> 177 * We currently do not report the exact changes. 178 * @author Michael Zangl 179 */ 180 public static class LayerOrderChangeEvent extends LayerManagerEvent { 181 LayerOrderChangeEvent(LayerManager source) { 182 super(source); 183 } 184 185 @Override 186 public String toString() { 187 return "LayerOrderChangeEvent []"; 188 } 189 } 190 191 /** 192 * This is the list of layers we manage. The list is unmodifiable. That way, read access does 193 * not need to be synchronized. 194 * 195 * It is only changed in the EDT. 196 * @see LayerManager#updateLayers(Consumer) 197 */ 198 private volatile List<Layer> layers = Collections.emptyList(); 199 200 private final List<LayerChangeListener> layerChangeListeners = new CopyOnWriteArrayList<>(); 201 202 /** 203 * Add a layer. The layer will be added at a given position and the mapview zoomed at its projection bounds. 204 * @param layer The layer to add 205 */ 206 public void addLayer(final Layer layer) { 207 addLayer(layer, true); 208 } 209 210 /** 211 * Add a layer. The layer will be added at a given position. 212 * @param layer The layer to add 213 * @param initialZoom whether if the mapview must be zoomed at layer projection bounds 214 */ 215 public void addLayer(final Layer layer, final boolean initialZoom) { 216 // we force this on to the EDT Thread to make events fire from there. 217 // The synchronization lock needs to be held by the EDT. 218 GuiHelper.runInEDTAndWaitWithException(() -> realAddLayer(layer, initialZoom)); 219 } 220 221 /** 222 * Add a layer (implementation). 223 * @param layer The layer to add 224 * @param initialZoom whether if the mapview must be zoomed at layer projection bounds 225 */ 226 protected synchronized void realAddLayer(Layer layer, boolean initialZoom) { 227 if (containsLayer(layer)) { 228 throw new IllegalArgumentException("Cannot add a layer twice: " + layer); 229 } 230 LayerPositionStrategy positionStrategy = layer.getDefaultLayerPosition(); 231 int position = positionStrategy.getPosition(this); 232 checkPosition(position); 233 insertLayerAt(layer, position); 234 fireLayerAdded(layer, initialZoom); 235 if (MainApplication.getMap() != null) { 236 layer.hookUpMapView(); // needs to be after fireLayerAdded 237 } 238 } 239 240 /** 241 * Remove the layer from the mapview. If the layer was in the list before, 242 * an LayerChange event is fired. 243 * @param layer The layer to remove 244 */ 245 public void removeLayer(final Layer layer) { 246 // we force this on to the EDT Thread to make events fire from there. 247 // The synchronization lock needs to be held by the EDT. 248 GuiHelper.runInEDTAndWaitWithException(() -> realRemoveLayer(layer)); 249 } 250 251 /** 252 * Remove the layer from the mapview (implementation). 253 * @param layer The layer to remove 254 */ 255 protected synchronized void realRemoveLayer(Layer layer) { 256 GuiHelper.assertCallFromEdt(); 257 Set<Layer> toRemove = Collections.newSetFromMap(new IdentityHashMap<Layer, Boolean>()); 258 toRemove.add(layer); 259 260 while (!toRemove.isEmpty()) { 261 Iterator<Layer> iterator = toRemove.iterator(); 262 Layer layerToRemove = iterator.next(); 263 iterator.remove(); 264 checkContainsLayer(layerToRemove); 265 266 Collection<Layer> newToRemove = realRemoveSingleLayer(layerToRemove); 267 toRemove.addAll(newToRemove); 268 } 269 } 270 271 /** 272 * Remove a single layer from the mapview (implementation). 273 * @param layerToRemove The layer to remove 274 * @return A list of layers that should be removed afterwards. 275 */ 276 protected Collection<Layer> realRemoveSingleLayer(Layer layerToRemove) { 277 updateLayers(mutableLayers -> mutableLayers.remove(layerToRemove)); 278 return fireLayerRemoving(layerToRemove); 279 } 280 281 /** 282 * Move a layer to a new position. 283 * @param layer The layer to move. 284 * @param position The position. 285 * @throws IndexOutOfBoundsException if the position is out of bounds. 286 */ 287 public void moveLayer(final Layer layer, final int position) { 288 // we force this on to the EDT Thread to make events fire from there. 289 // The synchronization lock needs to be held by the EDT. 290 GuiHelper.runInEDTAndWaitWithException(() -> realMoveLayer(layer, position)); 291 } 292 293 /** 294 * Move a layer to a new position (implementation). 295 * @param layer The layer to move. 296 * @param position The position. 297 * @throws IndexOutOfBoundsException if the position is out of bounds. 298 */ 299 protected synchronized void realMoveLayer(Layer layer, int position) { 300 checkContainsLayer(layer); 301 checkPosition(position); 302 303 int curLayerPos = getLayers().indexOf(layer); 304 if (position == curLayerPos) 305 return; // already in place. 306 // update needs to be done in one run 307 updateLayers(mutableLayers -> { 308 mutableLayers.remove(curLayerPos); 309 insertLayerAt(mutableLayers, layer, position); 310 }); 311 fireLayerOrderChanged(); 312 } 313 314 /** 315 * Insert a layer at a given position. 316 * @param layer The layer to add. 317 * @param position The position on which we should add it. 318 */ 319 private void insertLayerAt(Layer layer, int position) { 320 updateLayers(mutableLayers -> insertLayerAt(mutableLayers, layer, position)); 321 } 322 323 private static void insertLayerAt(List<Layer> layers, Layer layer, int position) { 324 if (position == layers.size()) { 325 layers.add(layer); 326 } else { 327 layers.add(position, layer); 328 } 329 } 330 331 /** 332 * Check if the (new) position is valid 333 * @param position The position index 334 * @throws IndexOutOfBoundsException if it is not. 335 */ 336 private void checkPosition(int position) { 337 if (position < 0 || position > getLayers().size()) { 338 throw new IndexOutOfBoundsException("Position " + position + " out of range."); 339 } 340 } 341 342 /** 343 * Update the {@link #layers} field. This method should be used instead of a direct field access. 344 * @param mutator A method that gets the writable list of layers and should modify it. 345 */ 346 private void updateLayers(Consumer<List<Layer>> mutator) { 347 GuiHelper.assertCallFromEdt(); 348 ArrayList<Layer> newLayers = new ArrayList<>(getLayers()); 349 mutator.accept(newLayers); 350 layers = Collections.unmodifiableList(newLayers); 351 } 352 353 /** 354 * Gets an unmodifiable list of all layers that are currently in this manager. This list won't update once layers are added or removed. 355 * @return The list of layers. 356 */ 357 public List<Layer> getLayers() { 358 return layers; 359 } 360 361 /** 362 * Replies an unmodifiable list of layers of a certain type. 363 * 364 * Example: 365 * <pre> 366 * List<WMSLayer> wmsLayers = getLayersOfType(WMSLayer.class); 367 * </pre> 368 * @param <T> The layer type 369 * @param ofType The layer type. 370 * @return an unmodifiable list of layers of a certain type. 371 */ 372 public <T extends Layer> List<T> getLayersOfType(Class<T> ofType) { 373 return new ArrayList<>(Utils.filteredCollection(getLayers(), ofType)); 374 } 375 376 /** 377 * replies true if the list of layers managed by this map view contain layer 378 * 379 * @param layer the layer 380 * @return true if the list of layers managed by this map view contain layer 381 */ 382 public boolean containsLayer(Layer layer) { 383 return getLayers().contains(layer); 384 } 385 386 /** 387 * Checks if the specified layer is handled by this layer manager. 388 * @param layer layer to check 389 * @throws IllegalArgumentException if layer is not handled by this layer manager 390 */ 391 protected void checkContainsLayer(Layer layer) { 392 if (!containsLayer(layer)) { 393 throw new IllegalArgumentException(layer + " is not managed by us."); 394 } 395 } 396 397 /** 398 * Adds a layer change listener 399 * 400 * @param listener the listener. 401 * @throws IllegalArgumentException If the listener was added twice. 402 * @see #addAndFireLayerChangeListener 403 */ 404 public synchronized void addLayerChangeListener(LayerChangeListener listener) { 405 if (layerChangeListeners.contains(listener)) { 406 throw new IllegalArgumentException("Listener already registered."); 407 } 408 layerChangeListeners.add(listener); 409 } 410 411 /** 412 * Adds a layer change listener and fire an add event for every layer in this manager. 413 * 414 * @param listener the listener. 415 * @throws IllegalArgumentException If the listener was added twice. 416 * @see #addLayerChangeListener 417 * @since 11905 418 */ 419 public synchronized void addAndFireLayerChangeListener(LayerChangeListener listener) { 420 addLayerChangeListener(listener); 421 for (Layer l : getLayers()) { 422 listener.layerAdded(new LayerAddEvent(this, l, true)); 423 } 424 } 425 426 /** 427 * Removes a layer change listener 428 * 429 * @param listener the listener. Ignored if null or already registered. 430 * @see #removeAndFireLayerChangeListener 431 */ 432 public synchronized void removeLayerChangeListener(LayerChangeListener listener) { 433 if (!layerChangeListeners.remove(listener)) { 434 throw new IllegalArgumentException("Listener was not registered before: " + listener); 435 } 436 } 437 438 /** 439 * Removes a layer change listener and fire a remove event for every layer in this manager. 440 * The event is fired as if the layer was deleted but 441 * {@link LayerRemoveEvent#scheduleRemoval(Collection)} is ignored. 442 * 443 * @param listener the listener. 444 * @see #removeLayerChangeListener 445 * @since 11905 446 */ 447 public synchronized void removeAndFireLayerChangeListener(LayerChangeListener listener) { 448 removeLayerChangeListener(listener); 449 for (Layer l : getLayers()) { 450 listener.layerRemoving(new LayerRemoveEvent(this, l)); 451 } 452 } 453 454 private void fireLayerAdded(Layer layer, boolean initialZoom) { 455 GuiHelper.assertCallFromEdt(); 456 LayerAddEvent e = new LayerAddEvent(this, layer, initialZoom); 457 for (LayerChangeListener l : layerChangeListeners) { 458 try { 459 l.layerAdded(e); 460 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 461 throw BugReport.intercept(t).put("listener", l).put("event", e); 462 } 463 } 464 } 465 466 /** 467 * Fire the layer remove event 468 * @param layer The layer that was removed 469 * @return A list of layers that should be removed afterwards. 470 */ 471 private Collection<Layer> fireLayerRemoving(Layer layer) { 472 GuiHelper.assertCallFromEdt(); 473 LayerRemoveEvent e = new LayerRemoveEvent(this, layer); 474 for (LayerChangeListener l : layerChangeListeners) { 475 try { 476 l.layerRemoving(e); 477 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 478 throw BugReport.intercept(t).put("listener", l).put("event", e).put("layer", layer); 479 } 480 } 481 if (layer instanceof OsmDataLayer) { 482 ((OsmDataLayer) layer).clear(); 483 } 484 return e.scheduleForRemoval; 485 } 486 487 private void fireLayerOrderChanged() { 488 GuiHelper.assertCallFromEdt(); 489 LayerOrderChangeEvent e = new LayerOrderChangeEvent(this); 490 for (LayerChangeListener l : layerChangeListeners) { 491 try { 492 l.layerOrderChanged(e); 493 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 494 throw BugReport.intercept(t).put("listener", l).put("event", e); 495 } 496 } 497 } 498 499 /** 500 * Reset all layer manager state. This includes removing all layers and then unregistering all listeners 501 * @since 10432 502 */ 503 public void resetState() { 504 // we force this on to the EDT Thread to have a clean synchronization 505 // The synchronization lock needs to be held by the EDT. 506 GuiHelper.runInEDTAndWaitWithException(this::realResetState); 507 } 508 509 /** 510 * Reset all layer manager state (implementation). 511 */ 512 protected synchronized void realResetState() { 513 // The listeners trigger the removal of other layers 514 while (!getLayers().isEmpty()) { 515 removeLayer(getLayers().get(0)); 516 } 517 518 layerChangeListeners.clear(); 519 } 520}