// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.layer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.tools.JosmRuntimeException;
import org.openstreetmap.josm.tools.Utils;
import org.openstreetmap.josm.tools.bugreport.BugReport;
/**
* This class handles the layer management.
* <p>
* This manager handles a list of layers with the first layer being the front layer.
* <h1>Threading</h1>
* Synchronization of the layer manager is done by synchronizing all read/write access. All changes are internally done in the EDT thread.
*
* Methods of this manager may be called from any thread in any order.
* Listeners are called while this layer manager is locked, so they should not block on other threads.
*
* @author Michael Zangl
* @since 10273
*/
public class LayerManager {
/**
* Interface to notify listeners of a layer change.
*/
public interface LayerChangeListener {
/**
* Notifies this listener that a layer has been added.
* <p>
* Listeners are called in the EDT thread. You should not do blocking or long-running tasks in this method.
* @param e The new added layer event
*/
void layerAdded(LayerAddEvent e);
/**
* Notifies this listener that a alayer was just removed.
* <p>
* Listeners are called in the EDT thread after the layer was removed.
* Use {@link LayerRemoveEvent#scheduleRemoval(Collection)} to remove more layers.
* You should not do blocking or long-running tasks in this method.
* @param e The layer to be removed (as event)
*/
void layerRemoving(LayerRemoveEvent e);
/**
* Notifies this listener that the order of layers was changed.
* <p>
* Listeners are called in the EDT thread.
* You should not do blocking or long-running tasks in this method.
* @param e The order change event.
*/
void layerOrderChanged(LayerOrderChangeEvent e);
}
protected static class LayerManagerEvent {
private final LayerManager source;
LayerManagerEvent(LayerManager source) {
this.source = source;
}
/**
* Returns the {@code LayerManager} at the origin of this event.
* @return the {@code LayerManager} at the origin of this event
*/
public LayerManager getSource() {
return source;
}
}
/**
* The event that is fired whenever a layer was added.
* @author Michael Zangl
*/
public static class LayerAddEvent extends LayerManagerEvent {
private final Layer addedLayer;
private final boolean requiresZoom;
LayerAddEvent(LayerManager source, Layer addedLayer, boolean requiresZoom) {
super(source);
this.addedLayer = addedLayer;
this.requiresZoom = requiresZoom;
}
/**
* Gets the layer that was added.
* @return The added layer.
*/
public Layer getAddedLayer() {
return addedLayer;
}
/**
* Determines if an initial zoom is required.
* @return {@code true} if a zoom is required when this layer is added
* @since 11774
*/
public final boolean isZoomRequired() {
return requiresZoom;
}
@Override
public String toString() {
return "LayerAddEvent [addedLayer=" + addedLayer + ']';
}
}
/**
* The event that is fired before removing a layer.
* @author Michael Zangl
*/
public static class LayerRemoveEvent extends LayerManagerEvent {
private final Layer removedLayer;
private final boolean lastLayer;
private final Collection<Layer> scheduleForRemoval = new ArrayList<>();
LayerRemoveEvent(LayerManager source, Layer removedLayer) {
super(source);
this.removedLayer = removedLayer;
this.lastLayer = source.getLayers().size() == 1;
}
/**
* Gets the layer that is about to be removed.
* @return The layer.
*/
public Layer getRemovedLayer() {
return removedLayer;
}
/**
* Check if the layer that was removed is the last layer in the list.
* @return <code>true</code> if this was the last layer.
* @since 10432
*/
public boolean isLastLayer() {
return lastLayer;
}
/**
* Schedule the removal of other layers after this layer has been deleted.
* <p>
* Dupplicate removal requests are ignored.
* @param layers The layers to remove.
* @since 10507
*/
public void scheduleRemoval(Collection<? extends Layer> layers) {
for (Layer layer : layers) {
getSource().checkContainsLayer(layer);
}
scheduleForRemoval.addAll(layers);
}
@Override
public String toString() {
return "LayerRemoveEvent [removedLayer=" + removedLayer + ", lastLayer=" + lastLayer + ']';
}
}
/**
* An event that is fired whenever the order of layers changed.
* <p>
* We currently do not report the exact changes.
* @author Michael Zangl
*/
public static class LayerOrderChangeEvent extends LayerManagerEvent {
LayerOrderChangeEvent(LayerManager source) {
super(source);
}
@Override
public String toString() {
return "LayerOrderChangeEvent []";
}
}
/**
* This is the list of layers we manage. The list is unmodifyable. That way, read access does not need to be synchronized.
*
* It is only changed in the EDT.
* @see LayerManager#updateLayers(Consumer)
*/
private volatile List<Layer> layers = Collections.emptyList();
private final List<LayerChangeListener> layerChangeListeners = new CopyOnWriteArrayList<>();
/**
* Add a layer. The layer will be added at a given position and the mapview zoomed at its projection bounds.
* @param layer The layer to add
*/
public void addLayer(final Layer layer) {
addLayer(layer, true);
}
/**
* Add a layer. The layer will be added at a given position.
* @param layer The layer to add
* @param initialZoom whether if the mapview must be zoomed at layer projection bounds
*/
public void addLayer(final Layer layer, final boolean initialZoom) {
// we force this on to the EDT Thread to make events fire from there.
// The synchronization lock needs to be held by the EDT.
GuiHelper.runInEDTAndWaitWithException(() -> realAddLayer(layer, initialZoom));
}
protected synchronized void realAddLayer(Layer layer, boolean initialZoom) {
if (containsLayer(layer)) {
throw new IllegalArgumentException("Cannot add a layer twice: " + layer);
}
LayerPositionStrategy positionStrategy = layer.getDefaultLayerPosition();
int position = positionStrategy.getPosition(this);
checkPosition(position);
insertLayerAt(layer, position);
fireLayerAdded(layer, initialZoom);
if (Main.map != null) {
layer.hookUpMapView(); // needs to be after fireLayerAdded
}
}
/**
* Remove the layer from the mapview. If the layer was in the list before,
* an LayerChange event is fired.
* @param layer The layer to remove
*/
public void removeLayer(final Layer layer) {
// we force this on to the EDT Thread to make events fire from there.
// The synchronization lock needs to be held by the EDT.
GuiHelper.runInEDTAndWaitWithException(() -> realRemoveLayer(layer));
}
protected synchronized void realRemoveLayer(Layer layer) {
GuiHelper.assertCallFromEdt();
Set<Layer> toRemove = Collections.newSetFromMap(new IdentityHashMap<Layer, Boolean>());
toRemove.add(layer);
while (!toRemove.isEmpty()) {
Iterator<Layer> iterator = toRemove.iterator();
Layer layerToRemove = iterator.next();
iterator.remove();
checkContainsLayer(layerToRemove);
Collection<Layer> newToRemove = realRemoveSingleLayer(layerToRemove);
toRemove.addAll(newToRemove);
}
}
protected Collection<Layer> realRemoveSingleLayer(Layer layerToRemove) {
updateLayers(mutableLayers -> mutableLayers.remove(layerToRemove));
return fireLayerRemoving(layerToRemove);
}
/**
* Move a layer to a new position.
* @param layer The layer to move.
* @param position The position.
* @throws IndexOutOfBoundsException if the position is out of bounds.
*/
public void moveLayer(final Layer layer, final int position) {
// we force this on to the EDT Thread to make events fire from there.
// The synchronization lock needs to be held by the EDT.
GuiHelper.runInEDTAndWaitWithException(() -> realMoveLayer(layer, position));
}
protected synchronized void realMoveLayer(Layer layer, int position) {
checkContainsLayer(layer);
checkPosition(position);
int curLayerPos = getLayers().indexOf(layer);
if (position == curLayerPos)
return; // already in place.
// update needs to be done in one run
updateLayers(mutableLayers -> {
mutableLayers.remove(curLayerPos);
insertLayerAt(mutableLayers, layer, position);
});
fireLayerOrderChanged();
}
/**
* Insert a layer at a given position.
* @param layer The layer to add.
* @param position The position on which we should add it.
*/
private void insertLayerAt(Layer layer, int position) {
updateLayers(mutableLayers -> insertLayerAt(mutableLayers, layer, position));
}
private static void insertLayerAt(List<Layer> layers, Layer layer, int position) {
if (position == layers.size()) {
layers.add(layer);
} else {
layers.add(position, layer);
}
}
/**
* Check if the (new) position is valid
* @param position The position index
* @throws IndexOutOfBoundsException if it is not.
*/
private void checkPosition(int position) {
if (position < 0 || position > getLayers().size()) {
throw new IndexOutOfBoundsException("Position " + position + " out of range.");
}
}
/**
* Update the {@link #layers} field. This method should be used instead of a direct field access.
* @param mutator A method that gets the writable list of layers and should modify it.
*/
private void updateLayers(Consumer<List<Layer>> mutator) {
GuiHelper.assertCallFromEdt();
ArrayList<Layer> newLayers = new ArrayList<>(getLayers());
mutator.accept(newLayers);
layers = Collections.unmodifiableList(newLayers);
}
/**
* Gets an unmodifiable list of all layers that are currently in this manager. This list won't update once layers are added or removed.
* @return The list of layers.
*/
public List<Layer> getLayers() {
return layers;
}
/**
* Replies an unmodifiable list of layers of a certain type.
*
* Example:
* <pre>
* List<WMSLayer> wmsLayers = getLayersOfType(WMSLayer.class);
* </pre>
* @param <T> The layer type
* @param ofType The layer type.
* @return an unmodifiable list of layers of a certain type.
*/
public <T extends Layer> List<T> getLayersOfType(Class<T> ofType) {
return new ArrayList<>(Utils.filteredCollection(getLayers(), ofType));
}
/**
* replies true if the list of layers managed by this map view contain layer
*
* @param layer the layer
* @return true if the list of layers managed by this map view contain layer
*/
public boolean containsLayer(Layer layer) {
return getLayers().contains(layer);
}
protected void checkContainsLayer(Layer layer) {
if (!containsLayer(layer)) {
throw new IllegalArgumentException(layer + " is not managed by us.");
}
}
/**
* Adds a layer change listener
*
* @param listener the listener.
* @throws IllegalArgumentException If the listener was added twice.
*/
public synchronized void addLayerChangeListener(LayerChangeListener listener) {
addLayerChangeListener(listener, false);
}
/**
* Adds a layer change listener
*
* @param listener the listener.
* @param fireAdd if we should fire an add event for every layer in this manager.
* @throws IllegalArgumentException If the listener was added twice.
*/
public synchronized void addLayerChangeListener(LayerChangeListener listener, boolean fireAdd) {
if (layerChangeListeners.contains(listener)) {
throw new IllegalArgumentException("Listener already registered.");
}
layerChangeListeners.add(listener);
if (fireAdd) {
for (Layer l : getLayers()) {
listener.layerAdded(new LayerAddEvent(this, l, true));
}
}
}
/**
* Removes a layer change listener
*
* @param listener the listener. Ignored if null or already registered.
*/
public synchronized void removeLayerChangeListener(LayerChangeListener listener) {
removeLayerChangeListener(listener, false);
}
/**
* Removes a layer change listener
*
* @param listener the listener.
* @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
* {@link LayerRemoveEvent#scheduleRemoval(Collection)} is ignored.
*/
public synchronized void removeLayerChangeListener(LayerChangeListener listener, boolean fireRemove) {
if (!layerChangeListeners.remove(listener)) {
throw new IllegalArgumentException("Listener was not registered before: " + listener);
} else {
if (fireRemove) {
for (Layer l : getLayers()) {
listener.layerRemoving(new LayerRemoveEvent(this, l));
}
}
}
}
private void fireLayerAdded(Layer layer, boolean initialZoom) {
GuiHelper.assertCallFromEdt();
LayerAddEvent e = new LayerAddEvent(this, layer, initialZoom);
for (LayerChangeListener l : layerChangeListeners) {
try {
l.layerAdded(e);
} catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) {
throw BugReport.intercept(t).put("listener", l).put("event", e);
}
}
}
/**
* Fire the layer remove event
* @param layer The layer that was removed
* @return A list of layers that should be removed afterwards.
*/
private Collection<Layer> fireLayerRemoving(Layer layer) {
GuiHelper.assertCallFromEdt();
LayerRemoveEvent e = new LayerRemoveEvent(this, layer);
for (LayerChangeListener l : layerChangeListeners) {
try {
l.layerRemoving(e);
} catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) {
throw BugReport.intercept(t).put("listener", l).put("event", e).put("layer", layer);
}
}
return e.scheduleForRemoval;
}
private void fireLayerOrderChanged() {
GuiHelper.assertCallFromEdt();
LayerOrderChangeEvent e = new LayerOrderChangeEvent(this);
for (LayerChangeListener l : layerChangeListeners) {
try {
l.layerOrderChanged(e);
} catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException t) {
throw BugReport.intercept(t).put("listener", l).put("event", e);
}
}
}
/**
* Reset all layer manager state. This includes removing all layers and then unregistering all listeners
* @since 10432
*/
public void resetState() {
// we force this on to the EDT Thread to have a clean synchronization
// The synchronization lock needs to be held by the EDT.
GuiHelper.runInEDTAndWaitWithException(this::realResetState);
}
protected synchronized void realResetState() {
// The listeners trigger the removal of other layers
while (!getLayers().isEmpty()) {
removeLayer(getLayers().get(0));
}
layerChangeListeners.clear();
}
}