// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.layer;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.GraphicsEnvironment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.io.AsynchronousUploadPrimitivesTask;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.tools.Logging;
/**
* This class extends the layer manager by adding an active and an edit layer.
*
* The active layer is the layer the user is currently working on.
*
* The edit layer is an data layer that we currently work with.
* @author Michael Zangl
* @since 10279
*/
public class MainLayerManager extends LayerManager {
/**
* This listener listens to changes of the active or the edit layer.
* @author Michael Zangl
* @since 10600 (functional interface)
*/
@FunctionalInterface
public interface ActiveLayerChangeListener {
/**
* Called whenever the active or edit layer changed.
*
* You can be sure that this layer is still contained in this set.
*
* Listeners are called in the EDT thread and you can manipulate the layer manager in the current thread.
* @param e The change event.
*/
void activeOrEditLayerChanged(ActiveLayerChangeEvent e);
}
/**
* This event is fired whenever the active or the data layer changes.
* @author Michael Zangl
*/
public static class ActiveLayerChangeEvent extends LayerManagerEvent {
private final OsmDataLayer previousDataLayer;
private final Layer previousActiveLayer;
/**
* Create a new {@link ActiveLayerChangeEvent}
* @param source The source
* @param previousDataLayer the previous data layer
* @param previousActiveLayer the previous active layer
*/
ActiveLayerChangeEvent(MainLayerManager source, OsmDataLayer previousDataLayer,
Layer previousActiveLayer) {
super(source);
this.previousDataLayer = previousDataLayer;
this.previousActiveLayer = previousActiveLayer;
}
/**
* Gets the data layer that was previously used.
* @return The old data layer, null
if there is none.
* @deprecated use {@link #getPreviousDataLayer}
*/
@Deprecated
public OsmDataLayer getPreviousEditLayer() {
return getPreviousDataLayer();
}
/**
* Gets the data layer that was previously used.
* @return The old data layer, null
if there is none.
* @since 13434
*/
public OsmDataLayer getPreviousDataLayer() {
return previousDataLayer;
}
/**
* Gets the active layer that was previously used.
* @return The old active layer, null
if there is none.
*/
public Layer getPreviousActiveLayer() {
return previousActiveLayer;
}
/**
* Gets the data set that was previously used.
* @return The data set of {@link #getPreviousDataLayer()}.
* @deprecated use {@link #getPreviousDataSet}
*/
@Deprecated
public DataSet getPreviousEditDataSet() {
return getPreviousDataSet();
}
/**
* Gets the data set that was previously used.
* @return The data set of {@link #getPreviousDataLayer()}.
* @since 13434
*/
public DataSet getPreviousDataSet() {
if (previousDataLayer != null) {
return previousDataLayer.data;
} else {
return null;
}
}
@Override
public MainLayerManager getSource() {
return (MainLayerManager) super.getSource();
}
}
/**
* This event is fired for {@link LayerAvailabilityListener}
* @author Michael Zangl
* @since 10508
*/
public static class LayerAvailabilityEvent extends LayerManagerEvent {
private final boolean hasLayers;
LayerAvailabilityEvent(LayerManager source, boolean hasLayers) {
super(source);
this.hasLayers = hasLayers;
}
/**
* Checks if this layer manager will have layers afterwards
* @return true if layers will be added.
*/
public boolean hasLayers() {
return hasLayers;
}
}
/**
* A listener that gets informed before any layer is displayed and after all layers are removed.
* @author Michael Zangl
* @since 10508
*/
public interface LayerAvailabilityListener {
/**
* This method is called in the UI thread right before the first layer is added.
* @param e The event.
*/
void beforeFirstLayerAdded(LayerAvailabilityEvent e);
/**
* This method is called in the UI thread after the last layer was removed.
* @param e The event.
*/
void afterLastLayerRemoved(LayerAvailabilityEvent e);
}
/**
* The layer from the layers list that is currently active.
*/
private Layer activeLayer;
/**
* The current active data layer. It might be editable or not, based on its read-only status.
*/
private OsmDataLayer dataLayer;
private final List activeLayerChangeListeners = new CopyOnWriteArrayList<>();
private final List layerAvailabilityListeners = new CopyOnWriteArrayList<>();
/**
* Adds a active/edit layer change listener
*
* @param listener the listener.
*/
public synchronized void addActiveLayerChangeListener(ActiveLayerChangeListener listener) {
if (activeLayerChangeListeners.contains(listener)) {
throw new IllegalArgumentException("Attempted to add listener that was already in list: " + listener);
}
activeLayerChangeListeners.add(listener);
}
/**
* Adds a active/edit layer change listener. Fire a fake active-layer-changed-event right after adding
* the listener. The previous layers will be null. The listener is notified in the current thread.
* @param listener the listener.
*/
public synchronized void addAndFireActiveLayerChangeListener(ActiveLayerChangeListener listener) {
addActiveLayerChangeListener(listener);
listener.activeOrEditLayerChanged(new ActiveLayerChangeEvent(this, null, null));
}
/**
* Removes an active/edit layer change listener.
* @param listener the listener.
*/
public synchronized void removeActiveLayerChangeListener(ActiveLayerChangeListener listener) {
if (!activeLayerChangeListeners.contains(listener)) {
throw new IllegalArgumentException("Attempted to remove listener that was not in list: " + listener);
}
activeLayerChangeListeners.remove(listener);
}
/**
* Add a new {@link LayerAvailabilityListener}.
* @param listener The listener
* @since 10508
*/
public synchronized void addLayerAvailabilityListener(LayerAvailabilityListener listener) {
if (!layerAvailabilityListeners.add(listener)) {
throw new IllegalArgumentException("Attempted to add listener that was already in list: " + listener);
}
}
/**
* Remove an {@link LayerAvailabilityListener}.
* @param listener The listener
* @since 10508
*/
public synchronized void removeLayerAvailabilityListener(LayerAvailabilityListener listener) {
if (!layerAvailabilityListeners.remove(listener)) {
throw new IllegalArgumentException("Attempted to remove listener that was not in list: " + listener);
}
}
/**
* Set the active layer, unless the layer is being uploaded.
* If the layer is an OsmDataLayer, the edit layer is also changed.
* @param layer The active layer.
*/
public void setActiveLayer(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.
if (layer instanceof OsmDataLayer && ((OsmDataLayer) layer).isUploadInProgress()) {
GuiHelper.runInEDT(() ->
JOptionPane.showMessageDialog(
MainApplication.parent,
tr("Trying to set a read only data layer as edit layer"),
tr("Warning"),
JOptionPane.WARNING_MESSAGE));
} else {
GuiHelper.runInEDTAndWaitWithException(() -> realSetActiveLayer(layer));
}
}
protected synchronized void realSetActiveLayer(final Layer layer) {
// to be called in EDT thread
checkContainsLayer(layer);
setActiveLayer(layer, false);
}
private void setActiveLayer(Layer layer, boolean forceEditLayerUpdate) {
ActiveLayerChangeEvent event = new ActiveLayerChangeEvent(this, dataLayer, activeLayer);
activeLayer = layer;
if (activeLayer instanceof OsmDataLayer) {
dataLayer = (OsmDataLayer) activeLayer;
} else if (forceEditLayerUpdate) {
dataLayer = null;
}
fireActiveLayerChange(event);
}
private void fireActiveLayerChange(ActiveLayerChangeEvent event) {
GuiHelper.assertCallFromEdt();
if (event.getPreviousActiveLayer() != activeLayer || event.getPreviousDataLayer() != dataLayer) {
for (ActiveLayerChangeListener l : activeLayerChangeListeners) {
l.activeOrEditLayerChanged(event);
}
}
}
@Override
protected synchronized void realAddLayer(Layer layer, boolean initialZoom) {
if (getLayers().isEmpty()) {
LayerAvailabilityEvent e = new LayerAvailabilityEvent(this, true);
for (LayerAvailabilityListener l : layerAvailabilityListeners) {
l.beforeFirstLayerAdded(e);
}
}
super.realAddLayer(layer, initialZoom);
// update the active layer automatically.
if (layer instanceof OsmDataLayer || activeLayer == null) {
setActiveLayer(layer);
}
}
@Override
protected Collection realRemoveSingleLayer(Layer layer) {
if ((layer instanceof OsmDataLayer) && (((OsmDataLayer) layer).isUploadInProgress())) {
GuiHelper.runInEDT(() -> JOptionPane.showMessageDialog(MainApplication.parent,
tr("Trying to delete the layer with background upload. Please wait until the upload is finished.")));
// Return an empty collection for allowing to delete other layers
return new ArrayList<>();
}
if (layer == activeLayer || layer == dataLayer) {
Layer nextActive = suggestNextActiveLayer(layer);
setActiveLayer(nextActive, true);
}
Collection toDelete = super.realRemoveSingleLayer(layer);
if (getLayers().isEmpty()) {
LayerAvailabilityEvent e = new LayerAvailabilityEvent(this, false);
for (LayerAvailabilityListener l : layerAvailabilityListeners) {
l.afterLastLayerRemoved(e);
}
}
return toDelete;
}
/**
* Determines the next active data layer according to the following
* rules:
*
* - if there is at least one {@link OsmDataLayer} the first one
* becomes active
* - otherwise, the top most layer of any type becomes active
*
*
* @param except A layer to ignore.
* @return the next active data layer
*/
private Layer suggestNextActiveLayer(Layer except) {
List layersList = new ArrayList<>(getLayers());
layersList.remove(except);
// First look for data layer
for (Layer layer : layersList) {
if (layer instanceof OsmDataLayer) {
return layer;
}
}
// Then any layer
if (!layersList.isEmpty())
return layersList.get(0);
// and then give up
return null;
}
/**
* Replies the currently active layer
*
* @return the currently active layer (may be null)
*/
public synchronized Layer getActiveLayer() {
if (activeLayer instanceof OsmDataLayer) {
if (!((OsmDataLayer) activeLayer).isUploadInProgress()) {
return activeLayer;
} else {
return null;
}
} else {
return activeLayer;
}
}
/**
* Replies the current edit layer, if present and not readOnly
*
* @return the current edit layer. May be null.
* @see #getActiveDataLayer
*/
public synchronized OsmDataLayer getEditLayer() {
if (dataLayer != null && !dataLayer.isReadOnly())
return dataLayer;
else
return null;
}
/**
* Replies the active data layer. The layer can be read-only.
*
* @return the current data layer. May be null or read-only.
* @see #getEditLayer
* @since 13434
*/
public synchronized OsmDataLayer getActiveDataLayer() {
if (dataLayer != null)
return dataLayer;
else
return null;
}
/**
* Gets the data set of the active edit layer, if not readOnly.
* @return That data set, null
if there is no edit layer.
* @see #getActiveDataSet
*/
public synchronized DataSet getEditDataSet() {
if (dataLayer != null && !dataLayer.isReadOnly()) {
return dataLayer.data;
} else {
return null;
}
}
/**
* Gets the data set of the active data layer. The dataset can be read-only.
* @return That data set, null
if there is no active data layer.
* @see #getEditDataSet
* @since 13434
*/
public synchronized DataSet getActiveDataSet() {
if (dataLayer != null) {
return dataLayer.data;
} else {
return null;
}
}
/**
* Creates a list of the visible layers in Z-Order, the layer with the lowest Z-Order
* first, layer with the highest Z-Order last.
*
* The active data layer is pulled above all adjacent data layers.
*
* @return a list of the visible in Z-Order, the layer with the lowest Z-Order
* first, layer with the highest Z-Order last.
*/
public synchronized List getVisibleLayersInZOrder() {
List ret = new ArrayList<>();
// This is set while we delay the addition of the active layer.
boolean activeLayerDelayed = false;
List layers = getLayers();
for (ListIterator iterator = layers.listIterator(layers.size()); iterator.hasPrevious();) {
Layer l = iterator.previous();
if (!l.isVisible()) {
// ignored
} else if (l == activeLayer && l instanceof OsmDataLayer) {
// delay and add after the current block of OsmDataLayer
activeLayerDelayed = true;
} else {
if (activeLayerDelayed && !(l instanceof OsmDataLayer)) {
// add active layer before the current one.
ret.add(activeLayer);
activeLayerDelayed = false;
}
// Add this layer now
ret.add(l);
}
}
if (activeLayerDelayed) {
ret.add(activeLayer);
}
return ret;
}
/**
* Invalidates current edit layer, if any. Does nothing if there is no edit layer.
* @since 13150
*/
public void invalidateEditLayer() {
if (dataLayer != null) {
dataLayer.invalidate();
}
}
@Override
protected synchronized void realResetState() {
// Reset state if no asynchronous upload is under progress
if (!AsynchronousUploadPrimitivesTask.getCurrentAsynchronousUploadTask().isPresent()) {
// active and edit layer are unset automatically
super.realResetState();
activeLayerChangeListeners.clear();
layerAvailabilityListeners.clear();
} else {
String msg = tr("A background upload is already in progress. Cannot reset state until the upload is finished.");
Logging.warn(msg);
if (!GraphicsEnvironment.isHeadless()) {
GuiHelper.runInEDT(() -> JOptionPane.showMessageDialog(MainApplication.parent, msg));
}
}
}
/**
* Prepares an OsmDataLayer for upload. The layer to be uploaded is locked and
* if the layer to be uploaded is the current editLayer then editLayer is reset
* to null for disallowing any changes to the layer. An ActiveLayerChangeEvent
* is fired to notify the listeners
*
* @param layer The OsmDataLayer to be uploaded
*/
public synchronized void prepareLayerForUpload(OsmDataLayer layer) {
GuiHelper.assertCallFromEdt();
layer.setUploadInProgress();
layer.setReadOnly();
// Reset only the edit layer as empty
if (dataLayer == layer) {
ActiveLayerChangeEvent activeLayerChangeEvent = new ActiveLayerChangeEvent(this, dataLayer, activeLayer);
dataLayer = null;
fireActiveLayerChange(activeLayerChangeEvent);
}
}
/**
* Post upload processing of the OsmDataLayer.
* If the current edit layer is empty this function sets the layer uploaded as the
* current editLayer. An ActiveLayerChangeEvent is fired to notify the listeners
*
* @param layer The OsmDataLayer uploaded
*/
public synchronized void processLayerAfterUpload(OsmDataLayer layer) {
GuiHelper.assertCallFromEdt();
layer.unsetReadOnly();
layer.unsetUploadInProgress();
// Set the layer as edit layer if the edit layer is empty.
if (dataLayer == null) {
ActiveLayerChangeEvent layerChangeEvent = new ActiveLayerChangeEvent(this, dataLayer, activeLayer);
dataLayer = layer;
fireActiveLayerChange(layerChangeEvent);
}
}
}