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;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.gui.util.GuiHelper;
015import org.openstreetmap.josm.tools.Utils;
016import org.openstreetmap.josm.tools.bugreport.BugReport;
017
018/**
019 * This class handles the layer management.
020 * <p>
021 * This manager handles a list of layers with the first layer being the front layer.
022 * <h1>Threading</h1>
023 * Methods of this manager may be called from any thread in any order.
024 * Listeners are called while this layer manager is locked, so they should not block.
025 *
026 * @author Michael Zangl
027 * @since 10273
028 */
029public class LayerManager {
030    /**
031     * Interface to notify listeners of a layer change.
032     */
033    public interface LayerChangeListener {
034        /**
035         * Notifies this listener that a layer has been added.
036         * <p>
037         * Listeners are called in the EDT thread and you can manipulate the layer manager in the current thread.
038         * @param e The new added layer event
039         */
040        void layerAdded(LayerAddEvent e);
041
042        /**
043         * Notifies this listener that a layer is about to be removed.
044         * <p>
045         * Listeners are called in the EDT thread and you can manipulate the layer manager in the current thread.
046         * @param e The layer to be removed (as event)
047         */
048        void layerRemoving(LayerRemoveEvent e);
049
050        /**
051         * Notifies this listener that the order of layers was changed.
052         * <p>
053         * Listeners are called in the EDT thread and you can manipulate the layer manager in the current thread.
054         * @param e The order change event.
055         */
056        void layerOrderChanged(LayerOrderChangeEvent e);
057    }
058
059    protected static class LayerManagerEvent {
060        private final LayerManager source;
061
062        LayerManagerEvent(LayerManager source) {
063            this.source = source;
064        }
065
066        public LayerManager getSource() {
067            return source;
068        }
069    }
070
071    /**
072     * The event that is fired whenever a layer was added.
073     * @author Michael Zangl
074     */
075    public static class LayerAddEvent extends LayerManagerEvent {
076        private final Layer addedLayer;
077
078        LayerAddEvent(LayerManager source, Layer addedLayer) {
079            super(source);
080            this.addedLayer = addedLayer;
081        }
082
083        /**
084         * Gets the layer that was added.
085         * @return The added layer.
086         */
087        public Layer getAddedLayer() {
088            return addedLayer;
089        }
090
091        @Override
092        public String toString() {
093            return "LayerAddEvent [addedLayer=" + addedLayer + ']';
094        }
095    }
096
097    /**
098     * The event that is fired before removing a layer.
099     * @author Michael Zangl
100     */
101    public static class LayerRemoveEvent extends LayerManagerEvent {
102        private final Layer removedLayer;
103        private final boolean lastLayer;
104        private Collection<Layer> scheduleForRemoval = new ArrayList<>();
105
106        LayerRemoveEvent(LayerManager source, Layer removedLayer) {
107            super(source);
108            this.removedLayer = removedLayer;
109            this.lastLayer = source.getLayers().size() == 1;
110        }
111
112        /**
113         * Gets the layer that is about to be removed.
114         * @return The layer.
115         */
116        public Layer getRemovedLayer() {
117            return removedLayer;
118        }
119
120        /**
121         * Check if the layer that was removed is the last layer in the list.
122         * @return <code>true</code> if this was the last layer.
123         * @since 10432
124         */
125        public boolean isLastLayer() {
126            return lastLayer;
127        }
128
129        /**
130         * Schedule the removal of other layers after this layer has been deleted.
131         * <p>
132         * Dupplicate removal requests are ignored.
133         * @param layers The layers to remove.
134         * @since 10507
135         */
136        public void scheduleRemoval(Collection<? extends Layer> layers) {
137            for (Layer layer : layers) {
138                getSource().checkContainsLayer(layer);
139            }
140            scheduleForRemoval.addAll(layers);
141        }
142
143        @Override
144        public String toString() {
145            return "LayerRemoveEvent [removedLayer=" + removedLayer + ", lastLayer=" + lastLayer + ']';
146        }
147    }
148
149    /**
150     * An event that is fired whenever the order of layers changed.
151     * <p>
152     * We currently do not report the exact changes.
153     * @author Michael Zangl
154     */
155    public static class LayerOrderChangeEvent extends LayerManagerEvent {
156        LayerOrderChangeEvent(LayerManager source) {
157            super(source);
158        }
159
160        @Override
161        public String toString() {
162            return "LayerOrderChangeEvent []";
163        }
164    }
165
166    /**
167     * This is the list of layers we manage.
168     */
169    private final List<Layer> layers = new ArrayList<>();
170
171    private final List<LayerChangeListener> layerChangeListeners = new CopyOnWriteArrayList<>();
172
173    /**
174     * Add a layer. The layer will be added at a given psoition.
175     * @param layer The layer to add
176     */
177    public void addLayer(final Layer layer) {
178        // we force this on to the EDT Thread to make events fire from there.
179        // The synchronization lock needs to be held by the EDT.
180        GuiHelper.runInEDTAndWaitWithException(new Runnable() {
181            @Override
182            public void run() {
183                realAddLayer(layer);
184            }
185        });
186    }
187
188    protected synchronized void realAddLayer(Layer layer) {
189        if (containsLayer(layer)) {
190            throw new IllegalArgumentException("Cannot add a layer twice.");
191        }
192        LayerPositionStrategy positionStrategy = layer.getDefaultLayerPosition();
193        int position = positionStrategy.getPosition(this);
194        checkPosition(position);
195        insertLayerAt(layer, position);
196        fireLayerAdded(layer);
197        if (Main.map != null) {
198            layer.hookUpMapView(); // needs to be after fireLayerAdded
199        }
200    }
201
202    /**
203     * Remove the layer from the mapview. If the layer was in the list before,
204     * an LayerChange event is fired.
205     * @param layer The layer to remove
206     */
207    public void removeLayer(final Layer layer) {
208        // we force this on to the EDT Thread to make events fire from there.
209        // The synchronization lock needs to be held by the EDT.
210        GuiHelper.runInEDTAndWaitWithException(new Runnable() {
211            @Override
212            public void run() {
213                realRemoveLayer(layer);
214            }
215        });
216    }
217
218    protected synchronized void realRemoveLayer(Layer layer) {
219        GuiHelper.assertCallFromEdt();
220        Set<Layer> toRemove = Collections.newSetFromMap(new IdentityHashMap<Layer, Boolean>());
221        toRemove.add(layer);
222
223        while (!toRemove.isEmpty()) {
224            Iterator<Layer> iterator = toRemove.iterator();
225            Layer layerToRemove = iterator.next();
226            iterator.remove();
227            checkContainsLayer(layerToRemove);
228
229            Collection<Layer> newToRemove = realRemoveSingleLayer(layerToRemove);
230            toRemove.addAll(newToRemove);
231        }
232    }
233
234    protected Collection<Layer> realRemoveSingleLayer(Layer layerToRemove) {
235        Collection<Layer> newToRemove = fireLayerRemoving(layerToRemove);
236        layers.remove(layerToRemove);
237        return newToRemove;
238    }
239
240    /**
241     * Move a layer to a new position.
242     * @param layer The layer to move.
243     * @param position The position.
244     * @throws IndexOutOfBoundsException if the position is out of bounds.
245     */
246    public void moveLayer(final Layer layer, final int position) {
247        // we force this on to the EDT Thread to make events fire from there.
248        // The synchronization lock needs to be held by the EDT.
249        GuiHelper.runInEDTAndWaitWithException(new Runnable() {
250            @Override
251            public void run() {
252                realMoveLayer(layer, position);
253            }
254        });
255    }
256
257    protected synchronized void realMoveLayer(Layer layer, int position) {
258        checkContainsLayer(layer);
259        checkPosition(position);
260
261        int curLayerPos = layers.indexOf(layer);
262        if (position == curLayerPos)
263            return; // already in place.
264        layers.remove(curLayerPos);
265        insertLayerAt(layer, position);
266        fireLayerOrderChanged();
267    }
268
269    /**
270     * Insert a layer at a given position.
271     * @param layer The layer to add.
272     * @param position The position on which we should add it.
273     */
274    private void insertLayerAt(Layer layer, int position) {
275        if (position == layers.size()) {
276            layers.add(layer);
277        } else {
278            layers.add(position, layer);
279        }
280    }
281
282    /**
283     * Check if the (new) position is valid
284     * @param position The position index
285     * @throws IndexOutOfBoundsException if it is not.
286     */
287    private void checkPosition(int position) {
288        if (position < 0 || position > layers.size()) {
289            throw new IndexOutOfBoundsException("Position " + position + " out of range.");
290        }
291    }
292
293    /**
294     * Gets an unmodifiable list of all layers that are currently in this manager. This list won't update once layers are added or removed.
295     * @return The list of layers.
296     */
297    public List<Layer> getLayers() {
298        return Collections.unmodifiableList(new ArrayList<>(layers));
299    }
300
301    /**
302     * Replies an unmodifiable list of layers of a certain type.
303     *
304     * Example:
305     * <pre>
306     *     List&lt;WMSLayer&gt; wmsLayers = getLayersOfType(WMSLayer.class);
307     * </pre>
308     * @param <T> The layer type
309     * @param ofType The layer type.
310     * @return an unmodifiable list of layers of a certain type.
311     */
312    public <T extends Layer> List<T> getLayersOfType(Class<T> ofType) {
313        return new ArrayList<>(Utils.filteredCollection(getLayers(), ofType));
314    }
315
316    /**
317     * replies true if the list of layers managed by this map view contain layer
318     *
319     * @param layer the layer
320     * @return true if the list of layers managed by this map view contain layer
321     */
322    public synchronized boolean containsLayer(Layer layer) {
323        return layers.contains(layer);
324    }
325
326    protected void checkContainsLayer(Layer layer) {
327        if (!containsLayer(layer)) {
328            throw new IllegalArgumentException(layer + " is not managed by us.");
329        }
330    }
331
332    /**
333     * Adds a layer change listener
334     *
335     * @param listener the listener.
336     * @throws IllegalArgumentException If the listener was added twice.
337     */
338    public synchronized void addLayerChangeListener(LayerChangeListener listener) {
339        addLayerChangeListener(listener, false);
340    }
341
342    /**
343     * Adds a layer change listener
344     *
345     * @param listener the listener.
346     * @param fireAdd if we should fire an add event for every layer in this manager.
347     * @throws IllegalArgumentException If the listener was added twice.
348     */
349    public synchronized void addLayerChangeListener(LayerChangeListener listener, boolean fireAdd) {
350        if (layerChangeListeners.contains(listener)) {
351            throw new IllegalArgumentException("Listener already registered.");
352        }
353        layerChangeListeners.add(listener);
354        if (fireAdd) {
355            for (Layer l : getLayers()) {
356                listener.layerAdded(new LayerAddEvent(this, l));
357            }
358        }
359    }
360
361    /**
362     * Removes a layer change listener
363     *
364     * @param listener the listener. Ignored if null or already registered.
365     */
366    public synchronized void removeLayerChangeListener(LayerChangeListener listener) {
367        removeLayerChangeListener(listener, false);
368    }
369
370    /**
371     * Removes a layer change listener
372     *
373     * @param listener the listener.
374     * @param fireRemove if we should fire a remove event for every layer in this manager. The event is fired as if the layer was deleted but
375     * {@link LayerRemoveEvent#scheduleRemoval(Collection)} is ignored.
376     */
377    public synchronized void removeLayerChangeListener(LayerChangeListener listener, boolean fireRemove) {
378        if (!layerChangeListeners.remove(listener)) {
379            throw new IllegalArgumentException("Listener was not registered before: " + listener);
380        } else {
381            if (fireRemove) {
382                for (Layer l : getLayers()) {
383                    listener.layerRemoving(new LayerRemoveEvent(this, l));
384                }
385            }
386        }
387    }
388
389    private void fireLayerAdded(Layer layer) {
390        GuiHelper.assertCallFromEdt();
391        LayerAddEvent e = new LayerAddEvent(this, layer);
392        for (LayerChangeListener l : layerChangeListeners) {
393            try {
394                l.layerAdded(e);
395            } catch (RuntimeException t) {
396                throw BugReport.intercept(t).put("listener", l).put("event", e);
397            }
398        }
399    }
400
401    /**
402     * Fire the layer remove event
403     * @param layer The layer to remove
404     * @return A list of layers that should be removed afterwards.
405     */
406    private Collection<Layer> fireLayerRemoving(Layer layer) {
407        GuiHelper.assertCallFromEdt();
408        LayerRemoveEvent e = new LayerRemoveEvent(this, layer);
409        for (LayerChangeListener l : layerChangeListeners) {
410            try {
411                l.layerRemoving(e);
412            } catch (RuntimeException t) {
413                throw BugReport.intercept(t).put("listener", l).put("event", e).put("layer", layer);
414            }
415        }
416        return e.scheduleForRemoval;
417    }
418
419    private void fireLayerOrderChanged() {
420        GuiHelper.assertCallFromEdt();
421        LayerOrderChangeEvent e = new LayerOrderChangeEvent(this);
422        for (LayerChangeListener l : layerChangeListeners) {
423            try {
424                l.layerOrderChanged(e);
425            } catch (RuntimeException t) {
426                throw BugReport.intercept(t).put("listener", l).put("event", e);
427            }
428        }
429    }
430
431    /**
432     * Reset all layer manager state. This includes removing all layers and then unregistering all listeners
433     * @since 10432
434     */
435    public void resetState() {
436        // some layer remove listeners remove other layers.
437        while (!getLayers().isEmpty()) {
438            removeLayer(getLayers().get(0));
439        }
440
441        layerChangeListeners.clear();
442    }
443}