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.data.osm.DataSet;
015import org.openstreetmap.josm.gui.MainApplication;
016import org.openstreetmap.josm.gui.util.GuiHelper;
017import org.openstreetmap.josm.tools.JosmRuntimeException;
018import org.openstreetmap.josm.tools.Utils;
019import org.openstreetmap.josm.tools.bugreport.BugReport;
020
021/**
022 * This class handles the layer management.
023 * <p>
024 * This manager handles a list of layers with the first layer being the front layer.
025 * <h1>Threading</h1>
026 * Synchronization of the layer manager is done by synchronizing all read/write access. All changes are internally done in the EDT thread.
027 *
028 * Methods of this manager may be called from any thread in any order.
029 * Listeners are called while this layer manager is locked, so they should not block on other threads.
030 *
031 * @author Michael Zangl
032 * @since 10273
033 */
034public class LayerManager {
035    /**
036     * Interface to notify listeners of a layer change.
037     */
038    public interface LayerChangeListener {
039        /**
040         * Notifies this listener that a layer has been added.
041         * <p>
042         * Listeners are called in the EDT thread. You should not do blocking or long-running tasks in this method.
043         * @param e The new added layer event
044         */
045        void layerAdded(LayerAddEvent e);
046
047        /**
048         * Notifies this listener that a alayer was just removed.
049         * <p>
050         * Listeners are called in the EDT thread after the layer was removed.
051         * Use {@link LayerRemoveEvent#scheduleRemoval(Collection)} to remove more layers.
052         * You should not do blocking or long-running tasks in this method.
053         * @param e The layer to be removed (as event)
054         */
055        void layerRemoving(LayerRemoveEvent e);
056
057        /**
058         * Notifies this listener that the order of layers was changed.
059         * <p>
060         * Listeners are called in the EDT thread.
061         *  You should not do blocking or long-running tasks in this method.
062         * @param e The order change event.
063         */
064        void layerOrderChanged(LayerOrderChangeEvent e);
065    }
066
067    /**
068     * Base class of layer manager events.
069     */
070    protected static class LayerManagerEvent {
071        private final LayerManager source;
072
073        LayerManagerEvent(LayerManager source) {
074            this.source = source;
075        }
076
077        /**
078         * Returns the {@code LayerManager} at the origin of this event.
079         * @return the {@code LayerManager} at the origin of this event
080         */
081        public LayerManager getSource() {
082            return source;
083        }
084    }
085
086    /**
087     * The event that is fired whenever a layer was added.
088     * @author Michael Zangl
089     */
090    public static class LayerAddEvent extends LayerManagerEvent {
091        private final Layer addedLayer;
092        private final boolean requiresZoom;
093
094        LayerAddEvent(LayerManager source, Layer addedLayer, boolean requiresZoom) {
095            super(source);
096            this.addedLayer = addedLayer;
097            this.requiresZoom = requiresZoom;
098        }
099
100        /**
101         * Gets the layer that was added.
102         * @return The added layer.
103         */
104        public Layer getAddedLayer() {
105            return addedLayer;
106        }
107
108        /**
109         * Determines if an initial zoom is required.
110         * @return {@code true} if a zoom is required when this layer is added
111         * @since 11774
112         */
113        public final boolean isZoomRequired() {
114            return requiresZoom;
115        }
116
117        @Override
118        public String toString() {
119            return "LayerAddEvent [addedLayer=" + addedLayer + ']';
120        }
121    }
122
123    /**
124     * The event that is fired before removing a layer.
125     * @author Michael Zangl
126     */
127    public static class LayerRemoveEvent extends LayerManagerEvent {
128        private final Layer removedLayer;
129        private final boolean lastLayer;
130        private final Collection<Layer> scheduleForRemoval = new ArrayList<>();
131
132        LayerRemoveEvent(LayerManager source, Layer removedLayer) {
133            super(source);
134            this.removedLayer = removedLayer;
135            this.lastLayer = source.getLayers().size() == 1;
136        }
137
138        /**
139         * Gets the layer that is about to be removed.
140         * @return The layer.
141         */
142        public Layer getRemovedLayer() {
143            return removedLayer;
144        }
145
146        /**
147         * Check if the layer that was removed is the last layer in the list.
148         * @return <code>true</code> if this was the last layer.
149         * @since 10432
150         */
151        public boolean isLastLayer() {
152            return lastLayer;
153        }
154
155        /**
156         * Schedule the removal of other layers after this layer has been deleted.
157         * <p>
158         * Dupplicate removal requests are ignored.
159         * @param layers The layers to remove.
160         * @since 10507
161         */
162        public void scheduleRemoval(Collection<? extends Layer> layers) {
163            for (Layer layer : layers) {
164                getSource().checkContainsLayer(layer);
165            }
166            scheduleForRemoval.addAll(layers);
167        }
168
169        @Override
170        public String toString() {
171            return "LayerRemoveEvent [removedLayer=" + removedLayer + ", lastLayer=" + lastLayer + ']';
172        }
173    }
174
175    /**
176     * An event that is fired whenever the order of layers changed.
177     * <p>
178     * We currently do not report the exact changes.
179     * @author Michael Zangl
180     */
181    public static class LayerOrderChangeEvent extends LayerManagerEvent {
182        LayerOrderChangeEvent(LayerManager source) {
183            super(source);
184        }
185
186        @Override
187        public String toString() {
188            return "LayerOrderChangeEvent []";
189        }
190    }
191
192    /**
193     * This is the list of layers we manage. The list is unmodifyable. That way, read access does 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&lt;WMSLayer&gt; 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            DataSet data = ((OsmDataLayer) layer).data;
483            if (data != null && !data.isLocked())
484                data.clear();
485        }
486        return e.scheduleForRemoval;
487    }
488
489    private void fireLayerOrderChanged() {
490        GuiHelper.assertCallFromEdt();
491        LayerOrderChangeEvent e = new LayerOrderChangeEvent(this);
492        for (LayerChangeListener l : layerChangeListeners) {
493            try {
494                l.layerOrderChanged(e);
495            } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) {
496                throw BugReport.intercept(t).put("listener", l).put("event", e);
497            }
498        }
499    }
500
501    /**
502     * Reset all layer manager state. This includes removing all layers and then unregistering all listeners
503     * @since 10432
504     */
505    public void resetState() {
506        // we force this on to the EDT Thread to have a clean synchronization
507        // The synchronization lock needs to be held by the EDT.
508        GuiHelper.runInEDTAndWaitWithException(this::realResetState);
509    }
510
511    /**
512     * Reset all layer manager state (implementation).
513     */
514    protected synchronized void realResetState() {
515        // The listeners trigger the removal of other layers
516        while (!getLayers().isEmpty()) {
517            removeLayer(getLayers().get(0));
518        }
519
520        layerChangeListeners.clear();
521    }
522}