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 alayer 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 * Dupplicate 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 unmodifyable. That way, read access does not need to be synchronized. 193 * 194 * It is only changed in the EDT. 195 * @see LayerManager#updateLayers(Consumer) 196 */ 197 private volatile List<Layer> layers = Collections.emptyList(); 198 199 private final List<LayerChangeListener> layerChangeListeners = new CopyOnWriteArrayList<>(); 200 201 /** 202 * Add a layer. The layer will be added at a given position and the mapview zoomed at its projection bounds. 203 * @param layer The layer to add 204 */ 205 public void addLayer(final Layer layer) { 206 addLayer(layer, true); 207 } 208 209 /** 210 * Add a layer. The layer will be added at a given position. 211 * @param layer The layer to add 212 * @param initialZoom whether if the mapview must be zoomed at layer projection bounds 213 */ 214 public void addLayer(final Layer layer, final boolean initialZoom) { 215 // we force this on to the EDT Thread to make events fire from there. 216 // The synchronization lock needs to be held by the EDT. 217 GuiHelper.runInEDTAndWaitWithException(() -> realAddLayer(layer, initialZoom)); 218 } 219 220 /** 221 * Add a layer (implementation). 222 * @param layer The layer to add 223 * @param initialZoom whether if the mapview must be zoomed at layer projection bounds 224 */ 225 protected synchronized void realAddLayer(Layer layer, boolean initialZoom) { 226 if (containsLayer(layer)) { 227 throw new IllegalArgumentException("Cannot add a layer twice: " + layer); 228 } 229 LayerPositionStrategy positionStrategy = layer.getDefaultLayerPosition(); 230 int position = positionStrategy.getPosition(this); 231 checkPosition(position); 232 insertLayerAt(layer, position); 233 fireLayerAdded(layer, initialZoom); 234 if (MainApplication.getMap() != null) { 235 layer.hookUpMapView(); // needs to be after fireLayerAdded 236 } 237 } 238 239 /** 240 * Remove the layer from the mapview. If the layer was in the list before, 241 * an LayerChange event is fired. 242 * @param layer The layer to remove 243 */ 244 public void removeLayer(final Layer layer) { 245 // we force this on to the EDT Thread to make events fire from there. 246 // The synchronization lock needs to be held by the EDT. 247 GuiHelper.runInEDTAndWaitWithException(() -> realRemoveLayer(layer)); 248 } 249 250 /** 251 * Remove the layer from the mapview (implementation). 252 * @param layer The layer to remove 253 */ 254 protected synchronized void realRemoveLayer(Layer layer) { 255 GuiHelper.assertCallFromEdt(); 256 Set<Layer> toRemove = Collections.newSetFromMap(new IdentityHashMap<Layer, Boolean>()); 257 toRemove.add(layer); 258 259 while (!toRemove.isEmpty()) { 260 Iterator<Layer> iterator = toRemove.iterator(); 261 Layer layerToRemove = iterator.next(); 262 iterator.remove(); 263 checkContainsLayer(layerToRemove); 264 265 Collection<Layer> newToRemove = realRemoveSingleLayer(layerToRemove); 266 toRemove.addAll(newToRemove); 267 } 268 } 269 270 /** 271 * Remove a single layer from the mapview (implementation). 272 * @param layerToRemove The layer to remove 273 * @return A list of layers that should be removed afterwards. 274 */ 275 protected Collection<Layer> realRemoveSingleLayer(Layer layerToRemove) { 276 updateLayers(mutableLayers -> mutableLayers.remove(layerToRemove)); 277 return fireLayerRemoving(layerToRemove); 278 } 279 280 /** 281 * Move a layer to a new position. 282 * @param layer The layer to move. 283 * @param position The position. 284 * @throws IndexOutOfBoundsException if the position is out of bounds. 285 */ 286 public void moveLayer(final Layer layer, final int position) { 287 // we force this on to the EDT Thread to make events fire from there. 288 // The synchronization lock needs to be held by the EDT. 289 GuiHelper.runInEDTAndWaitWithException(() -> realMoveLayer(layer, position)); 290 } 291 292 /** 293 * Move a layer to a new position (implementation). 294 * @param layer The layer to move. 295 * @param position The position. 296 * @throws IndexOutOfBoundsException if the position is out of bounds. 297 */ 298 protected synchronized void realMoveLayer(Layer layer, int position) { 299 checkContainsLayer(layer); 300 checkPosition(position); 301 302 int curLayerPos = getLayers().indexOf(layer); 303 if (position == curLayerPos) 304 return; // already in place. 305 // update needs to be done in one run 306 updateLayers(mutableLayers -> { 307 mutableLayers.remove(curLayerPos); 308 insertLayerAt(mutableLayers, layer, position); 309 }); 310 fireLayerOrderChanged(); 311 } 312 313 /** 314 * Insert a layer at a given position. 315 * @param layer The layer to add. 316 * @param position The position on which we should add it. 317 */ 318 private void insertLayerAt(Layer layer, int position) { 319 updateLayers(mutableLayers -> insertLayerAt(mutableLayers, layer, position)); 320 } 321 322 private static void insertLayerAt(List<Layer> layers, Layer layer, int position) { 323 if (position == layers.size()) { 324 layers.add(layer); 325 } else { 326 layers.add(position, layer); 327 } 328 } 329 330 /** 331 * Check if the (new) position is valid 332 * @param position The position index 333 * @throws IndexOutOfBoundsException if it is not. 334 */ 335 private void checkPosition(int position) { 336 if (position < 0 || position > getLayers().size()) { 337 throw new IndexOutOfBoundsException("Position " + position + " out of range."); 338 } 339 } 340 341 /** 342 * Update the {@link #layers} field. This method should be used instead of a direct field access. 343 * @param mutator A method that gets the writable list of layers and should modify it. 344 */ 345 private void updateLayers(Consumer<List<Layer>> mutator) { 346 GuiHelper.assertCallFromEdt(); 347 ArrayList<Layer> newLayers = new ArrayList<>(getLayers()); 348 mutator.accept(newLayers); 349 layers = Collections.unmodifiableList(newLayers); 350 } 351 352 /** 353 * Gets an unmodifiable list of all layers that are currently in this manager. This list won't update once layers are added or removed. 354 * @return The list of layers. 355 */ 356 public List<Layer> getLayers() { 357 return layers; 358 } 359 360 /** 361 * Replies an unmodifiable list of layers of a certain type. 362 * 363 * Example: 364 * <pre> 365 * List<WMSLayer> wmsLayers = getLayersOfType(WMSLayer.class); 366 * </pre> 367 * @param <T> The layer type 368 * @param ofType The layer type. 369 * @return an unmodifiable list of layers of a certain type. 370 */ 371 public <T extends Layer> List<T> getLayersOfType(Class<T> ofType) { 372 return new ArrayList<>(Utils.filteredCollection(getLayers(), ofType)); 373 } 374 375 /** 376 * replies true if the list of layers managed by this map view contain layer 377 * 378 * @param layer the layer 379 * @return true if the list of layers managed by this map view contain layer 380 */ 381 public boolean containsLayer(Layer layer) { 382 return getLayers().contains(layer); 383 } 384 385 /** 386 * Checks if the specified layer is handled by this layer manager. 387 * @param layer layer to check 388 * @throws IllegalArgumentException if layer is not handled by this layer manager 389 */ 390 protected void checkContainsLayer(Layer layer) { 391 if (!containsLayer(layer)) { 392 throw new IllegalArgumentException(layer + " is not managed by us."); 393 } 394 } 395 396 /** 397 * Adds a layer change listener 398 * 399 * @param listener the listener. 400 * @throws IllegalArgumentException If the listener was added twice. 401 * @see #addAndFireLayerChangeListener 402 */ 403 public synchronized void addLayerChangeListener(LayerChangeListener listener) { 404 if (layerChangeListeners.contains(listener)) { 405 throw new IllegalArgumentException("Listener already registered."); 406 } 407 layerChangeListeners.add(listener); 408 } 409 410 /** 411 * Adds a layer change listener and fire an add event for every layer in this manager. 412 * 413 * @param listener the listener. 414 * @throws IllegalArgumentException If the listener was added twice. 415 * @see #addLayerChangeListener 416 * @since 11905 417 */ 418 public synchronized void addAndFireLayerChangeListener(LayerChangeListener listener) { 419 addLayerChangeListener(listener); 420 for (Layer l : getLayers()) { 421 listener.layerAdded(new LayerAddEvent(this, l, true)); 422 } 423 } 424 425 /** 426 * Removes a layer change listener 427 * 428 * @param listener the listener. Ignored if null or already registered. 429 * @see #removeAndFireLayerChangeListener 430 */ 431 public synchronized void removeLayerChangeListener(LayerChangeListener listener) { 432 if (!layerChangeListeners.remove(listener)) { 433 throw new IllegalArgumentException("Listener was not registered before: " + listener); 434 } 435 } 436 437 /** 438 * Removes a layer change listener and fire a remove event for every layer in this manager. 439 * The event is fired as if the layer was deleted but 440 * {@link LayerRemoveEvent#scheduleRemoval(Collection)} is ignored. 441 * 442 * @param listener the listener. 443 * @see #removeLayerChangeListener 444 * @since 11905 445 */ 446 public synchronized void removeAndFireLayerChangeListener(LayerChangeListener listener) { 447 removeLayerChangeListener(listener); 448 for (Layer l : getLayers()) { 449 listener.layerRemoving(new LayerRemoveEvent(this, l)); 450 } 451 } 452 453 private void fireLayerAdded(Layer layer, boolean initialZoom) { 454 GuiHelper.assertCallFromEdt(); 455 LayerAddEvent e = new LayerAddEvent(this, layer, initialZoom); 456 for (LayerChangeListener l : layerChangeListeners) { 457 try { 458 l.layerAdded(e); 459 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 460 throw BugReport.intercept(t).put("listener", l).put("event", e); 461 } 462 } 463 } 464 465 /** 466 * Fire the layer remove event 467 * @param layer The layer that was removed 468 * @return A list of layers that should be removed afterwards. 469 */ 470 private Collection<Layer> fireLayerRemoving(Layer layer) { 471 GuiHelper.assertCallFromEdt(); 472 LayerRemoveEvent e = new LayerRemoveEvent(this, layer); 473 for (LayerChangeListener l : layerChangeListeners) { 474 try { 475 l.layerRemoving(e); 476 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 477 throw BugReport.intercept(t).put("listener", l).put("event", e).put("layer", layer); 478 } 479 } 480 if (layer instanceof OsmDataLayer) { 481 ((OsmDataLayer) layer).clear(); 482 } 483 return e.scheduleForRemoval; 484 } 485 486 private void fireLayerOrderChanged() { 487 GuiHelper.assertCallFromEdt(); 488 LayerOrderChangeEvent e = new LayerOrderChangeEvent(this); 489 for (LayerChangeListener l : layerChangeListeners) { 490 try { 491 l.layerOrderChanged(e); 492 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) { 493 throw BugReport.intercept(t).put("listener", l).put("event", e); 494 } 495 } 496 } 497 498 /** 499 * Reset all layer manager state. This includes removing all layers and then unregistering all listeners 500 * @since 10432 501 */ 502 public void resetState() { 503 // we force this on to the EDT Thread to have a clean synchronization 504 // The synchronization lock needs to be held by the EDT. 505 GuiHelper.runInEDTAndWaitWithException(this::realResetState); 506 } 507 508 /** 509 * Reset all layer manager state (implementation). 510 */ 511 protected synchronized void realResetState() { 512 // The listeners trigger the removal of other layers 513 while (!getLayers().isEmpty()) { 514 removeLayer(getLayers().get(0)); 515 } 516 517 layerChangeListeners.clear(); 518 } 519}