/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2003-2008, 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.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; /** * Store the contents of a map for display primarily as a list of Layers. * <p> * This object is similar to early drafts of the OGC Open Web Service Context specification. * * @author Jody Garnett * * @source $URL: http://svn.osgeo.org/geotools/trunk/modules/library/render/src/main/java/org/geotools/map/MapContent.java $ * http://svn.osgeo.org/geotools/trunk/modules/library/render/src/main/java/org/geotools * /map/MapContext.java $ * @version $Id: Map.java 35310 2010-04-30 10:32:15Z jive $ */ 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 CopyOnWriteArrayList<Layer> 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 seperate viewport; or indeed construct many viewports representing * tiles to be renderered. */ protected MapViewport viewport; /** Listener used to watch individual layers and report changes to MapLayerListListeners */ private MapLayerListener layerListener; public MapContent() { } /** * 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(){ if( this.mapListeners != null ){ this.mapListeners.clear(); this.mapListeners = null; } if( this.layerList != null ){ // remove mapListeners prior to removing layers for( Layer layer : layerList ){ if( layer == null ) continue; if( this.layerListener != null ){ layer.removeMapLayerListener(layerListener); } layer.dispose(); } layerList.clear(); layerList = 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){ for( MapLayer mapLayer : context.getLayers() ){ layers().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) { 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) { if( array != null ){ for (MapLayer mapLayer : array) { if( mapLayer == null ){ continue; } Layer layer = mapLayer.toLayer(); layers().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) { if (mapListeners == null) { mapListeners = new CopyOnWriteArrayList<MapLayerListListener>(); } boolean added = mapListeners.addIfAbsent(listener); if (added && mapListeners.size() == 1) { listenToMapLayers(true); } } /** * 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 synchronized void listenToMapLayers(boolean listen) { if( mapListeners == null || mapListeners.isEmpty()){ return; // not worth listening nobody is interested } if (layerListener == null) { layerListener = new MapLayerListener() { public void layerShown(MapLayerEvent event) { Layer layer = (Layer) event.getSource(); int index = layers().indexOf(layer); fireLayerEvent(layer, index, event); } public void layerSelected(MapLayerEvent event) { Layer layer = (Layer) event.getSource(); int index = layers().indexOf(layer); fireLayerEvent(layer, index, event); } public void layerHidden(MapLayerEvent event) { Layer layer = (Layer) event.getSource(); int index = layers().indexOf(layer); fireLayerEvent(layer, index, event); } public void layerDeselected(MapLayerEvent event) { Layer layer = (Layer) event.getSource(); int index = layers().indexOf(layer); fireLayerEvent(layer, index, event); } public void layerChanged(MapLayerEvent event) { Layer layer = (Layer) event.getSource(); int index = layers().indexOf(layer); fireLayerEvent(layer, index, event); } }; } if (listen) { for (Layer layer : layers()) { layer.addMapLayerListener(layerListener); } } else { for (Layer layer : layers()) { layer.removeMapLayerListener(layerListener); } } } /** * Remove interest in receiving {@link LayerListEvent}. * * @param listener * The object to stop sending <code>LayerListEvent</code>s. */ public void removeMapLayerListListener(MapLayerListListener listener) { if (mapListeners != null) { mapListeners.remove(listener); } } /** * 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) { return layers().addIfAbsent(layer); } /** * Remove a layer, if present, and trigger a {@link LayerListEvent}. * * @param layer * a MapLayer that will be added. * * @return true if the layer has been removed */ public boolean removeLayer(Layer layer) { return layers().remove(layer); } /** * Moves a layer from a position to another. Will fire a MapLayerListEvent * * @param sourcePosition * the layer current position * @param destPosition * the layer new position */ public void moveLayer(int sourcePosition, int destPosition) { List<Layer> layers = layers(); Layer destLayer = layers.get(destPosition); Layer sourceLayer = layers.get(sourcePosition); layers.set(destPosition, sourceLayer); layers.set(sourcePosition, destLayer); } /** * Direct access to the layer list; contents arranged by zorder. * <p> * Please note this list is live and modifications made to the list will trigger * {@link LayerListEvent} * * @return Direct access to layers */ public synchronized CopyOnWriteArrayList<Layer> layers() { if (layerList == null) { layerList = new CopyOnWriteArrayList<Layer>() { private static final long serialVersionUID = 8011733882551971475L; public void add(int index, Layer element) { super.add(index, element); if( layerListener != null ){ element.addMapLayerListener(layerListener); } fireLayerAdded(element, index, index); } @Override public boolean add(Layer element) { boolean added = super.add(element); if (added) { if( layerListener != null ){ element.addMapLayerListener(layerListener); } fireLayerAdded(element, size() - 1, size() - 1); } return added; } public boolean addAll(Collection<? extends Layer> c) { int start = size() - 1; boolean added = super.addAll(c); if( layerListener != null ){ for( Layer element : c ){ element.addMapLayerListener(layerListener); } } fireLayerAdded(null, start, size() - 1); return added; } public boolean addAll(int index, Collection<? extends Layer> c) { boolean added = super.addAll(index, c); if( layerListener != null ){ for( Layer element : c ){ element.addMapLayerListener(layerListener); } } fireLayerAdded(null, index, size() - 1); return added; } @Override public int addAllAbsent(Collection<? extends Layer> c) { int start = size() - 1; int added = super.addAllAbsent(c); if( layerListener != null ){ // taking the risk that layer is correctly impelmented and will // not have layerListener not mapped for( Layer element : c ){ element.addMapLayerListener(layerListener); } } fireLayerAdded(null, start, size() - 1); return added; } @Override public boolean addIfAbsent(Layer element) { boolean added = super.addIfAbsent(element); if (added) { if (layerListener != null) { element.addMapLayerListener(layerListener); } fireLayerAdded(element, size() - 1, size() - 1); } return added; } @Override public void clear() { for( Layer element : this ){ if( layerListener != null ){ element.removeMapLayerListener( layerListener ); } element.dispose(); } super.clear(); fireLayerRemoved(null, -1, -1); } @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; } @Override public boolean remove(Object o) { boolean removed = super.remove(o); if (removed) { fireLayerRemoved((Layer) o, -1, -1); if( o instanceof Layer ){ Layer element = (Layer) o; if( layerListener != null ){ element.removeMapLayerListener( layerListener ); } element.dispose(); } } return removed; } @Override public boolean removeAll(Collection<?> c) { for( Object obj : c ){ if( !contains(obj) ){ continue; } if( obj instanceof Layer ){ Layer element = (Layer) obj; if( layerListener != null ){ element.removeMapLayerListener( layerListener ); } element.dispose(); } } boolean removed = super.removeAll(c); fireLayerRemoved(null, 0, size() - 1); return removed; } @Override public boolean retainAll(Collection<?> c) { for( Object obj : c ){ if( contains(obj) ){ continue; } if( obj instanceof Layer ){ Layer element = (Layer) obj; if( layerListener != null ){ element.removeMapLayerListener( layerListener ); } element.dispose(); } } boolean removed = super.retainAll(c); fireLayerRemoved(null, 0, size() - 1); return removed; } }; } return layerList; } protected void fireLayerAdded(Layer element, int fromIndex, int toIndex) { 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); } } } } protected void fireLayerRemoved(Layer element, int fromIndex, int toIndex) { 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); } } } } protected void fireLayerEvent(Layer element, int index, MapLayerEvent layerEvent) { if (mapListeners == null) { return; } MapLayerListEvent mapEvent = new MapLayerListEvent(this, element, index, layerEvent); for (MapLayerListListener mapLayerListListener : mapListeners) { try { mapLayerListListener.layerChanged(mapEvent); } catch (Throwable t) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.logp(Level.FINE, mapLayerListListener.getClass().getName(), "layerAdded", t.getLocalizedMessage(), t); } } } } /** * 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() { CoordinateReferenceSystem mapCrs = null; if (viewport != null) { mapCrs = viewport.getCoordianteReferenceSystem(); } ReferencedEnvelope maxBounds = null; for (Layer layer : layers()) { 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.warning("Unable to determine bounds of " + layer + ":" + eek); } } if (maxBounds == null && mapCrs != null) { maxBounds = new ReferencedEnvelope(mapCrs); } return maxBounds; } // // 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 synchronized MapViewport getViewport() { if (viewport == null) { viewport = new MapViewport( getMaxBounds() ); } return viewport; } /** * 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) { getViewport().addMapBoundsListener(listener); } /** * Remove interest in receiving a {@link BoundingBoxEvent}s. * * @param listener * The object to stop sending change events. */ public void removeMapBoundsListener(MapBoundsListener listener) { getViewport().removeMapBoundsListener(listener); } /** * 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() { return getViewport().getBounds(); } /** * 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() { return getViewport().getCoordianteReferenceSystem(); } /** * Set the <code>CoordinateReferenceSystem</code> for this map's internal viewport. * * @param crs * @throws FactoryException * @throws TransformException */ void setCoordinateReferenceSystem(CoordinateReferenceSystem crs) { getViewport().setCoordinateReferenceSystem(crs); } // // Properties // /** * Registers PropertyChangeListener to receive events. * * @param listener * The listener to register. */ public void addPropertyChangeListener(java.beans.PropertyChangeListener listener) { if (propertyListeners == null) { propertyListeners = new CopyOnWriteArrayList<java.beans.PropertyChangeListener>(); } if (!propertyListeners.contains(listener)) { propertyListeners.add(listener); } } /** * Removes PropertyChangeListener from the list of listeners. * * @param listener * The listener to remove. */ public void removePropertyChangeListener(java.beans.PropertyChangeListener listener) { if (propertyListeners != null) { propertyListeners.remove(listener); } } /** * 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 synchronized java.util.Map<String, Object> getUserData() { if (userData == null) { userData = new HashMap<String, Object>() { private static final long serialVersionUID = 8011733882551971475L; public Object put(String key, Object value) { Object old = super.put(key, value); fireProperty(key, old, value); return old; } 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; } /** * 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() { return title; } /** * Set the title of this context. * * @param title * the title. */ public void setTitle(String title) { String old = this.title; this.title = title; fireProperty("title", old, title); } protected void fireProperty(String propertyName, Object old, Object value) { 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); } } } } }