/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
* (C) 2012, Geomatys
*
* 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.geotoolkit.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.apache.sis.geometry.Envelope2D;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.geotoolkit.geometry.jts.transform.CoordinateSequenceMathTransformer;
import org.geotoolkit.geometry.jts.transform.GeometryCSTransformer;
import org.geotoolkit.geometry.jts.transform.CoordinateSequenceTransformer;
import org.apache.sis.referencing.CRS;
import org.geotoolkit.referencing.GeodeticCalculator;
import org.apache.sis.referencing.CommonCRS;
import org.geotoolkit.referencing.operation.TransformPathNotFoundException;
import org.apache.sis.referencing.operation.projection.ProjectionException;
import org.apache.sis.util.Classes;
import org.geotoolkit.display.shape.ShapeUtilities;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.factory.HintsPending;
import org.apache.sis.util.ArgumentChecks;
import org.opengis.geometry.BoundingBox;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.util.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.CoordinateSequence;
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.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.algorithm.CGAlgorithms;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Point;
import java.awt.Rectangle;
import javax.vecmath.Vector3d;
import org.apache.sis.util.Utilities;
/**
* JTS Geometry utility methods, bringing GeotoolKit to JTS. <p> Offers
* GeotoolKit 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>
*
* @module
* @since 2.2
* @version $Id$
* @author Jody Garnett
* @author Martin Desruisseaux
* @author Simone Giannecchini, GeoSolutions
* @author Quentin Boileau (Geomatys).
*/
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.geotoolkit.util.collection.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(Errors.Keys.NullArgument_1, name));
}
}
/**
* Check that the first and last points have the same 2d coordinate. If not,
* override the last coordinate value to close the ring.
*/
public static void ensureClosed(final Coordinate[] array) {
if (!array[0].equals2D(array[array.length - 1])) {
array[array.length - 1].setCoordinate(array[0]);
}
}
/**
* 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.
*
* @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.
*
* @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(Errors.Keys.IllegalTransformForType_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 to given crs.
* Id the geometry has no crs, it is assumed to be already in the given crs.
*
* @param geom The geom to transform
* @param crs target crs.
* @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.
* @throws org.opengis.util.FactoryException
*/
public static Geometry transform(final Geometry geom, final CoordinateReferenceSystem crs)
throws MismatchedDimensionException, TransformException, FactoryException {
ArgumentChecks.ensureNonNull("crs", crs);
final CoordinateReferenceSystem geomCrs = findCoordinateReferenceSystem(geom);
if(geomCrs==null) return geom;
final MathTransform trs = CRS.findOperation(geomCrs, crs, null).getMathTransform();
return transform(geom, trs);
}
/**
* 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 CoordinateSequenceTransformer cstrs = new CoordinateSequenceMathTransformer(transform);
final GeometryCSTransformer transformer = new GeometryCSTransformer(cstrs);
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 (Utilities.equalsIgnoreMetadata(crs, CommonCRS.WGS84.normalizedGeographic())) {
return envelope;
}
final MathTransform transform;
try {
transform = CRS.findOperation(crs, CommonCRS.WGS84.normalizedGeographic(), null).getMathTransform();
} catch (FactoryException exception) {
throw new TransformPathNotFoundException(Errors.format(
Errors.Keys.CantTransformEnvelope, 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 JTSEnvelope2D} 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 Types 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);
// Ensure the CRS is 2D and retrieve the new envelope
final CoordinateReferenceSystem crs2D = CRS.getHorizontalComponent(crs);
if (crs2D == null) {
throw new MismatchedDimensionException(
Errors.format(
Errors.Keys.CantSeparateCrs_1, crs));
}
return new Envelope2D(crs2D, envelope.getMinX(), envelope.getMinY(), envelope.getWidth(),
envelope.getHeight());
}
public static Polygon toGeometry(final Rectangle envelope) {
GeometryFactory gf = new GeometryFactory();
return gf.createPolygon(gf.createLinearRing(
new Coordinate[]{
new Coordinate(envelope.getMinX(), envelope.getMinY()),
new Coordinate(envelope.getMaxX(), envelope.getMinY()),
new Coordinate(envelope.getMaxX(), envelope.getMaxY()),
new Coordinate(envelope.getMinX(), envelope.getMaxY()),
new Coordinate(envelope.getMinX(), envelope.getMinY())
}), null);
}
/**
* 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(final Envelope envelope) {
GeometryFactory gf = new GeometryFactory();
return gf.createPolygon(gf.createLinearRing(
new Coordinate[]{
new Coordinate(envelope.getMinX(), envelope.getMinY()),
new Coordinate(envelope.getMaxX(), envelope.getMinY()),
new Coordinate(envelope.getMaxX(), envelope.getMaxY()),
new Coordinate(envelope.getMinX(), envelope.getMaxY()),
new Coordinate(envelope.getMinX(), envelope.getMinY())
}), null);
}
/**
* This method is not correct when dealing with antemeridian.
*
* @param env
* @return
* @deprecated Use GeometricUtilities.toGeometryJTS(Envelope, WrapResolution.NONE) for exact same behavior
*/
@Deprecated
public static Polygon toGeometry(final org.opengis.geometry.Envelope env){
final GeometryFactory gf = new GeometryFactory();
final Coordinate[] coordinates = new Coordinate[]{
new Coordinate(env.getMinimum(0), env.getMinimum(1)),
new Coordinate(env.getMinimum(0), env.getMaximum(1)),
new Coordinate(env.getMaximum(0), env.getMaximum(1)),
new Coordinate(env.getMaximum(0), env.getMinimum(1)),
new Coordinate(env.getMinimum(0), env.getMinimum(1)),
};
final LinearRing ring = gf.createLinearRing(coordinates);
return gf.createPolygon(ring, new LinearRing[0]);
}
/**
* 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 JTSEnvelope2D toEnvelope(final 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.forCode(srsName);
} catch (NoSuchAuthorityCodeException e) {
// e.printStackTrace();
} catch (FactoryException e) {
// e.printStackTrace();
}
}
return new JTSEnvelope2D(geom.getEnvelopeInternal(), crs);
}
/**
* Create an empty geometry of given type.
*
* @param <T>
* @param geomClass
* @param crs
* @param factory
* @return
*/
public static <T extends Geometry> T emptyGeometry(Class<T> geomClass, CoordinateReferenceSystem crs, GeometryFactory factory) {
ArgumentChecks.ensureNonNull("geometry class", geomClass);
if(factory==null) factory = new GeometryFactory();
final T geometry;
if(Point.class.equals(geomClass)){
geometry = (T) factory.createPoint((Coordinate)null);
}else if(LineString.class.equals(geomClass)){
geometry = (T) factory.createLineString((CoordinateSequence)null);
}else if(Polygon.class.equals(geomClass)){
geometry = (T) factory.createPolygon((CoordinateSequence)null);
}else if(MultiPoint.class.equals(geomClass)){
geometry = (T) factory.createMultiPoint((CoordinateSequence)null);
}else if(MultiLineString.class.equals(geomClass)){
geometry = (T) factory.createMultiLineString(null);
}else if(MultiPolygon.class.equals(geomClass)){
geometry = (T) factory.createMultiPolygon(null);
}else if(GeometryCollection.class.equals(geomClass)){
geometry = (T) factory.buildGeometry(null);
}else{
throw new IllegalArgumentException("Unknown geometry class "+geomClass.getName());
}
if (crs!=null) JTS.setCRS(geometry, crs);
return geometry;
}
/**
* 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(final BoundingBox envelope) {
GeometryFactory gf = new GeometryFactory();
return gf.createPolygon(gf.createLinearRing(
new Coordinate[]{
new Coordinate(envelope.getMinX(), envelope.getMinY()),
new Coordinate(envelope.getMaxX(), envelope.getMinY()),
new Coordinate(envelope.getMaxX(), envelope.getMaxY()),
new Coordinate(envelope.getMinX(), envelope.getMaxY()),
new Coordinate(envelope.getMinX(), envelope.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 ProjectionException} is thrown
*
* @param geom the geometry to check
* @param crs that defines the are of validity (must not be null)
* @throws ProjectionException
* @since 2.4
*/
public static void checkCoordinatesRange(final Geometry geom, final CoordinateReferenceSystem crs)
throws ProjectionException {
// 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 ProjectionException(c[i].x + " outside of (" + x.getMinimumValue() + "," + x.getMaximumValue() + ")");
}
if (!yUnbounded && ((c[i].y < y.getMinimumValue()) || (c[i].y > y.getMaximumValue()))) {
throw new ProjectionException(c[i].y + " outside of (" + y.getMinimumValue() + "," + y.getMaximumValue() + ")");
}
}
}
/**
* Set a crs to a geometry. 1 - if user data is a CRS, set with the crs 2 -
* if user data is a Map, add an entry with the crs
*
* @param geom, should not be null
* @param crs, if null method has no effect
*/
public static void setCRS(Geometry geom, final CoordinateReferenceSystem crs) {
ArgumentChecks.ensureNonNull("geometry", geom);
if (crs == null) {
return;
}
int srid = SRIDGenerator.toSRID(crs, SRIDGenerator.Version.V1);
Object userData = geom.getUserData();
if (userData instanceof CoordinateReferenceSystem) {
userData = crs;
} else {
if (userData instanceof Map) {
Map values = (Map) userData;
values.put(HintsPending.JTS_GEOMETRY_CRS, crs);
userData = values;
}
}
if (userData == null) {
userData = crs;
}
geom.setUserData(userData);
geom.setSRID(srid);
}
/**
* Find the crs of the geometry. 1 - search if the user data is a CRS 2 -
* search if the user data is a Map and has key JTSGeometryCRS 3 - try to
* rebuild CRS from the srid.
*
* @param geom a Geometry.
* @return null if none where successful or if geometry is null.
* @throws NoSuchAuthorityCodeException, FactoryException
*/
public static CoordinateReferenceSystem findCoordinateReferenceSystem(final Geometry geom)
throws NoSuchAuthorityCodeException, FactoryException {
//chexk if geometry is defined and prevent NullPointerException
if (geom == null) {
return null;
}
//we don't know in which crs it is, try to find it
CoordinateReferenceSystem crs = null;
final Object userData = geom.getUserData();
if (userData instanceof CoordinateReferenceSystem) {
crs = (CoordinateReferenceSystem) userData;
} else if (userData instanceof Map) {
final Map values = (Map) userData;
final Object candidate = values.get(HintsPending.JTS_GEOMETRY_CRS);
if (candidate instanceof CoordinateReferenceSystem) {
crs = (CoordinateReferenceSystem) candidate;
}
}
//not found yet, try to rebuild it from the srid
if (crs == null) {
final int srid = geom.getSRID();
if (srid != 0 && srid != -1) {
final String srs = SRIDGenerator.toSRS(srid, SRIDGenerator.Version.V1);
crs = CRS.forCode(srs);
}
}
return crs;
}
/**
* Determine the min and max "z" values in an array of Coordinates.
*
* @param cs The array to search.
* @param target array with at least two elements where to hold the min and
* max zvalues. target[0] will be filled with the minimum zvalue, target[1]
* with the maximum. The array current values, if not NaN, will be taken
* into acount in the computation.
*/
public static void zMinMax(final CoordinateSequence cs, final double[] target) {
if (cs.getDimension() < 3) {
return;
}
double zmin;
double zmax;
boolean validZFound = false;
zmin = Double.NaN;
zmax = Double.NaN;
double z;
final int size = cs.size();
for (int t = size - 1; t >= 0; t--) {
z = cs.getOrdinate(t, 2);
if (!(Double.isNaN(z))) {
if (validZFound) {
if (z < zmin) {
zmin = z;
}
if (z > zmax) {
zmax = z;
}
} else {
validZFound = true;
zmin = z;
zmax = z;
}
}
}
if (!Double.isNaN(zmin)) {
target[0] = zmin;
}
if (!Double.isNaN(zmax)) {
target[1] = zmax;
}
}
/**
* Does what it says, reverses the order of the Coordinates in the ring.
*
* @param lr The ring to reverse.
* @return A new ring with the reversed Coordinates.
*/
public static LinearRing reverseRing(final LinearRing lr) {
final GeometryFactory gf = new GeometryFactory();
final int numPoints = lr.getNumPoints() - 1;
final Coordinate[] newCoords = new Coordinate[numPoints + 1];
for (int t = numPoints; t >= 0; t--) {
newCoords[t] = lr.getCoordinateN(numPoints - t);
}
return gf.createLinearRing(newCoords);
}
/**
* Create a clockwise Polygon from the given Polygon. Will ensure that
* shells are clockwise and holes are counter-clockwise. Handle only
* <b>2D</b> Polygon and MultiPolygon Geometry. This method use the
* CGAlgorithm.isCCW() method form JTS.
*
* @param p The Polygon to make CW.
* @return The "nice" Polygon.
*/
public static <T extends Geometry> T ensureClockWise(final T g) {
if (!(g instanceof MultiPolygon) && !(g instanceof Polygon)) {
return g;
}
final GeometryFactory gf = new GeometryFactory();
boolean isMultiPolygon = false;
int nbPolygon = 1;
if (g instanceof MultiPolygon) {
nbPolygon = g.getNumGeometries();
isMultiPolygon = true;
}
final Polygon[] ps = new Polygon[nbPolygon];
for (int i = 0; i < nbPolygon; i++) {
final Polygon p;
if (isMultiPolygon) {
p = (Polygon) g.getGeometryN(i);
} else {
p = (Polygon) g;
}
final LinearRing outer;
final LinearRing[] holes = new LinearRing[p.getNumInteriorRing()];
Coordinate[] coords;
coords = p.getExteriorRing().getCoordinates();
if (CGAlgorithms.isCCW(coords)) {
outer = reverseRing((LinearRing) p.getExteriorRing());
} else {
outer = (LinearRing) p.getExteriorRing();
}
for (int t = 0, tt = p.getNumInteriorRing(); t < tt; t++) {
coords = p.getInteriorRingN(t).getCoordinates();
if (!(CGAlgorithms.isCCW(coords))) {
holes[t] = reverseRing((LinearRing) p.getInteriorRingN(t));
} else {
holes[t] = (LinearRing) p.getInteriorRingN(t);
}
}
ps[i] = gf.createPolygon(outer, holes);
}
if (isMultiPolygon) {
return (T) gf.createMultiPolygon(ps);
} else {
return (T) ps[0];
}
}
/**
* Create a counter clockwise Polygon from the given Polygon. Will ensure
* that shells are counter-clockwise and holes are clockwise. Handle only
* <b>2D</b> Polygon and MultiPolygon Geometry. This method use the
* CGAlgorithm.isCCW() method form JTS.
*
* @param p The Polygon to make CCW.
* @return The "nice" Polygon.
*/
public static <T extends Geometry> T ensureCounterClockWise(final T g) {
if (!(g instanceof MultiPolygon) && !(g instanceof Polygon)) {
return g;
}
final GeometryFactory gf = new GeometryFactory();
boolean isMultiPolygon = false;
int nbPolygon = 1;
if (g instanceof MultiPolygon) {
nbPolygon = g.getNumGeometries();
isMultiPolygon = true;
}
final Polygon[] ps = new Polygon[nbPolygon];
for (int i = 0; i < nbPolygon; i++) {
final Polygon p;
if (isMultiPolygon) {
p = (Polygon) g.getGeometryN(i);
} else {
p = (Polygon) g;
}
final LinearRing outer;
final LinearRing[] holes = new LinearRing[p.getNumInteriorRing()];
Coordinate[] coords;
coords = p.getExteriorRing().getCoordinates();
if (CGAlgorithms.isCCW(coords)) {
outer = (LinearRing) p.getExteriorRing();
} else {
outer = reverseRing((LinearRing) p.getExteriorRing());
}
for (int t = 0, tt = p.getNumInteriorRing(); t < tt; t++) {
coords = p.getInteriorRingN(t).getCoordinates();
if (!(CGAlgorithms.isCCW(coords))) {
holes[t] = (LinearRing) p.getInteriorRingN(t);
} else {
holes[t] = reverseRing((LinearRing) p.getInteriorRingN(t));
}
}
ps[i] = gf.createPolygon(outer, holes);
}
if (isMultiPolygon) {
return (T) gf.createMultiPolygon(ps);
} else {
return (T) ps[0];
}
}
/**
* Create a clockwise Polygon from the given Polygon. Will ensure that
* shells are clockwise and holes are counter-clockwise. Handle 2D and 3D
* Polygon and MultiPolygon Geometry.
*
* @param p The Polygon to make CW.
* @return The "nice" Polygon.
*/
public static <T extends Geometry> T ensureClockWise3D(final T g) {
if (!(g instanceof MultiPolygon) && !(g instanceof Polygon)) {
return g;
}
final GeometryFactory gf = new GeometryFactory();
boolean isMultiPolygon = false;
int nbPolygon = 1;
if (g instanceof MultiPolygon) {
nbPolygon = g.getNumGeometries();
isMultiPolygon = true;
}
final Polygon[] ps = new Polygon[nbPolygon];
for (int i = 0; i < nbPolygon; i++) {
final Polygon p;
if (isMultiPolygon) {
p = (Polygon) g.getGeometryN(i);
} else {
p = (Polygon) g;
}
final LinearRing outer;
final LinearRing[] holes = new LinearRing[p.getNumInteriorRing()];
Coordinate[] coords;
coords = p.getExteriorRing().getCoordinates();
if (isCCW3D(coords)) {
outer = reverseRing((LinearRing) p.getExteriorRing());
} else {
outer = (LinearRing) p.getExteriorRing();
}
for (int t = 0, tt = p.getNumInteriorRing(); t < tt; t++) {
coords = p.getInteriorRingN(t).getCoordinates();
if (!(isCCW3D(coords))) {
holes[t] = reverseRing((LinearRing) p.getInteriorRingN(t));
} else {
holes[t] = (LinearRing) p.getInteriorRingN(t);
}
}
ps[i] = gf.createPolygon(outer, holes);
}
if (isMultiPolygon) {
return (T) gf.createMultiPolygon(ps);
} else {
return (T) ps[0];
}
}
/**
* Create a counter clockwise Polygon from the given Polygon. Will ensure
* that shells are counter-clockwise and holes are clockwise. Handle 2D and
* 3D Polygon and MultiPolygon Geometry.
*
* @param p The Polygon to make CCW.
* @return The "nice" Polygon.
*/
public static <T extends Geometry> T ensureCounterClockWise3D(final T g) {
if (!(g instanceof MultiPolygon) && !(g instanceof Polygon)) {
return g;
}
final GeometryFactory gf = new GeometryFactory();
boolean isMultiPolygon = false;
int nbPolygon = 1;
if (g instanceof MultiPolygon) {
nbPolygon = g.getNumGeometries();
isMultiPolygon = true;
}
final Polygon[] ps = new Polygon[nbPolygon];
for (int i = 0; i < nbPolygon; i++) {
final Polygon p;
if (isMultiPolygon) {
p = (Polygon) g.getGeometryN(i);
} else {
p = (Polygon) g;
}
final LinearRing outer;
final LinearRing[] holes = new LinearRing[p.getNumInteriorRing()];
Coordinate[] coords;
coords = p.getExteriorRing().getCoordinates();
if (isCCW3D(coords)) {
outer = (LinearRing) p.getExteriorRing();
} else {
outer = reverseRing((LinearRing) p.getExteriorRing());
}
for (int t = 0, tt = p.getNumInteriorRing(); t < tt; t++) {
coords = p.getInteriorRingN(t).getCoordinates();
if (!(isCCW3D(coords))) {
holes[t] = (LinearRing) p.getInteriorRingN(t);
} else {
holes[t] = reverseRing((LinearRing) p.getInteriorRingN(t));
}
}
ps[i] = gf.createPolygon(outer, holes);
}
if (isMultiPolygon) {
return (T) gf.createMultiPolygon(ps);
} else {
return (T) ps[0];
}
}
/**
* Test if a ring is counter clockwise oriended or not in 3D.
*
* @param ring The ring to test.
* @return true if ring is CCW, false if ring is CW.
*/
public static boolean isCCW3D(Coordinate[] ring) {
final int nbCoords = ring.length - 1;
double buff = 0.0;
for (int i = 1; i < nbCoords; i++) {
final Vector3d v1 = new Vector3d((ring[i].x - ring[i - 1].x), (ring[i].y - ring[i - 1].y), (ring[i].z - ring[i - 1].z));
final Vector3d v2 = new Vector3d((ring[i + 1].x - ring[i].x), (ring[i + 1].y - ring[i].y), (ring[i + 1].z - ring[i].z));
v1.cross(v1, v2);
v1.normalize();
buff += (v1.x + v1.y + v1.z);
}
if (buff > 0.0) {
return true;
} else {
return false;
}
}
/**
* This method return the common CoordinateReferenceSystem of two
* geometries. If first geometry has a CRS, it'll be returned. If second
* geometry has a CRS AND the first one CRS is null, it's the second
* geometry CRS that will be returned. If first and second geometries CRS
* are null, null CRS will be returned.
*
* @param geom1
* @param geom2
* @return the CRS keeped for the geometries.
* @throws FactoryException
* @throws TransformException
*/
public static CoordinateReferenceSystem getCommonCRS(final Geometry geom1, final Geometry geom2) throws FactoryException, TransformException {
CoordinateReferenceSystem resultCRS = null;
//get geometies CRS
final CoordinateReferenceSystem crs1 = findCoordinateReferenceSystem(geom1);
final CoordinateReferenceSystem crs2 = findCoordinateReferenceSystem(geom2);
//crs1 exist
if (crs1 != null) {
resultCRS = crs1;
} else {
//crs1 == null and crs2 exist
if (crs2 != null) {
resultCRS = crs2;
}
}
return resultCRS;
}
/**
* This utility method convert a geometry into a different CoordinateReferenceSystem.
* If the geometry crs is not defined, the geometry will be returned without transformation.
* @param geom a geometry to convert. (Not null)
* @param crsTarget the target CoordinateReferenceSystem (Not null)
* @return the geometry converted with targetCRS as geometry CRS.
* @throws MismatchedDimensionException
* @throws TransformException
* @throws FactoryException
*/
public static Geometry convertToCRS(final Geometry geom, final CoordinateReferenceSystem crsTarget)
throws MismatchedDimensionException, TransformException, FactoryException {
ArgumentChecks.ensureNonNull("geometry", geom);
ArgumentChecks.ensureNonNull("crsTarget", crsTarget);
//get geometry CRS
final CoordinateReferenceSystem crsGeom = findCoordinateReferenceSystem(geom);
if (crsGeom == null) {
return geom;
}
//convert geometry
final MathTransform mt = CRS.findOperation(crsGeom, crsTarget, null).getMathTransform();
final Geometry result = transform(geom, mt);
setCRS(result, crsTarget);
return result;
}
/**
* This method check if two geometries have a different CRS.
* @param geom1
* @param geom2
* @return true if geom1 and geom2 have different CRS.
* @throws FactoryException
*/
public static boolean isConversionNeeded(final Geometry geom1, final Geometry geom2) throws FactoryException {
final CoordinateReferenceSystem crs1 = findCoordinateReferenceSystem(geom1);
final CoordinateReferenceSystem crs2 = findCoordinateReferenceSystem(geom2);
return crs1 != null && crs2 != null && (!crs1.equals(crs2));
}
}