/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2010-2011, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.map;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.event.MapBoundsEvent;
import org.geotools.map.event.MapBoundsListener;
import org.geotools.map.event.MapLayerEvent;
import org.geotools.map.event.MapLayerListEvent;
import org.geotools.map.event.MapLayerListListener;
import org.geotools.map.event.MapLayerListener;
import org.geotools.referencing.CRS;
import org.geotools.util.logging.Logging;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
/**
* Stores the contents of a map for display, including a list of layers, a
* {@linkplain MapViewport} defining the device and world bounds of display
* area, and optional user data.
* <p>
*
* Methods are provided to add, remove and reorder layers. Alternatively, the
* list of layers can be accessed directly with the {@linkplain #layers()}.
* For example:
* <pre><code>
* mapContent.layers().add( newLayer );
* </code></pre>
*
* Operations on the list returned by the {@code layers{}} method are guaranteed to
* be thread safe, and modifying the list contents will result in {@code MapLayerListEvents}
* being published.
* <p>
*
* Note: This object is similar to early drafts of the OGC Open Web Service Context
* specification.
*
* @author Jody Garnett
* @since 2.7
* @source $URL$
* @version $Id$
*/
public class MapContent {
/** The logger for the map module. */
static protected final Logger LOGGER = Logging.getLogger("org.geotools.map");
/** List of Layers to be rendered */
private final LayerList layerList;
/** MapLayerListListeners to be notified in the event of change */
private CopyOnWriteArrayList<MapLayerListListener> mapListeners;
/** Map used to hold application specific information */
private HashMap<String, Object> userData;
/** Map title */
private String title;
/** PropertyListener list used for notifications */
private CopyOnWriteArrayList<PropertyChangeListener> propertyListeners;
/**
* Viewport for map rendering.
*
* While the map maintains one viewport internally to better reflect a map
* context document you are free to maintain a separate viewport; or indeed
* construct many viewports representing tiles to be rendered.
*/
protected MapViewport viewport;
private MapViewport defaultViewport;
/** Listener used to watch individual layers and report changes to MapLayerListListeners */
private MapLayerListener layerListener;
private final ReadWriteLock monitor;
/**
* Creates a new map content.
*/
public MapContent() {
layerList = new LayerList();
monitor = new ReentrantReadWriteLock();
}
/**
* Checks that dispose has been called; producing a warning if needed.
*/
@Override
protected void finalize() throws Throwable {
if( this.layerList != null){
if( !this.layerList.isEmpty()){
LOGGER.severe("Call MapContent dispose() to prevent memory leaks");
}
}
super.finalize();
}
/**
* Clean up any listeners or cached state associated with this MapContent.
* <p>
* Please note that open connections (FeatureSources and GridCoverage readers) are
* the responsibility of your application and are not cleaned up by this method.
*/
public void dispose() {
for (Layer layer : layerList) {
if (layer != null) {
// Layer.dispose will inform listeners of the impending
// disposal, then remove listeners from this layer
layer.dispose();
}
}
layerList.clear();
if (this.mapListeners != null) {
this.mapListeners.clear();
this.mapListeners = null;
}
if (this.layerListener != null) {
this.layerListener = null;
}
if (this.propertyListeners != null) {
this.propertyListeners.clear();
this.propertyListeners = null;
}
this.title = null;
if (this.userData != null) {
// remove property listeners prior to removing userData
this.userData.clear();
this.userData = null;
}
}
public MapContent(MapContext context) {
this();
for (MapLayer mapLayer : context.getLayers()) {
layerList.add(mapLayer.toLayer());
}
if (context.getTitle() != null) {
setTitle(context.getTitle());
}
if (context.getAbstract() != null) {
getUserData().put("abstract", context.getAbstract());
}
if (context.getContactInformation() != null) {
getUserData().put("contact", context.getContactInformation());
}
if (context.getKeywords() != null) {
getUserData().put("keywords", context.getKeywords());
}
if (context.getAreaOfInterest() != null) {
getViewport().setBounds(context.getAreaOfInterest());
}
}
@Deprecated
public MapContent(CoordinateReferenceSystem crs) {
this();
getViewport().setCoordinateReferenceSystem(crs);
}
@Deprecated
public MapContent(MapLayer[] array) {
this(array, null);
}
@Deprecated
public MapContent(MapLayer[] array, CoordinateReferenceSystem crs) {
this(array, "Untitled", "", "", null, crs);
}
@Deprecated
public MapContent(MapLayer[] array, String title, String contextAbstract, String contactInformation,
String[] keywords) {
this(array, title, contextAbstract, contactInformation, keywords, null);
}
@Deprecated
public MapContent(MapLayer[] array, String title, String contextAbstract, String contactInformation,
String[] keywords, final CoordinateReferenceSystem crs) {
this();
if (array != null) {
for (MapLayer mapLayer : array) {
if (mapLayer == null) {
continue;
}
Layer layer = mapLayer.toLayer();
layerList.add(layer);
}
}
if (title != null) {
setTitle(title);
}
if (contextAbstract != null) {
getUserData().put("abstract", contextAbstract);
}
if (contactInformation != null) {
getUserData().put("contact", contactInformation);
}
if (keywords != null) {
getUserData().put("keywords", keywords);
}
if (crs != null) {
getViewport().setCoordinateReferenceSystem(crs);
}
}
/**
* Register interest in receiving a {@link LayerListEvent}. A <code>LayerListEvent</code> is
* sent if a layer is added or removed, but not if the data within a layer changes.
*
* @param listener
* The object to notify when Layers have changed.
*/
public void addMapLayerListListener(MapLayerListListener listener) {
monitor.writeLock().lock();
try {
if (mapListeners == null) {
mapListeners = new CopyOnWriteArrayList<MapLayerListListener>();
}
boolean added = mapListeners.addIfAbsent(listener);
if (added && mapListeners.size() == 1) {
listenToMapLayers(true);
}
} finally {
monitor.writeLock().unlock();
}
}
/**
* Listen to the map layers; passing any events on to our own mapListListeners.
* <p>
* This method only has an effect if we have any actuall mapListListeners.
*
* @param listen
* True to connect to all the layers and listen to events
*/
protected void listenToMapLayers(boolean listen) {
monitor.writeLock().lock();
try {
if (mapListeners == null || mapListeners.isEmpty()) {
return; // not worth listening nobody is interested
}
if (layerListener == null) {
layerListener = new MapLayerListener() {
@Override
public void layerShown(MapLayerEvent event) {
Layer layer = (Layer) event.getSource();
int index = layerList.indexOf(layer);
fireLayerEvent(layer, index, event);
}
@Override
public void layerSelected(MapLayerEvent event) {
Layer layer = (Layer) event.getSource();
int index = layerList.indexOf(layer);
fireLayerEvent(layer, index, event);
}
@Override
public void layerHidden(MapLayerEvent event) {
Layer layer = (Layer) event.getSource();
int index = layerList.indexOf(layer);
fireLayerEvent(layer, index, event);
}
@Override
public void layerDeselected(MapLayerEvent event) {
Layer layer = (Layer) event.getSource();
int index = layerList.indexOf(layer);
fireLayerEvent(layer, index, event);
}
@Override
public void layerChanged(MapLayerEvent event) {
Layer layer = (Layer) event.getSource();
int index = layerList.indexOf(layer);
fireLayerEvent(layer, index, event);
}
@Override
public void layerPreDispose(MapLayerEvent event) {
Layer layer = (Layer) event.getSource();
int index = layerList.indexOf(layer);
fireLayerEvent(layer, index, event);
}
};
}
if (listen) {
for (Layer layer : layerList) {
layer.addMapLayerListener(layerListener);
}
} else {
for (Layer layer : layerList) {
layer.removeMapLayerListener(layerListener);
}
}
} finally {
monitor.writeLock().unlock();
}
}
/**
* Remove interest in receiving {@link LayerListEvent}.
*
* @param listener
* The object to stop sending <code>LayerListEvent</code>s.
*/
public void removeMapLayerListListener(MapLayerListListener listener) {
monitor.writeLock().lock();
try {
if (mapListeners != null) {
mapListeners.remove(listener);
}
} finally {
monitor.writeLock().unlock();
}
}
/**
* Add a new layer (if not already present).
* <p>
* In an interactive setting this will trigger a {@link LayerListEvent}
*
* @param layer
* @return true if the layer was added
*/
public boolean addLayer(Layer layer) {
monitor.writeLock().lock();
try {
return layerList.addIfAbsent(layer);
} finally {
monitor.writeLock().unlock();
}
}
/**
* Adds all layers from the input collection that are not already present
* in this map content.
*
* @param layers layers to add (may be {@code null} or empty)
* @return the number of layers added
*/
public int addLayers(Collection<? extends Layer> layers) {
monitor.writeLock().lock();
try {
if (layers == null || layers.isEmpty()) {
return 0;
}
return layerList.addAllAbsent(layers);
} finally {
monitor.writeLock().unlock();
}
}
/**
* Removes the given layer, if present, and publishes a {@linkplain MapLayerListEvent}.
*
* @param layer the layer to be removed
*
* @return {@code true} if the layer was removed
*/
public boolean removeLayer(Layer layer) {
monitor.writeLock().lock();
try {
return layerList.remove(layer);
} finally {
monitor.writeLock().unlock();
}
}
/**
* Moves a layer in the layer list. Will fire a MapLayerListEvent.
*
* @param sourcePosition existing position of the layer
* @param destPosition new position of the layer
*/
public void moveLayer(int sourcePosition, int destPosition) {
monitor.writeLock().lock();
try {
layerList.move(sourcePosition, destPosition);
} finally {
monitor.writeLock().unlock();
}
}
/**
* Gets the list of layers for this map content. The returned list has the following
* characteristics:
* <ul>
* <li>
* It is "live", ie. changes to its contents will be reflected in this map content.
* </li>
* <li>
* It is thread-safe. Accessing list elements directly or via a
* {@linkplain java.util.ListIterator} returns a snapshot view of the list
* contents (as per Java's {@linkplain CopyOnWriteArrayList} class).
* </li>
* <li>
* Adding a layer to the list, or removing a layer from it, results in
* a {@linkplain MapLayerListEvent} being published by the map content.
* </li>
* </ul>
*
* For these reasons, you should always work directly with the list returned by
* this method and avoid making copies since they will not have the above behaviour.
*
* @return a "live" reference to the layer list for this map content
*/
public List<Layer> layers() {
monitor.readLock().lock();
try {
return layerList;
} finally {
monitor.readLock().unlock();
}
}
protected void fireLayerAdded(Layer element, int fromIndex, int toIndex) {
monitor.readLock().lock();
try {
if (mapListeners == null) {
return;
}
MapLayerListEvent event = new MapLayerListEvent(this, element, fromIndex, toIndex);
for (MapLayerListListener mapLayerListListener : mapListeners) {
try {
mapLayerListListener.layerAdded(event);
} catch (Throwable t) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.logp(Level.FINE, mapLayerListListener.getClass().getName(),
"layerAdded", t.getLocalizedMessage(), t);
}
}
}
} finally {
monitor.readLock().unlock();
}
}
protected void fireLayerRemoved(Layer element, int fromIndex, int toIndex) {
monitor.readLock().lock();
try {
if (mapListeners == null) {
return;
}
MapLayerListEvent event = new MapLayerListEvent(this, element, fromIndex, toIndex);
for (MapLayerListListener mapLayerListListener : mapListeners) {
try {
mapLayerListListener.layerRemoved(event);
} catch (Throwable t) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.logp(Level.FINE, mapLayerListListener.getClass().getName(),
"layerAdded", t.getLocalizedMessage(), t);
}
}
}
} finally {
monitor.readLock().unlock();
}
}
protected void fireLayerMoved(Layer element, int toIndex) {
monitor.readLock().lock();
try {
if (mapListeners == null) {
return;
}
MapLayerListEvent event = new MapLayerListEvent(this, element, toIndex);
for (MapLayerListListener mapLayerListListener : mapListeners) {
try {
mapLayerListListener.layerMoved(event);
} catch (Throwable t) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.logp(Level.FINE, mapLayerListListener.getClass().getName(),
"layerMoved", t.getLocalizedMessage(), t);
}
}
}
} finally {
monitor.readLock().unlock();
}
}
protected void fireLayerPreDispose(Layer element, int toIndex) {
monitor.readLock().lock();
try {
if (mapListeners == null) {
return;
}
MapLayerListEvent event = new MapLayerListEvent(this, element, toIndex);
for (MapLayerListListener mapLayerListListener : mapListeners) {
try {
mapLayerListListener.layerPreDispose(event);
} catch (Throwable t) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.logp(Level.FINE, mapLayerListListener.getClass().getName(),
"layerMoved", t.getLocalizedMessage(), t);
}
}
}
} finally {
monitor.readLock().unlock();
}
}
protected void fireLayerEvent(Layer element, int index, MapLayerEvent layerEvent) {
monitor.readLock().lock();
try {
if (mapListeners == null) {
return;
}
MapLayerListEvent mapEvent = new MapLayerListEvent(this, element, index, layerEvent);
for (MapLayerListListener mapLayerListListener : mapListeners) {
try {
switch (layerEvent.getReason()) {
case MapLayerEvent.PRE_DISPOSE:
mapLayerListListener.layerPreDispose(mapEvent);
break;
default:
mapLayerListListener.layerChanged(mapEvent);
}
} catch (Throwable t) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.logp(Level.FINE, mapLayerListListener.getClass().getName(),
"layerAdded", t.getLocalizedMessage(), t);
}
}
}
} finally {
monitor.readLock().unlock();
}
}
/**
* Get the bounding box of all the layers in this Map. If all the layers cannot determine the
* bounding box in the speed required for each layer, then null is returned. The bounds will be
* expressed in the Map coordinate system.
*
* @return The bounding box of the features or null if unknown and too expensive for the method
* to calculate.
*
* @throws IOException
* if an IOException occurs while accessing the FeatureSource bounds
*/
public ReferencedEnvelope getMaxBounds() {
monitor.readLock().lock();
try {
CoordinateReferenceSystem mapCrs = null;
if (viewport != null) {
mapCrs = viewport.getCoordinateReferenceSystem();
}
ReferencedEnvelope maxBounds = null;
for (Layer layer : layerList) {
if (layer == null) {
continue;
}
try {
ReferencedEnvelope layerBounds = layer.getBounds();
if (layerBounds == null || layerBounds.isEmpty() || layerBounds.isNull()) {
continue;
}
if (mapCrs == null) {
// crs for the map is not defined; let us start with the first CRS we see then!
maxBounds = new ReferencedEnvelope(layerBounds);
mapCrs = layerBounds.getCoordinateReferenceSystem();
continue;
}
ReferencedEnvelope normalized;
if (CRS.equalsIgnoreMetadata(mapCrs, layerBounds.getCoordinateReferenceSystem())) {
normalized = layerBounds;
} else {
try {
normalized = layerBounds.transform(mapCrs, true);
} catch (Exception e) {
LOGGER.log(Level.FINE, "Unable to transform: {0}", e);
continue;
}
}
if (maxBounds == null) {
maxBounds = normalized;
} else {
maxBounds.expandToInclude(normalized);
}
} catch (Throwable eek) {
LOGGER.log(Level.WARNING, "Unable to determine bounds of " + layer, eek);
}
}
if (maxBounds == null) {
maxBounds = new ReferencedEnvelope(mapCrs);
}
return maxBounds;
} finally {
monitor.readLock().unlock();
}
}
//
// Viewport Information
//
/**
* Viewport describing the area visible on screen.
* <p>
* Applications may create multiple viewports (perhaps to render tiles of content); the viewport
* recorded here is intended for interactive applications where it is helpful to have a single
* viewport representing what the user is seeing on screen.
* <p>
* With that in mind; if the user has not already supplied a viewport one will be created:
* <ul>
* <li>The viewport will be configured to show the extent of the current layers as provided by
* {@link #getMaxBounds()}.</li>
* <li>The viewport will have an empty {@link MapViewport#getBounds()} if no layers have been
* added yet.</li>
* </ul>
* @return MapViewport describing how to draw this map
*/
public MapViewport getViewport() {
monitor.readLock().lock();
try {
if (viewport == null) {
viewport = new MapViewport(getMaxBounds());
}
return viewport;
} finally {
monitor.readLock().unlock();
}
}
/**
* Sets the viewport for this map content and returns the previously
* set one (which may be {@code null}). The {@code viewport} argument may
* be {@code null}, in which case a subsequent to {@linkplain #getViewport()}
* will return a new instance with default settings.
*
* @param viewport the new viewport
*/
public void setViewport(MapViewport viewport) {
monitor.writeLock().lock();
try {
this.viewport = viewport;
} finally {
monitor.writeLock().unlock();
}
}
/**
* Register interest in receiving {@link MapBoundsEvent}s.
*
* @param listener
* The object to notify when the area of interest has changed.
*/
public void addMapBoundsListener(MapBoundsListener listener) {
monitor.writeLock().lock();
try {
getViewport().addMapBoundsListener(listener);
} finally {
monitor.writeLock().unlock();
}
}
/**
* Remove interest in receiving a {@link BoundingBoxEvent}s.
*
* @param listener
* The object to stop sending change events.
*/
public void removeMapBoundsListener(MapBoundsListener listener) {
monitor.writeLock().lock();
try {
getViewport().removeMapBoundsListener(listener);
} finally {
monitor.writeLock().unlock();
}
}
/**
* The extent of the map currently (sometimes called the map "viewport".
* <p>
* Note Well: The bounds should match your screen aspect ratio (or the map will appear
* squashed). Please note this only covers spatial extent; you may wish to use the user data map
* to record the current viewport time or elevation.
*/
ReferencedEnvelope getBounds() {
monitor.readLock().lock();
try {
return getViewport().getBounds();
} finally {
monitor.readLock().unlock();
}
}
/**
* The coordinate reference system used for rendering the map.
* <p>
* The coordinate reference system used for rendering is often considered to be the "world"
* coordinate reference system; this is distinct from the coordinate reference system used for
* each layer (which is often data dependent).
* </p>
*
* @return coordinate reference system used for rendering the map.
*/
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
monitor.readLock().lock();
try {
return getViewport().getCoordinateReferenceSystem();
} finally {
monitor.readLock().unlock();
}
}
/**
* Set the <code>CoordinateReferenceSystem</code> for this map's internal viewport.
*
* @param crs
* @throws FactoryException
* @throws TransformException
*/
void setCoordinateReferenceSystem(CoordinateReferenceSystem crs) {
monitor.writeLock().lock();
try {
getViewport().setCoordinateReferenceSystem(crs);
} finally {
monitor.writeLock().unlock();
}
}
//
// Properties
//
/**
* Registers PropertyChangeListener to receive events.
*
* @param listener
* The listener to register.
*/
public void addPropertyChangeListener(java.beans.PropertyChangeListener listener) {
monitor.writeLock().lock();
try {
if (propertyListeners == null) {
propertyListeners = new CopyOnWriteArrayList<java.beans.PropertyChangeListener>();
}
if (!propertyListeners.contains(listener)) {
propertyListeners.add(listener);
}
} finally {
monitor.writeLock().unlock();
}
}
/**
* Removes PropertyChangeListener from the list of listeners.
*
* @param listener
* The listener to remove.
*/
public void removePropertyChangeListener(java.beans.PropertyChangeListener listener) {
monitor.writeLock().lock();
try {
if (propertyListeners != null) {
propertyListeners.remove(listener);
}
} finally {
monitor.writeLock().unlock();
}
}
/**
* As an example it can be used to record contact details, map abstract, keywords and so forth
* for an OGC "Open Web Service Context" document.
* <p>
* Modifications to the userData will result in a propertyChange event.
* </p>
*
* @return
*/
public java.util.Map<String, Object> getUserData() {
monitor.writeLock().lock();
try {
if (userData == null) {
userData = new HashMap<String, Object>() {
private static final long serialVersionUID = 8011733882551971475L;
@Override
public Object put(String key, Object value) {
Object old = super.put(key, value);
fireProperty(key, old, value);
return old;
}
@Override
public Object remove(Object key) {
Object old = super.remove(key);
fireProperty((String) key, old, null);
return old;
}
@Override
public void putAll(java.util.Map<? extends String, ? extends Object> m) {
super.putAll(m);
fireProperty("userData", null, null);
}
@Override
public void clear() {
super.clear();
fireProperty("userData", null, null);
}
};
}
return this.userData;
} finally {
monitor.writeLock().unlock();
}
}
/**
* Get the title, returns an empty string if it has not been set yet.
*
* @return the title, or an empty string if it has not been set.
*/
public String getTitle() {
monitor.readLock().lock();
try {
return title;
} finally {
monitor.readLock().unlock();
}
}
/**
* Set the title of this context.
*
* @param title
* the title.
*/
public void setTitle(String title) {
monitor.writeLock().lock();
try {
String old = this.title;
this.title = title;
fireProperty("title", old, title);
} finally {
monitor.writeLock().unlock();
}
}
protected void fireProperty(String propertyName, Object old, Object value) {
monitor.readLock().lock();
try {
if (propertyListeners == null) {
return;
}
PropertyChangeEvent event = new PropertyChangeEvent(this, "propertyName", old, value);
for (PropertyChangeListener propertyChangeListener : propertyListeners) {
try {
propertyChangeListener.propertyChange(event);
} catch (Throwable t) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.logp(Level.FINE, propertyChangeListener.getClass().getName(),
"propertyChange", t.getLocalizedMessage(), t);
}
}
}
} finally {
monitor.readLock().unlock();
}
}
/**
* Sets the CRS of the viewport, if one exists, based on the first Layer
* with a non-null CRS. This is called when a new Layer is added to the
* Layer list. Does nothing if the viewport already has a CRS set or if
* it has been set as non-editable.
*/
private void checkViewportCRS() {
if (viewport != null
&& getCoordinateReferenceSystem() == null
&& viewport.isEditable()) {
for (Layer layer : layerList) {
ReferencedEnvelope bounds = layer.getBounds();
if (bounds != null) {
CoordinateReferenceSystem crs = bounds.getCoordinateReferenceSystem();
if (crs != null) {
viewport.setCoordinateReferenceSystem(crs);
return;
}
}
}
}
}
private class LayerList extends CopyOnWriteArrayList<Layer> {
private static final long serialVersionUID = 8011733882551971475L;
/**
* Adds a layer at the specified position in this list. Does
* nothing if the layer is already present.
*
* @param index position for the layer
* @param element the layer to add
*/
@Override
public void add(int index, Layer element) {
if (!contains(element)) {
super.add(index, element);
if (layerListener != null) {
element.addMapLayerListener(layerListener);
}
checkViewportCRS();
fireLayerAdded(element, index, index);
}
}
/**
* Adds a layer if it is not already present. Equivalent to
* {@linkplain #addIfAbsent(Layer)}.
*
* @param element the layer to add
* @return {@code true} if the layer was added; {@code false} if
* it was already present in this list
*/
@Override
public boolean add(Layer element) {
return addIfAbsent(element);
}
/**
* Adds all layers from the input collection that are not already
* present in this list. Equivalent to {@code addAllAbsent(layers) > 0}.
*
* @param layers candidate layers to add
* @return {@code true} is any layers were added; {@code false} otherwise
*/
@Override
public boolean addAll(Collection<? extends Layer> layers) {
return addAllAbsent(layers) > 0;
}
/**
* Adds all layers from the input collection that are not already
* present in this list, with the first added layer taking position
* {@code index}.
*
* @param index position of the first added layer in this list
* @param layers candidate layers to add
*
* @return {@code true} if any layers were added; {@code false} otherwise
*/
@Override
public boolean addAll(int index, Collection<? extends Layer> layers) {
boolean added = false;
int pos = index;
for (Layer layer : layers) {
if (!contains(layer)) {
add(pos, layer);
if (layerListener != null) {
layer.addMapLayerListener(layerListener);
}
added = true;
pos++ ;
}
}
if (added) {
checkViewportCRS();
fireLayerAdded(null, index, size() - 1);
}
return added;
}
/**
* Adds all layers from the input collection that are not already
* present in this list.
*
* @param layers candidate layers to add
* @return the number of layers added
*/
@Override
public int addAllAbsent(Collection<? extends Layer> layers) {
int start = size();
int added = super.addAllAbsent(layers);
if (added > 0) {
if (layerListener != null) {
for (int i = start; i < size(); i++) {
get(i).addMapLayerListener(layerListener);
}
}
checkViewportCRS();
fireLayerAdded(null, start, size() - 1);
}
return added;
}
/**
* Adds a layer if it is not already present.
*
* @param element the layer to add
* @return {@code true} if the layer was added; {@code false} if
* it was already present in this list
*/
@Override
public boolean addIfAbsent(Layer element) {
boolean added = super.addIfAbsent(element);
if (added) {
if (layerListener != null) {
element.addMapLayerListener(layerListener);
}
checkViewportCRS();
fireLayerAdded(element, size() - 1, size() - 1);
}
return added;
}
/**
* Removes all layers from this list and calls their {@code dispose} methods.
*/
@Override
public void clear() {
for (Layer element : this) {
if (layerListener != null) {
element.removeMapLayerListener(layerListener);
}
element.dispose();
}
super.clear();
fireLayerRemoved(null, -1, -1);
}
/**
* Removes the layer at position {@code index} from this list.
* Note: removing a layer causes its {@code dispose} method to be called, so
* although a reference to the removed layer is returned by this method it
* should not be used subsequently.
*
* @param index the position of the layer to be removed
* @return the layer that was removed (will have been disposed)
*/
@Override
public Layer remove(int index) {
Layer removed = super.remove(index);
fireLayerRemoved(removed, index, index);
if (layerListener != null) {
removed.removeMapLayerListener(layerListener);
}
removed.dispose();
return removed;
}
/**
* Removes the specified element, which much be a Layer, from this list
* if present. This method calls the layer's {@code dispose} method, so any
* external references to the layer should be discarded.
*
* @param element the element to remove
* @return {@code true} if removed; {@code false} if not present in this list
*/
@Override
public boolean remove(Object element) {
boolean removed = super.remove(element);
if (removed) {
fireLayerRemoved((Layer) element, -1, -1);
if (element instanceof Layer) {
Layer layer = (Layer) element;
if (layerListener != null) {
layer.removeMapLayerListener(layerListener);
}
layer.dispose();
}
}
return removed;
}
/**
* Removes all layers in the input collection from this list, if present.
*
* @param layers the candidate layers to remove
* @return {@code true} if any layers were removed; {@code false} otherwise
*/
@Override
public boolean removeAll(Collection<?> layers) {
for (Object obj : layers) {
Layer element = (Layer) obj;
if (!contains(element)) {
continue;
}
if (layerListener != null) {
element.removeMapLayerListener(layerListener);
}
element.dispose();
}
boolean removed = super.removeAll(layers);
fireLayerRemoved(null, 0, size() - 1);
return removed;
}
/**
* Removes any layers from this list that are not contained in the
* input collection.
*
* @param layers the layers which should not be removed
* @return {@code true} if any layers were removed; {@code false} otherwise
*/
@Override
public boolean retainAll(Collection<?> layers) {
for (Layer element : this) {
if (!layers.contains(element)) {
if (layerListener != null) {
element.removeMapLayerListener(layerListener);
}
element.dispose();
}
}
boolean removed = super.retainAll(layers);
if (removed) {
fireLayerRemoved(null, 0, size() - 1);
}
return removed;
}
/**
* Replaces the layer at the given position with another. Equivalent to:
* <pre><code>
* remove(index);
* add(index, element);
* </code></pre>
* The same events will be sent to {@link MapLayerListListener} objects as
* if the above code had been called.
*
* @param index position of the layer to be replaced
* @param element the new layer
* @return the layer that was replaced
*/
@Override
public Layer set(int index, Layer element) {
/*
* Note: rather than calling the superclass set method here
* we call remove followed by add to ensure correct event
* and listener handling.
*/
Layer removed = remove(index);
add(index, element);
checkViewportCRS();
return removed;
}
/**
* Moves a layer in this list.
*
* @param sourcePosition existing position of the layer
* @param destPosition new position of the layer
*/
private void move(int sourcePosition, int destPosition) {
Layer layer = super.remove(sourcePosition);
super.add(destPosition, layer);
fireLayerMoved(layer, destPosition);
}
}
}