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&lt;WMSLayer&gt; 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        return e.scheduleForRemoval;
481    }
482
483    private void fireLayerOrderChanged() {
484        GuiHelper.assertCallFromEdt();
485        LayerOrderChangeEvent e = new LayerOrderChangeEvent(this);
486        for (LayerChangeListener l : layerChangeListeners) {
487            try {
488                l.layerOrderChanged(e);
489            } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) {
490                throw BugReport.intercept(t).put("listener", l).put("event", e);
491            }
492        }
493    }
494
495    /**
496     * Reset all layer manager state. This includes removing all layers and then unregistering all listeners
497     * @since 10432
498     */
499    public void resetState() {
500        // we force this on to the EDT Thread to have a clean synchronization
501        // The synchronization lock needs to be held by the EDT.
502        GuiHelper.runInEDTAndWaitWithException(this::realResetState);
503    }
504
505    /**
506     * Reset all layer manager state (implementation).
507     */
508    protected synchronized void realResetState() {
509        // The listeners trigger the removal of other layers
510        while (!getLayers().isEmpty()) {
511            removeLayer(getLayers().get(0));
512        }
513
514        layerChangeListeners.clear();
515    }
516}