/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-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.geometry.jts;
import java.awt.Shape;
import java.awt.geom.IllegalPathStateException;
import java.awt.geom.PathIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.geotools.geometry.Envelope2D;
import org.geotools.geometry.GeneralDirectPosition;
import org.geotools.referencing.CRS;
import org.geotools.referencing.GeodeticCalculator;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.operation.TransformPathNotFoundException;
import org.geotools.referencing.operation.projection.PointOutsideEnvelopeException;
import org.geotools.resources.Classes;
import org.geotools.resources.geometry.ShapeUtilities;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.opengis.geometry.BoundingBox;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
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.MultiLineString;
import com.vividsolutions.jts.geom.Polygon;
/**
* JTS Geometry utility methods, bringing Geotools to JTS.
* <p>
* Offers geotools based services such as reprojection.
* <p>
* Responsibilities:
* <ul>
* <li>transformation</li>
* <li>coordinate sequence editing</li>
* <li>common coordinate sequence implementations for specific uses</li>
* </ul>
*
* @since 2.2
* @source $URL$
* @version $Id$
* @author Jody Garnett
* @author Martin Desruisseaux
* @author Simone Giannecchini, GeoSolutions.
*/
public final class JTS {
/**
* A pool of direct positions for use in {@link #orthodromicDistance}.
*/
private static final GeneralDirectPosition[] POSITIONS = new GeneralDirectPosition[4];
static {
for (int i = 0; i < POSITIONS.length; i++) {
POSITIONS[i] = new GeneralDirectPosition(i);
}
}
/**
* Geodetic calculators already created for a given coordinate reference system.
* For use in {@link #orthodromicDistance}.
*
* Note: We would like to use {@link org.geotools.util.CanonicalSet}, but we can't because
* {@link GeodeticCalculator} keep a reference to the CRS which is used as the key.
*/
private static final Map<CoordinateReferenceSystem,GeodeticCalculator> CALCULATORS = new HashMap<CoordinateReferenceSystem,GeodeticCalculator>();
/**
* Do not allow instantiation of this class.
*/
private JTS() {
}
/**
* Makes sure that an argument is non-null.
*
* @param name Argument name.
* @param object User argument.
* @throws IllegalArgumentException if {@code object} is null.
*/
private static void ensureNonNull(final String name, final Object object)
throws IllegalArgumentException {
if (object == null) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, name));
}
}
/**
* Transforms the envelope using the specified math transform.
* Note that this method can not handle the case where the envelope contains the North or
* South pole, or when it cross the ±180� longitude, because {@linkplain MathTransform
* math transforms} do not carry suffisient informations. For a more robust envelope
* transformation, use {@link ReferencedEnvelope#transform(CoordinateReferenceSystem,
* boolean)} instead.
*
* @param envelope The envelope to transform.
* @param transform The transform to use.
* @return The transformed Envelope
* @throws TransformException if at least one coordinate can't be transformed.
*/
public static Envelope transform(final Envelope envelope, final MathTransform transform)
throws TransformException {
return transform(envelope, null, transform, 5);
}
/**
* Transforms the densified envelope using the specified math transform.
* The envelope is densified (extra points put around the outside edge)
* to provide a better new envelope for high deformed situations.
* <p>
* If an optional target envelope is provided, this envelope will be
* {@linkplain Envelope#expandToInclude expanded} with the transformation result. It will
* <strong>not</strong> be {@linkplain Envelope#setToNull nullified} before the expansion.
* <p>
* Note that this method can not handle the case where the envelope contains the North or
* South pole, or when it cross the ±180� longitude, because {@linkplain MathTransform
* math transforms} do not carry suffisient informations. For a more robust envelope
* transformation, use {@link ReferencedEnvelope#transform(CoordinateReferenceSystem,
* boolean, int)} instead.
*
* @param sourceEnvelope The envelope to transform.
* @param targetEnvelope An envelope to expand with the transformation result, or {@code null}
* for returning an new envelope.
* @param transform The transform to use.
* @param npoints Densification of each side of the rectange.
* @return {@code targetEnvelope} if it was non-null, or a new envelope otherwise.
* In all case, the returned envelope fully contains the transformed envelope.
* @throws TransformException if a coordinate can't be transformed.
*/
public static Envelope transform(final Envelope sourceEnvelope, Envelope targetEnvelope,
final MathTransform transform, int npoints) throws TransformException {
ensureNonNull("sourceEnvelope", sourceEnvelope);
ensureNonNull("transform", transform);
if ((transform.getSourceDimensions() != 2) || (transform.getTargetDimensions() != 2)) {
throw new MismatchedDimensionException(Errors.format(ErrorKeys.BAD_TRANSFORM_$1,
Classes.getShortClassName(transform)));
}
npoints++; // for the starting point.
final double[] coordinates = new double[(4 * npoints) * 2];
final double xmin = sourceEnvelope.getMinX();
final double xmax = sourceEnvelope.getMaxX();
final double ymin = sourceEnvelope.getMinY();
final double ymax = sourceEnvelope.getMaxY();
final double scaleX = (xmax - xmin) / npoints;
final double scaleY = (ymax - ymin) / npoints;
int offset = 0;
for (int t = 0; t < npoints; t++) {
final double dx = scaleX * t;
final double dy = scaleY * t;
coordinates[offset++] = xmin; // Left side, increasing toward top.
coordinates[offset++] = ymin + dy;
coordinates[offset++] = xmin + dx; // Top side, increasing toward right.
coordinates[offset++] = ymax;
coordinates[offset++] = xmax; // Right side, increasing toward bottom.
coordinates[offset++] = ymax - dy;
coordinates[offset++] = xmax - dx; // Bottom side, increasing toward left.
coordinates[offset++] = ymin;
}
assert offset == coordinates.length;
xform(transform, coordinates, coordinates);
// Now find the min/max of the result
if (targetEnvelope == null) {
targetEnvelope = new Envelope();
}
for (int t = 0; t < offset;) {
targetEnvelope.expandToInclude(coordinates[t++], coordinates[t++]);
}
return targetEnvelope;
}
/**
* Transforms the geometry using the default transformer.
*
* @param geom The geom to transform
* @param transform the transform to use during the transformation.
* @return the transformed geometry. It will be a new geometry.
* @throws MismatchedDimensionException if the geometry doesn't have the expected dimension
* for the specified transform.
* @throws TransformException if a point can't be transformed.
*/
public static Geometry transform(final Geometry geom, final MathTransform transform)
throws MismatchedDimensionException, TransformException {
final GeometryCoordinateSequenceTransformer transformer = new GeometryCoordinateSequenceTransformer();
transformer.setMathTransform(transform);
return transformer.transform(geom);
}
/**
* Transforms the coordinate using the provided math transform.
*
* @param source the source coordinate that will be transformed
* @param dest the coordinate that will be set. May be null or the source coordinate
* (or new coordinate of course).
* @return the destination coordinate if not null or a new Coordinate.
* @throws TransformException if the coordinate can't be transformed.
*/
public static Coordinate transform(final Coordinate source, Coordinate dest,
final MathTransform transform) throws TransformException {
ensureNonNull("source", source);
ensureNonNull("transform", transform);
if (dest == null) {
dest = new Coordinate();
}
final double[] array = new double[transform.getSourceDimensions()];
copy(source, array);
transform.transform(array, 0, array, 0, 1);
switch (transform.getTargetDimensions()) {
case 3:
dest.z = array[2]; // Fall through
case 2:
dest.y = array[1]; // Fall through
case 1:
dest.x = array[0]; // Fall through
case 0:
break;
}
return dest;
}
/**
* Transforms the envelope from its current crs to WGS84 coordinate reference system.
* If the specified envelope is already in WGS84, then it is returned unchanged.
*
* @param envelope The envelope to transform.
* @param crs The CRS the envelope is currently in.
* @return The envelope transformed to be in WGS84 CRS.
* @throws TransformException If at least one coordinate can't be transformed.
*/
public static Envelope toGeographic(final Envelope envelope, final CoordinateReferenceSystem crs)
throws TransformException {
if (CRS.equalsIgnoreMetadata(crs, DefaultGeographicCRS.WGS84)) {
return envelope;
}
final MathTransform transform;
try {
transform = CRS.findMathTransform(crs, DefaultGeographicCRS.WGS84, true);
} catch (FactoryException exception) {
throw new TransformPathNotFoundException(Errors.format(
ErrorKeys.CANT_TRANSFORM_ENVELOPE, exception));
}
return transform(envelope, transform);
}
/**
* Like a transform but eXtreme!
*
* Transforms an array of coordinates using the provided math transform.
* Each coordinate is transformed separately. In case of a transform exception then
* the new value of the coordinate is the last coordinate correctly transformed.
*
* @param transform The math transform to apply.
* @param src The source coordinates.
* @param dest The destination array for transformed coordinates.
* @throws TransformException if this method failed to transform any of the points.
*/
public static void xform(final MathTransform transform, final double[] src, final double[] dest)
throws TransformException {
ensureNonNull("transform", transform);
final int sourceDim = transform.getSourceDimensions();
final int targetDim = transform.getTargetDimensions();
if (targetDim != sourceDim) {
throw new MismatchedDimensionException();
}
TransformException firstError = null;
boolean startPointTransformed = false;
for (int i = 0; i < src.length; i += sourceDim) {
try {
transform.transform(src, i, dest, i, 1);
if (!startPointTransformed) {
startPointTransformed = true;
for (int j = 0; j < i; j++) {
System.arraycopy(dest, j, dest, i, targetDim);
}
}
} catch (TransformException e) {
if (firstError == null) {
firstError = e;
}
if (startPointTransformed) {
System.arraycopy(dest, i - targetDim, dest, i, targetDim);
}
}
}
if (!startPointTransformed && (firstError != null)) {
throw firstError;
}
}
/**
* Computes the orthodromic distance between two points. This method:
* <p>
* <ol>
* <li>Transforms both points to geographic coordinates
* (<var>latitude</var>,<var>longitude</var>).</li>
* <li>Computes the orthodromic distance between the two points using ellipsoidal
* calculations.</li>
* </ol>
* <p>
* The real work is performed by {@link GeodeticCalculator}. This convenience method simply
* manages a pool of pre-defined geodetic calculators for the given coordinate reference system
* in order to avoid repetitive object creation. If a large amount of orthodromic distances
* need to be computed, direct use of {@link GeodeticCalculator} provides better performance
* than this convenience method.
*
* @param p1 First point
* @param p2 Second point
* @param crs Reference system the two points are in.
* @return Orthodromic distance between the two points, in meters.
* @throws TransformException if the coordinates can't be transformed from the specified
* CRS to a {@linkplain org.opengis.referencing.crs.GeographicCRS geographic CRS}.
*/
public static synchronized double orthodromicDistance(final Coordinate p1, final Coordinate p2,
final CoordinateReferenceSystem crs) throws TransformException {
ensureNonNull("p1", p1);
ensureNonNull("p2", p2);
ensureNonNull("crs", crs);
/*
* Need to synchronize because we use a single instance of a Map (CALCULATORS) as well as
* shared instances of GeodeticCalculator and GeneralDirectPosition (POSITIONS). None of
* them are thread-safe.
*/
GeodeticCalculator gc = (GeodeticCalculator) CALCULATORS.get(crs);
if (gc == null) {
gc = new GeodeticCalculator(crs);
CALCULATORS.put(crs, gc);
}
assert crs.equals(gc.getCoordinateReferenceSystem()) : crs;
final GeneralDirectPosition pos = POSITIONS[Math.min(POSITIONS.length - 1,
crs.getCoordinateSystem().getDimension())];
pos.setCoordinateReferenceSystem(crs);
copy(p1, pos.ordinates);
gc.setStartingPosition(pos);
copy(p2, pos.ordinates);
gc.setDestinationPosition(pos);
return gc.getOrthodromicDistance();
}
/**
* Copies the ordinates values from the specified JTS coordinates to the specified array. The
* destination array can have any length. Only the relevant field of the source coordinate will
* be copied. If the array length is greater than 3, then all extra dimensions will be set to
* {@link Double#NaN NaN}.
*
* @param point The source coordinate.
* @param ordinates The destination array.
*/
public static void copy(final Coordinate point, final double[] ordinates) {
ensureNonNull("point", point);
ensureNonNull("ordinates", ordinates);
switch (ordinates.length) {
default:
Arrays.fill(ordinates, 3, ordinates.length, Double.NaN); // Fall through
case 3:
ordinates[2] = point.z; // Fall through
case 2:
ordinates[1] = point.y; // Fall through
case 1:
ordinates[0] = point.x; // Fall through
case 0:
break;
}
}
/**
* Converts an arbitrary Java2D shape into a JTS geometry. The created JTS geometry
* may be any of {@link LineString}, {@link LinearRing} or {@link MultiLineString}.
*
* @param shape The Java2D shape to create.
* @param factory The JTS factory to use for creating geometry.
* @return The JTS geometry.
*/
public static Geometry shapeToGeometry(final Shape shape, final GeometryFactory factory) {
ensureNonNull("shape", shape);
ensureNonNull("factory", factory);
final PathIterator iterator = shape.getPathIterator(null, ShapeUtilities.getFlatness(shape));
final double[] buffer = new double[6];
final List<Coordinate> coords = new ArrayList<Coordinate>();
final List<LineString> lines = new ArrayList<LineString>();
while (!iterator.isDone()) {
switch (iterator.currentSegment(buffer)) {
/*
* Close the polygon: the last point is equal to
* the first point, and a LinearRing is created.
*/
case PathIterator.SEG_CLOSE: {
if (!coords.isEmpty()) {
coords.add( coords.get(0) );
lines.add(factory.createLinearRing(
(Coordinate[]) coords.toArray(new Coordinate[coords.size()])));
coords.clear();
}
break;
}
/*
* General case: A LineString is created from previous
* points, and a new LineString begin for next points.
*/
case PathIterator.SEG_MOVETO: {
if (!coords.isEmpty()) {
lines.add(factory.createLineString(
(Coordinate[]) coords.toArray(new Coordinate[coords.size()])));
coords.clear();
}
// Fall through
}
case PathIterator.SEG_LINETO: {
coords.add(new Coordinate(buffer[0], buffer[1]));
break;
}
default:
throw new IllegalPathStateException();
}
iterator.next();
}
/*
* End of loops: create the last LineString if any, then create the MultiLineString.
*/
if (!coords.isEmpty()) {
lines.add(factory.createLineString(
(Coordinate[]) coords.toArray(new Coordinate[coords.size()])));
}
switch (lines.size()) {
case 0:
return null;
case 1:
return (LineString) lines.get(0);
default:
return factory.createMultiLineString(GeometryFactory.toLineStringArray(lines));
}
}
/**
* Converts a JTS 2D envelope in an {@link Envelope2D} for interoperability with the
* referencing package.
* <p>
* If the provided envelope is a {@link ReferencedEnvelope} we check
* that the provided CRS and the implicit CRS are similar.
*
* @param envelope The JTS envelope to convert.
* @param crs The coordinate reference system for the specified envelope.
* @return The GeoAPI envelope.
* @throws MismatchedDimensionException if a two-dimensional envelope can't be created
* from an envelope with the specified CRS.
*
* @since 2.3
*/
public static Envelope2D getEnvelope2D(final Envelope envelope,
final CoordinateReferenceSystem crs) throws MismatchedDimensionException {
// Initial checks
ensureNonNull("envelope", envelope);
ensureNonNull("crs", crs);
if (envelope instanceof ReferencedEnvelope) {
final ReferencedEnvelope referenced = (ReferencedEnvelope) envelope;
final CoordinateReferenceSystem implicitCRS = referenced.getCoordinateReferenceSystem();
if ((crs != null) && !CRS.equalsIgnoreMetadata(crs, implicitCRS)) {
throw new IllegalArgumentException(Errors.format(
ErrorKeys.MISMATCHED_ENVELOPE_CRS_$2, crs.getName().getCode(),
implicitCRS.getName().getCode()));
}
}
// Ensure the CRS is 2D and retrieve the new envelope
final CoordinateReferenceSystem crs2D = CRS.getHorizontalCRS(crs);
if(crs2D==null)
throw new MismatchedDimensionException(
Errors.format(
ErrorKeys.CANT_SEPARATE_CRS_$1,crs));
return new Envelope2D(crs2D, envelope.getMinX(), envelope.getMinY(), envelope.getWidth(),
envelope.getHeight());
}
/**
* Converts an envelope to a polygon.
* <p>
* The resulting polygon contains an outer ring with verticies:
* (x1,y1),(x2,y1),(x2,y2),(x1,y2),(x1,y1)
*
* @param envelope The original envelope.
* @return The envelope as a polygon.
*
* @since 2.4
*/
public static Polygon toGeometry(Envelope e) {
GeometryFactory gf = new GeometryFactory();
return gf.createPolygon(gf.createLinearRing(
new Coordinate[] {
new Coordinate(e.getMinX(), e.getMinY()),
new Coordinate(e.getMaxX(), e.getMinY()),
new Coordinate(e.getMaxX(), e.getMaxY()),
new Coordinate(e.getMinX(), e.getMaxY()),
new Coordinate(e.getMinX(), e.getMinY())
}), null);
}
/**
* Create a ReferencedEnvelope from the provided geometry, we will
* do our best to guess the CoordinateReferenceSystem making use
* of getUserData() and getSRID() as needed.
*
* @param geom Provided Geometry
* @return RefernecedEnveleope describing the bounds of the provided Geometry
*/
public static ReferencedEnvelope toEnvelope( Geometry geom ){
if( geom == null ){
return null; //return new ReferencedEnvelope(); // very empty!
}
String srsName = null;
Object userData = geom.getUserData();
if( userData != null && userData instanceof String){
srsName = (String) userData;
}
else if (geom.getSRID() > 0){
srsName = "EPSG:"+geom.getSRID();
}
CoordinateReferenceSystem crs = null;
if( userData != null && userData instanceof CoordinateReferenceSystem){
crs = (CoordinateReferenceSystem) userData;
}
else if( srsName != null ){
try {
crs = CRS.decode( srsName );
} catch (NoSuchAuthorityCodeException e) {
// e.printStackTrace();
} catch (FactoryException e) {
// e.printStackTrace();
}
}
return new ReferencedEnvelope( geom.getEnvelopeInternal(), crs );
}
/**
* Converts a {@link BoundingBox} to a polygon.
* <p>
* The resulting polygon contains an outer ring with verticies:
* (x1,y1),(x2,y1),(x2,y2),(x1,y2),(x1,y1)
*
* @param envelope The original envelope.
* @return The envelope as a polygon.
*
* @since 2.4
*/
public static Polygon toGeometry(BoundingBox e) {
GeometryFactory gf = new GeometryFactory();
return gf.createPolygon(gf.createLinearRing(
new Coordinate[] {
new Coordinate(e.getMinX(), e.getMinY()),
new Coordinate(e.getMaxX(), e.getMinY()),
new Coordinate(e.getMaxX(), e.getMaxY()),
new Coordinate(e.getMinX(), e.getMaxY()),
new Coordinate(e.getMinX(), e.getMinY())
}), null);
}
/**
* Checks a Geometry coordinates are within the area of validity of the
* specified reference system. If a coordinate falls outside the area of
* validity a {@link PointOutsideEnvelopeException} is thrown
*
* @param geom
* the geometry to check
* @param the
* crs that defines the are of validity (must not be null)
* @throws PointOutsideEnvelopeException
* @since 2.4
*/
public static void checkCoordinatesRange(Geometry geom, CoordinateReferenceSystem crs)
throws PointOutsideEnvelopeException {
// named x,y, but could be anything
CoordinateSystemAxis x = crs.getCoordinateSystem().getAxis(0);
CoordinateSystemAxis y = crs.getCoordinateSystem().getAxis(1);
// check if unbounded, many projected systems are, in this case no check
// is needed
boolean xUnbounded = Double.isInfinite(x.getMinimumValue())
&& Double.isInfinite(x.getMaximumValue());
boolean yUnbounded = Double.isInfinite(y.getMinimumValue())
&& Double.isInfinite(y.getMaximumValue());
if (xUnbounded && yUnbounded) {
return;
}
// check each coordinate
Coordinate[] c = geom.getCoordinates();
for (int i = 0; i < c.length; i++) {
if (!xUnbounded && ((c[i].x < x.getMinimumValue()) || (c[i].x > x.getMaximumValue()))) {
throw new PointOutsideEnvelopeException(c[i].x + " outside of ("
+ x.getMinimumValue() + "," + x.getMaximumValue() + ")");
}
if (!yUnbounded && ((c[i].y < y.getMinimumValue()) || (c[i].y > y.getMaximumValue()))) {
throw new PointOutsideEnvelopeException(c[i].y + " outside of ("
+ y.getMinimumValue() + "," + y.getMaximumValue() + ")");
}
}
}
}