/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-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.renderer.crs;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryComponentFilter;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* A {@link ProjectionHandler} for projections that do warp in the East/West direction, it will
* replicate the geometries generating a Google Maps like effect
*
* @author Andrea Aime - OpenGeo
*
*
* @source $URL$
*/
public class WrappingProjectionHandler extends ProjectionHandler {
private int maxWraps;
private boolean datelineWrappingCheckEnabled = true;
/**
* Provides the strategy with the area we want to render and its CRS (the SPI lookup will do
* this step)
* @throws FactoryException
*/
public WrappingProjectionHandler(ReferencedEnvelope renderingEnvelope,
ReferencedEnvelope validArea, CoordinateReferenceSystem sourceCrs, double centralMeridian, int maxWraps) throws FactoryException {
super(sourceCrs, validArea, renderingEnvelope);
this.maxWraps = maxWraps;
// if we are wrapping, we query across the dateline no matter what
this.queryAcrossDateline = true;
// this will compute the radius
setCentralMeridian(centralMeridian);
}
@Override
public Geometry postProcess(MathTransform mt, Geometry geometry) {
// First let's check if the geometry is undoubtedly not going to need
// processing
Envelope env = geometry.getEnvelopeInternal();
final double width;
final double reWidth;
final boolean northEast = CRS.getAxisOrder(targetCRS) == CRS.AxisOrder.NORTH_EAST;
if(northEast) {
width = env.getHeight();
reWidth = renderingEnvelope.getHeight();
} else {
width = env.getWidth();
reWidth = renderingEnvelope.getWidth();
}
if (width < radius && renderingEnvelope.contains(env)
&& reWidth <= radius * 2) {
return geometry;
}
// Check if the geometry has wrapped the dateline. Heuristic: we assume
// anything larger than half of the world might have wrapped it, however,
// if it's touching both datelines then don't wrap it, as it might be something
// like antarctica
if (datelineWrappingCheckEnabled && width > radius && width < radius * 2) {
final Geometry wrapped = (Geometry) geometry.clone();
wrapped.apply(new WrappingCoordinateFilter(radius, radius * 2, mt, northEast));
wrapped.geometryChanged();
// did we un-wrap it?
// if (isUnwrapped(wrapped)) {
geometry = wrapped;
env = geometry.getEnvelopeInternal();
// }
}
// The viewing area might contain the geometry multiple times due to
// wrapping.
// This is obvious for the geometries that wrapped the dateline, but the
// viewing
// area might be large enough to contain the same continent multiple
// times (a-la Google Maps)
List<Geometry> geoms = new ArrayList<Geometry>();
Class geomType = null;
// search the west-most location inside the current rendering envelope
// (there may be many)
double base, curr, lowLimit, highLimit;
if(northEast) {
base = env.getMinY();
curr = env.getMinY();
lowLimit = Math.max(renderingEnvelope.getMinY(), renderingEnvelope.getMedian(1) - maxWraps * radius * 2);
highLimit = Math.min(renderingEnvelope.getMaxY(), renderingEnvelope.getMedian(1) + maxWraps * radius * 2);
} else {
base = env.getMinX();
curr = env.getMinX();
double geometryWidth = geometry.getEnvelopeInternal().getWidth();
lowLimit = Math.max(
renderingEnvelope.getMinX() - geometryWidth,
renderingEnvelope.getMedian(0) - maxWraps * radius * 2);
highLimit = Math.min(renderingEnvelope.getMaxX() + geometryWidth,
renderingEnvelope.getMedian(0) + maxWraps * radius * 2);
}
while (curr > lowLimit) {
curr -= radius * 2;
}
// clone and offset as necessary
geomType = accumulate(geoms, geometry, geomType);
while (curr <= highLimit) {
double offset = curr - base;
if (Math.abs(offset) < radius) {
// in this case we can keep the original geometry, which is already in
} else {
// in all other cases we make a copy and offset it
Geometry offseted = (Geometry) geometry.clone();
offseted.apply(new OffsetOrdinateFilter(northEast ? 1 : 0, offset));
offseted.geometryChanged();
geomType = accumulate(geoms, offseted, geomType);
}
curr += radius * 2;
}
// if we could not find any geom type we stumbled int an empty geom collection
if(geomType == null) {
return null;
}
// if we did not have to actually clone the geometries
if(geoms.size() == 1) {
return geoms.get(0);
}
// rewrap all the clones into a single geometry
if (Point.class.equals(geomType)) {
Point[] points = geoms.toArray(new Point[geoms.size()]);
return geometry.getFactory().createMultiPoint(points);
} else if (LineString.class.isAssignableFrom(geomType)) {
LineString[] lines = geoms.toArray(new LineString[geoms.size()]);
return geometry.getFactory().createMultiLineString(lines);
} else if (Polygon.class.equals(geomType)) {
Polygon[] polys = geoms.toArray(new Polygon[geoms.size()]);
return geometry.getFactory().createMultiPolygon(polys);
} else {
return geometry.getFactory().createGeometryCollection(
geoms.toArray(new Geometry[geoms.size()]));
}
}
private boolean isUnwrapped(final Geometry geometry) {
if (geometry instanceof GeometryCollection) {
final AtomicBoolean unwrapped = new AtomicBoolean(true);
geometry.apply(new GeometryComponentFilter() {
@Override
public void filter(Geometry geom) {
if (geom != geometry && geom.getEnvelopeInternal().getWidth() > radius) {
unwrapped.set(false);
}
}
});
return unwrapped.get();
} else {
return geometry.getEnvelopeInternal().getWidth() <= radius;
}
}
/**
* Adds the geometries into the collection by recursively splitting apart geometry collections,
* so that geoms will contains only simple geometries.
*
* @param geoms
* @param geometry
* @param geomType
*
* @return the geometry type that all geometries added to the collection conform to. Worst case
* it's going to be Geometry.class
*/
private Class accumulate(List<Geometry> geoms, Geometry geometry, Class geomType) {
for (int i = 0; i < geometry.getNumGeometries(); i++) {
Geometry g = geometry.getGeometryN(i);
Class gtype = null;
if (g instanceof GeometryCollection) {
gtype = accumulate(geoms, g, geomType);
} else {
if(renderingEnvelope.intersects(g.getEnvelopeInternal())) {
geoms.add(g);
gtype = g.getClass();
}
}
if (geomType == null) {
geomType = g.getClass();
} else if (!g.getClass().equals(geomType)) {
geomType = Geometry.class;
}
}
return geomType;
}
@Override
public boolean requiresProcessing(Geometry geometry) {
return true;
}
public boolean isDatelineWrappingCheckEnabled() {
return datelineWrappingCheckEnabled;
}
/**
* Enables the check using the "half world" heuristic on the input geometries, if larger it
* assumes they spanned the dateline. Enabled by default
*
* @param datelineWrappingCheckEnabled
*/
public void setDatelineWrappingCheckEnabled(boolean datelineWrappingCheckEnabled) {
this.datelineWrappingCheckEnabled = datelineWrappingCheckEnabled;
}
}