/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.web.crs;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Transaction;
import org.geotools.data.memory.MemoryDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.GeoTools;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.DefaultFeatureCollections;
import org.geotools.feature.FeatureCollections;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.map.DefaultMapContext;
import org.geotools.map.DefaultMapLayer;
import org.geotools.map.FeatureLayer;
import org.geotools.map.MapContext;
import org.geotools.map.Layer;
import org.geotools.referencing.CRS;
import org.geotools.renderer.GTRenderer;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.styling.SLDParser;
import org.geotools.styling.Style;
import org.geotools.styling.StyleFactory;
import org.geotools.styling.StyledLayerDescriptor;
import org.geotools.styling.UserLayer;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Polygon;
/**
* Helper class to dynamically create a graphic representation of the area of validity for a
* {@link CoordinateReferenceSystem coordinate reference system}.
*
* @author Gabriel Roldan
* @version $Id$
*/
class CRSAreaOfValidityMapBuilder {
private static final int DEFAULT_MAP_WIDTH = 400;
private static final int DEFAULT_MAP_HEIGHT = 200;
private static final GeometryFactory gf = new GeometryFactory();
private final int mapWidth;
private final int mapHeight;
private static Map<String, Style> STYLES = new WeakHashMap<String, Style>();
private static WeakReference<DataStore> LATLON = null;
public CRSAreaOfValidityMapBuilder() {
this(DEFAULT_MAP_WIDTH, DEFAULT_MAP_HEIGHT);
}
public CRSAreaOfValidityMapBuilder(int mapWidth, int mapHeight) {
this.mapWidth = mapWidth;
this.mapHeight = mapHeight;
}
@SuppressWarnings("unchecked")
private SimpleFeatureSource getFeatureSource(final URL shpfile)
throws IOException {
Map params = new HashMap<String, String>();
params.put(ShapefileDataStoreFactory.CREATE_SPATIAL_INDEX.key, "false");
params.put(ShapefileDataStoreFactory.URLP.key, shpfile);
DataStore ds = DataStoreFinder.getDataStore(params);
return ds.getFeatureSource(ds.getTypeNames()[0]);
}
private Geometry getGeographicBoundingBox(CoordinateReferenceSystem crs) {
GeographicBoundingBox envelope = CRS.getGeographicBoundingBox(crs);
if (envelope == null) {
return null;
}
final double westBoundLongitude = envelope.getWestBoundLongitude();
final double eastBoundLongitude = envelope.getEastBoundLongitude();
final double southBoundLatitude = envelope.getSouthBoundLatitude();
final double northBoundLatitude = envelope.getNorthBoundLatitude();
final int numSteps = 80;
Geometry geogBoundingGeom;
if (westBoundLongitude < eastBoundLongitude) {
geogBoundingGeom = createBoundingPolygon(westBoundLongitude, eastBoundLongitude,
southBoundLatitude, northBoundLatitude, numSteps);
} else {
// the geographic bounds cross the day line (lon -180/180), trick it into two adjacent
// polygons
Polygon eastPolygon = createBoundingPolygon(-180, eastBoundLongitude,
southBoundLatitude, northBoundLatitude, numSteps);
Polygon westPolygon = createBoundingPolygon(westBoundLongitude, 180,
southBoundLatitude, northBoundLatitude, numSteps);
geogBoundingGeom = gf.createMultiPolygon(new Polygon[] { eastPolygon, westPolygon });
}
return geogBoundingGeom;
}
private Polygon createBoundingPolygon(final double westBoundLongitude,
final double eastBoundLongitude, final double southBoundLatitude,
final double northBoundLatitude, final int numSteps) {
// build a densified LinearRing so it does reproject better
final double dx = (eastBoundLongitude - westBoundLongitude) / numSteps;
final double dy = (northBoundLatitude - southBoundLatitude) / numSteps;
List<Coordinate> coords = new ArrayList<Coordinate>(4 * numSteps + 1);
double x = westBoundLongitude;
for (int i = 0; i < numSteps; i++) {
coords.add(new Coordinate(x, southBoundLatitude));
x += dx;
}
double y = southBoundLatitude;
for (int i = 0; i < numSteps; i++) {
coords.add(new Coordinate(eastBoundLongitude, y));
y += dy;
}
x = eastBoundLongitude;
for (int i = 0; i < numSteps; i++) {
coords.add(new Coordinate(x, northBoundLatitude));
x -= dx;
}
y = northBoundLatitude;
for (int i = 0; i < numSteps; i++) {
coords.add(new Coordinate(westBoundLongitude, y));
y -= dy;
}
coords.add(new Coordinate(westBoundLongitude, southBoundLatitude));
Coordinate[] coordinates = coords.toArray(new Coordinate[coords.size()]);
LinearRing shell = gf.createLinearRing(coordinates);
Polygon polygon = gf.createPolygon(shell, null);
return polygon;
}
private Style getStyle(final String styleName) {
Style style = STYLES.get(styleName);
if (style == null) {
StyleFactory styleFactory = CommonFactoryFinder.getStyleFactory(GeoTools
.getDefaultHints());
SLDParser parser = new SLDParser(styleFactory);
try {
parser.setInput(getClass().getResource(styleName));
} catch (IOException e) {
throw new RuntimeException(e);
}
StyledLayerDescriptor sld = parser.parseSLD();
UserLayer layer = (UserLayer) sld.getStyledLayers()[0];
style = layer.getUserStyles()[0];
STYLES.put(styleName, style);
}
return style;
}
public RenderedImage createMapFor(CoordinateReferenceSystem crs,
com.vividsolutions.jts.geom.Envelope areaOfInterest) throws IOException {
BufferedImage image = new BufferedImage(mapWidth, mapHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
createMapFor(crs, areaOfInterest, graphics);
graphics.dispose();
return image;
}
@SuppressWarnings("unchecked")
public void createMapFor(final CoordinateReferenceSystem crs,
final com.vividsolutions.jts.geom.Envelope areaOfInterest, final Graphics2D graphics)
throws IOException {
Geometry geographicBoundingBox = getGeographicBoundingBox(crs);
MapContext mapContent = getMapContext(crs, geographicBoundingBox, areaOfInterest);
graphics.setColor(new Color(153, 179, 204));
graphics.fillRect(0, 0, mapWidth, mapHeight);
Rectangle paintArea = new Rectangle(mapWidth, mapHeight);
mapContent.setAreaOfInterest(areaOfInterest, crs);
GTRenderer renderer = new StreamingRenderer();
renderer.setContext(mapContent);
RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
renderer.setJava2DHints(hints);
Map renderingHints = new HashMap();
renderingHints.put("optimizedDataLoadingEnabled", Boolean.TRUE);
renderingHints.put(StreamingRenderer.ADVANCED_PROJECTION_HANDLING_KEY, Boolean.TRUE);
renderingHints.put(StreamingRenderer.CONTINUOUS_MAP_WRAPPING, Boolean.TRUE);
renderer.setRendererHints(renderingHints);
renderer.paint(graphics, paintArea, areaOfInterest);
mapContent.dispose();
}
private SimpleFeature createCrsBoundsFeature(Geometry geom, CoordinateReferenceSystem crs) {
SimpleFeatureType featureType;
SimpleFeatureTypeBuilder sftb = new SimpleFeatureTypeBuilder();
sftb.setName("Bounds");
try {
sftb.add("the_geom", Geometry.class, CRS.decode("EPSG:4326", true));
} catch (Exception e) {
throw new RuntimeException(e);
}
featureType = sftb.buildFeatureType();
SimpleFeature feature = SimpleFeatureBuilder.template(featureType, null);
feature.setAttribute("the_geom", geom);
return feature;
}
private Layer createCrsLayer(Geometry geom, CoordinateReferenceSystem crs) {
DefaultFeatureCollection collection = new DefaultFeatureCollection();
collection.add(createCrsBoundsFeature(geom, crs));
Style style = getStyle("crs.sld");
FeatureLayer ml = new FeatureLayer(collection, style);
return ml;
}
private MapContext getMapContext(CoordinateReferenceSystem crs, Geometry geographicBoundingBox,
com.vividsolutions.jts.geom.Envelope areaOfInterest) throws IOException {
DefaultMapContext mapContent = new DefaultMapContext();
Style style;
URL shpfile;
SimpleFeatureSource source;
shpfile = getClass().getResource("TM_WORLD_BORDERS.shp");
source = getFeatureSource(shpfile);
style = getStyle("TM_WORLD_BORDERS.sld");
mapContent.addLayer(new DefaultMapLayer(source, style));
source = getLatLonFeatureSource();
style = getStyle("latlon.sld");
mapContent.addLayer(new DefaultMapLayer(source, style));
shpfile = getClass().getResource("cities.shp");
source = getFeatureSource(shpfile);
style = getStyle("cities.sld");
mapContent.addLayer(new DefaultMapLayer(source, style));
Layer layer = createCrsLayer(geographicBoundingBox, crs);
mapContent.addLayer(layer);
return mapContent;
}
private SimpleFeatureSource getLatLonFeatureSource() {
try {
DataStore ds = LATLON == null ? null : LATLON.get();
if (ds == null) {
ds = createLatLonDataStore();
LATLON = new WeakReference<DataStore>(ds);
}
return ds.getFeatureSource("latlon");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private DataStore createLatLonDataStore() throws Exception {
SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
tb.setName("latlon");
tb.add("the_geom", LineString.class, CRS.decode("EPSG:4326", true));
tb.add("level", Integer.class);
SimpleFeatureType type = tb.buildFeatureType();
MemoryDataStore ds = new MemoryDataStore();
ds.createSchema(type);
FeatureWriter<SimpleFeatureType, SimpleFeature> writer;
writer = ds.getFeatureWriterAppend("latlon", Transaction.AUTO_COMMIT);
for (int lon = -180; lon < 180; lon += 5) {
for (int lat = -90; lat < 90; lat += 5) {
LineString geom;
int level;
geom = gf.createLineString(new Coordinate[] { new Coordinate(lon, lat),
new Coordinate(lon, lat + 5) });
level = 1;
if (lon % 10 == 0) {
level = 10;
}
if (lon % 30 == 0) {
level = 30;
}
SimpleFeature f;
f = writer.next();
f.setAttribute(0, geom);
f.setAttribute(1, Integer.valueOf(level));
writer.write();
geom = gf.createLineString(new Coordinate[] { new Coordinate(lon, lat),
new Coordinate(lon + 5, lat) });
level = 1;
if (lat % 10 == 0) {
level = 10;
}
if (lat % 30 == 0) {
level = 30;
}
f = writer.next();
f.setAttribute(0, geom);
f.setAttribute(1, Integer.valueOf(level));
writer.write();
}
}
writer.close();
return ds;
}
}