/* * 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.awt.geom.AffineTransform; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.logging.Level; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader; import org.geotools.data.FeatureSource; import org.geotools.feature.FeatureCollection; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.event.MapBoundsEvent; import org.geotools.map.event.MapBoundsListener; import org.geotools.map.event.MapLayerListListener; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.styling.Style; import org.opengis.coverage.grid.GridCoverage; import org.opengis.feature.type.FeatureType; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Envelope; /** * Extension of MapContent to cover the requirements of the OGC Map Context specifications. *
* The following OGC specifications (or working drafts) are the inspiration for this class: *
* This method is used to prevent duplication in classes supporting deprecated getMapContext() * methods. * * @param content MapContent to be represented */ public MapContext( MapContent content ){ this(content.getCoordinateReferenceSystem()); for (Layer layer : content.layers()) { addLayer(layer); } this.setAreaOfInterest( content.getViewport().getBounds()); } /** * Creates a default empty map context * * @param crs * the coordindate reference system to be used with this context (may be null and set * later) */ public MapContext(final CoordinateReferenceSystem crs) { this(null, null, null, null, null, crs); } /** * Creates a map context with the provided layers. *
* Note, the coordinate reference system for the context will be set from that of the first * layer with an available CRS. * * @param layers * an array of MapLayer objects (may be empty or null) to be added to this context */ public MapContext(MapLayer[] layers) { this(layers, DefaultGeographicCRS.WGS84); } /** * Creates a map context with the provided layers and coordinate reference system * * @param layers * an array of MapLayer objects (may be empty or null) to be added to this context * * @param crs * the coordindate reference system to be used with this context (may be null and set * later) */ public MapContext(MapLayer[] layers, final CoordinateReferenceSystem crs) { this(layers, null, null, null, null, crs); } /** * Creates a map context *
* Note, the coordinate reference system for the context will be set from that of the first
* layer with an available CRS.
*
* @param layers
* an array of MapLayer objects (may be empty or null) to be added to this context
*
* @param title
* a title for this context (e.g. might be used by client-code that is displaying the
* context's layers); may be null or an empty string
*
* @param contextAbstract
* a short description of the context and its contents; may be null or an empty
* string
*
* @param contactInformation
* can be used, for example, to record the creators or custodians of the data that
* are, or will be, held by this context; may be null or an empty string
*
* @param keywords
* an optional array of key words pertaining to the data that are, or will be, held
* by this context; may be null or a zero-length String array
*
*/
public MapContext(MapLayer[] layers, String title, String contextAbstract,
String contactInformation, String[] keywords) {
this(layers, title, contextAbstract, contactInformation, keywords, null);
}
/**
* Creates a new map context
*
* @param layers
* an array of MapLayer objects (may be empty or null) to be added to this context
*
* @param title
* a title for this context (e.g. might be used by client-code that is displaying the
* context's layers); may be null or an empty string
*
* @param contextAbstract
* a short description of the context and its contents; may be null or an empty
* string
*
* @param contactInformation
* can be used, for example, to record the creators or custodians of the data that
* are, or will be, held by this context; may be null or an empty string
*
* @param keywords
* an optional array of key words pertaining to the data that are, or will be, held
* by this context; may be null or a zero-length String array
*
* @param crs
* the coordindate reference system to be used with this context (may be null and set
* later)
*/
public MapContext(MapLayer[] layers, String title, String contextAbstract,
String contactInformation, String[] keywords, final CoordinateReferenceSystem crs) {
super(layers, title, contextAbstract, contactInformation, keywords, crs);
}
/**
* Add a new layer if not already present and trigger a {@link LayerListEvent}.
*
* @param layer
* the layer to be inserted
*
* @return true if the layer has been added, false otherwise
*/
public boolean addLayer(MapLayer mapLayer) {
layers().add(mapLayer.toLayer());
return true;
}
/**
* Add a new layer in the specified position and trigger a {@link LayerListEvent}. Layer won't
* be added if it's already in the list.
*
* @param index
* index at which the layer will be inserted
* @param layer
* the layer to be inserted
*
* @return true if the layer has been added, false otherwise
*/
public boolean addLayer(int index, MapLayer mapLayer) {
Layer layer = mapLayer.toLayer();
layers().add(index, layer);
return true;
}
/**
* Add a new layer and trigger a {@link LayerListEvent}.
*
* @param featureSource
* a SimpleFeatureSource with the new layer that will be added.
*/
public void addLayer(FeatureSource featureSource, Style style) {
addLayer(new DefaultMapLayer(featureSource, style, ""));
}
/**
* Add a new layer and trigger a {@link LayerListEvent}.
*
* @param collection
* a SimpleFeatureCollection with the new layer that will be added.
*/
public void addLayer(FeatureCollection featureCollection, Style style) {
Layer layer = new FeatureLayer(featureCollection, style);
this.layers().add(layer);
}
/**
* Add a new layer and trigger a {@link LayerListEvent}.
*
* @param collection
* Collection with the new layer that will be added.
*/
public void addLayer(Collection collection, Style style) {
if (collection instanceof FeatureCollection) {
FeatureCollection featureCollection = (FeatureCollection) collection;
addLayer(featureCollection, style);
} else {
throw new IllegalArgumentException("FeatureCollection required");
}
}
/**
* Add a new layer and trigger a {@link LayerListEvent}
*
* @param gridCoverage
* a GridCoverage with the new layer that will be added.
*
*/
public void addLayer(GridCoverage gridCoverage, Style style) {
if (style == null) {
throw new IllegalArgumentException("style cannot be null");
}
if (gridCoverage instanceof GridCoverage2D) {
Layer layer = new GridCoverageLayer((GridCoverage2D) gridCoverage, style);
layers().add(layer);
} else {
throw new UnsupportedOperationException("GridCoverage2D required");
}
}
/**
* Add a new layer and trigger a {@link LayerListEvent}
*
* @param gridCoverage
* an AbstractGridCoverage2DReader with the new layer that will be added.
*
*/
public void addLayer(AbstractGridCoverage2DReader reader, Style style) {
if (style == null) {
throw new IllegalArgumentException("style cannot be null");
}
Layer layer = new GridReaderLayer(reader, style);
layers().add(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(MapLayer layer) {
int index = indexOf(layer);
if (index == -1) {
return false;
} else {
removeLayer(index);
return true;
}
}
/**
* Remove a layer and trigger a {@link LayerListEvent}.
*
* @param index
* The index of the layer that it's going to be removed
*
* @return the layer removed, if any
*/
public MapLayer removeLayer(int index) {
Layer removed = layers().remove(index);
return new DefaultMapLayer(removed);
}
/**
* Add an array of new layers and trigger a {@link LayerListEvent}.
*
* @param layers
* The new layers that are to be added.
*
* @return the number of layers actually added to the MapContext
*/
public int addLayers(MapLayer[] array) {
if ((array == null) || (array.length == 0)) {
return 0;
}
return super.addLayers(toLayerList(array));
}
/**
* Remove an array of layers and trigger a {@link LayerListEvent}.
*
* @param layers
* The layers that are to be removed.
*/
public void removeLayers(MapLayer[] array) {
if ((array == null) || (array.length == 0) || layers().isEmpty()) {
return;
}
List
* This implementation is more forgiving then getMaxBounds() as it is willing to consider the
* bounds of layers that are incomplete and not record a coordinate reference 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 getLayerBounds() throws IOException {
// return getMaxBounds();
// Jody: I don't like the following implementation because it will allow a mapLayer
// with null CRS to produce a bounds that is not transformed into the mapCRS
// (I would prefer to skip these layers as getMaxBounds() does however some test cases
// depend on this feature)
ReferencedEnvelope maxBounds = null;
CoordinateReferenceSystem mapCRS = viewport != null ? viewport.getCoordinateReferenceSystem() : null;
for (Layer layer : layers()) {
if (layer == null) {
continue; // skip empty entry
}
ReferencedEnvelope dataBounds = layer.getBounds();
if (dataBounds == null) {
continue;
} else {
try {
CoordinateReferenceSystem dataCrs = dataBounds.getCoordinateReferenceSystem();
if ((dataCrs != null) && mapCRS != null
&& !CRS.equalsIgnoreMetadata(dataCrs, mapCRS)) {
dataBounds = dataBounds.transform(mapCRS, true);
}
if (dataCrs == null && mapCRS != null) {
LOGGER.log(Level.SEVERE,
"It was not possible to get a projected bounds estimate");
}
} catch (FactoryException e) {
LOGGER
.log(
Level.SEVERE,
"Data source and map context coordinate system differ, yet it was not possible to get a projected bounds estimate...",
e);
continue;
} catch (TransformException e) {
LOGGER
.log(
Level.SEVERE,
"Data source and map context coordinate system differ, yet it was not possible to get a projected bounds estimate...",
e);
continue;
}
if (maxBounds == null) {
maxBounds = dataBounds;
} else {
maxBounds.expandToInclude(dataBounds);
}
if (mapCRS == null) {
mapCRS = dataBounds.getCoordinateReferenceSystem();
}
}
}
return maxBounds;
}
/**
* Register interest in receiving a {@link LayerListEvent}. A LayerListEvent
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){
super.addMapLayerListListener(listener);
}
/**
* Remove interest in receiving {@link LayerListEvent}.
*
* @param listener
* The object to stop sending LayerListEvent
s.
*/
public void removeMapLayerListListener(MapLayerListListener listener){
super.removeMapLayerListListener(listener);
}
/**
* Set the area of interest. This triggers a MapBoundsEvent to be published.
*
* @param areaOfInterest
* the new area of interest
* @param coordinateReferenceSystem
* the CRS for the new area of interest
*
* @throws IllegalArgumentException
* if either argument is {@code null}
*/
public void setAreaOfInterest(Envelope areaOfInterest, CoordinateReferenceSystem crs)
throws IllegalArgumentException {
getViewport().setBounds(new ReferencedEnvelope(areaOfInterest, crs));
}
/**
* Set the area of interest. This triggers a MapBoundsEvent to be published.
*
* @param bounds
* the new area of interest
*
* @throws IllegalArgumentException
* if the provided areaOfInterest is {@code null} or does not have a coordinate
* reference system
*/
public void setAreaOfInterest(ReferencedEnvelope bounds) throws IllegalArgumentException {
if (bounds == null) {
throw new NullPointerException("bounds must not be null");
}
getViewport().setBounds(bounds);
}
/**
* Gets the current area of interest provided by {@link #getViewport()#getBounds()}.
*
* If the viewport has not been created, it will be filled in by default
* based on the layer bounds provided by {@link #getMaxBounds()}.
*
* @return Current area of interest
*/
public ReferencedEnvelope getAreaOfInterest() {
return getViewport().getBounds();
}
/**
* Get the current coordinate system.
*
* @return the coordinate system of this box.
*/
public CoordinateReferenceSystem getCoordinateReferenceSystem(){
return super.getCoordinateReferenceSystem();
}
/**
* Transform the current area of interest for this context using the provided
* transform. This may be useful for zooming and panning processes. Does nothing
* if no area of interest is set.
*
* @param transform
* The transform to change map viewport
*/
public void transform(AffineTransform transform) {
ReferencedEnvelope oldBounds = getAreaOfInterest();
if (!(oldBounds == null || oldBounds.isEmpty())) {
double[] coords = new double[4];
coords[0] = oldBounds.getMinX();
coords[1] = oldBounds.getMinY();
coords[2] = oldBounds.getMaxX();
coords[3] = oldBounds.getMaxY();
transform.transform(coords, 0, coords, 0, 2);
ReferencedEnvelope newBounds = new ReferencedEnvelope(
coords[0], coords[2], coords[1], coords[3],
oldBounds.getCoordinateReferenceSystem());
setAreaOfInterest(newBounds);
}
}
/**
* 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){
super.addMapBoundsListener(listener);
}
/**
* Remove interest in receiving a {@link BoundingBoxEvent}s.
*
* @param listener
* The object to stop sending change events.
*/
public void removeMapBoundsListener(MapBoundsListener listener){
super.removeMapBoundsListener(listener);
}
/**
* Get the abstract which describes this interface, returns an empty string if this has not been
* set yet.
*
* @return The Abstract or an empty string if not present
*/
public String getAbstract(){
String description = (String) getUserData().get("abstract");
return description == null ? "" : description;
}
/**
* Set an abstract which describes this context.
*
* @param conAbstract
* the Abstract.
*/
public void setAbstract(final String contextAbstract){
getUserData().put("abstract", contextAbstract);
}
/**
* Get the contact information associated with this context, returns an empty string if
* contactInformation has not been set.
*
* @return the ContactInformation or an empty string if not present
*/
public String getContactInformation(){
String contact = (String) getUserData().get("contact");
return contact == null ? "" : contact;
}
/**
* Set contact information associated with this class.
*
* @param contactInformation
* the ContactInformation.
*/
public void setContactInformation(final String contactInformation){
getUserData().put("contact", contactInformation);
}
/**
* Set or change the coordinate reference system for this context. This will trigger a
* MapBoundsEvent to be published to listeners.
*
* @param crs
* @throws FactoryException
* @throws TransformException
*/
public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs) {
getViewport().setCoordinateReferenceSystem(crs);
}
/**
* Get an array of keywords associated with this context, returns an empty array if no keywords
* have been set. The array returned is a copy, changes to the returned array won't influence
* the MapContextState
*
* @return array of keywords
*/
public String[] getKeywords(){
Object obj = getUserData().get("keywords");
if (obj == null) {
return new String[0];
} else if (obj instanceof String) {
String keywords = (String) obj;
return keywords.split(",");
} else if (obj instanceof String[]) {
String keywords[] = (String[]) obj;
String[] copy = new String[keywords.length];
System.arraycopy(keywords, 0, copy, 0, keywords.length);
return copy;
} else if (obj instanceof Collection) {
Collection