JTS.java
/*
* 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.heigit.ors.jts;
import org.geotools.geometry.GeneralDirectPosition;
import org.geotools.metadata.i18n.ErrorKeys;
import org.geotools.metadata.i18n.Errors;
import org.geotools.referencing.GeodeticCalculator;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.geom.*;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import java.util.*;
/**
* 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>
*
* @author Jody Garnett
* @author Martin Desruisseaux
* @author Simone Giannecchini, GeoSolutions.
* @author Michael Bedward
* @version $Id$
* @source $URL$
* @since 2.2
*/
public final class JTS {
/**
* A pool of direct positions for use in {@link #orthodromicDistance}.
*/
private static final GeneralDirectPosition[] POSITIONS = new GeneralDirectPosition[4];
private static final GeometryFactory factory;
static {
for (int i = 0; i < POSITIONS.length; i++) {
POSITIONS[i] = new GeneralDirectPosition(i);
}
factory = new GeometryFactory();
}
/**
* Geodetic calculators already created for a given coordinate reference system. For use in
* {@link #orthodromicDistance}.
* <p>
* 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<>();
/**
* 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));
}
}
/**
* 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 = CALCULATORS.get(crs);
if (gc == null) {
gc = new GeodeticCalculator(crs);
CALCULATORS.put(crs, gc);
}
if (!crs.equals(gc.getCoordinateReferenceSystem())) throw new AssertionError(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) {
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;
default:
Arrays.fill(ordinates, 3, ordinates.length, Double.NaN); // Fall through
}
}
/**
* Creates a smoothed copy of the input Geometry. This is only useful for polygonal and lineal
* geometries. Point objects will be returned unchanged. The smoothing algorithm inserts new
* vertices which are positioned using Bezier splines. All vertices of the input Geometry will
* be present in the output Geometry.
* <p>
* The {@code fit} parameter controls how tightly the smoothed lines conform to the input line
* segments, with a value of 1 being tightest and 0 being loosest. Values outside this range
* will be adjusted up or down as required.
* <p>
* The input Geometry can be a simple type (e.g. LineString, Polygon), a multi-type (e.g.
* MultiLineString, MultiPolygon) or a GeometryCollection. The returned object will be of the
* same type.
*
* @param geom the input geometry
* @param fit tightness of fit from 0 (loose) to 1 (tight)
* @return a new Geometry object of the same class as {@code geom}
* @throws IllegalArgumentException if {@code geom} is {@code null}
*/
public static Geometry smooth(final Geometry geom, double fit) {
return smooth(geom, fit, new GeometryFactory());
}
/**
* Creates a smoothed copy of the input Geometry. This is only useful for polygonal and lineal
* geometries. Point objects will be returned unchanged. The smoothing algorithm inserts new
* vertices which are positioned using Bezier splines. All vertices of the input Geometry will
* be present in the output Geometry.
* <p>
* The {@code fit} parameter controls how tightly the smoothed lines conform to the input line
* segments, with a value of 1 being tightest and 0 being loosest. Values outside this range
* will be adjusted up or down as required.
* <p>
* The input Geometry can be a simple type (e.g. LineString, Polygon), a multi-type (e.g.
* MultiLineString, MultiPolygon) or a GeometryCollection. The returned object will be of the
* same type.
*
* @param geom the input geometry
* @param fit tightness of fit from 0 (loose) to 1 (tight)
* @param factory the GeometryFactory to use for creating smoothed objects
* @return a new Geometry object of the same class as {@code geom}
* @throws IllegalArgumentException if either {@code geom} or {@code factory} is {@code null}
*/
public static Geometry smooth(final Geometry geom, double fit, final GeometryFactory factory) {
ensureNonNull("geom", geom);
ensureNonNull("factory", factory);
// Adjust fit if necessary
fit = Math.max(0.0, Math.min(1.0, fit));
return smooth(geom, fit, factory, new GeometrySmoother(factory));
}
private static Geometry smooth(final Geometry geom, final double fit,
final GeometryFactory factory, GeometrySmoother smoother) {
return switch (geom.getGeometryType().toUpperCase()) {
case "POINT", "MULTIPOINT" ->
// For points, just return the input geometry
geom;
case "LINESTRING" ->
// This handles open and closed lines (LinearRings)
smoothLineString(factory, smoother, geom, fit);
case "MULTILINESTRING" -> smoothMultiLineString(factory, smoother, geom, fit);
case "POLYGON" -> smoother.smooth((Polygon) geom, fit);
case "MULTIPOLYGON" -> smoothMultiPolygon(factory, smoother, geom, fit);
case "GEOMETRYCOLLECTION" -> smoothGeometryCollection(factory, smoother, geom, fit);
default -> throw new UnsupportedOperationException("No smoothing method available for "
+ geom.getGeometryType());
};
}
private static Geometry smoothLineString(GeometryFactory factory, GeometrySmoother smoother,
Geometry geom, double fit) {
if (geom instanceof LinearRing linearRing) {
// Treat as a Polygon
Polygon poly = factory.createPolygon(linearRing, null);
Polygon smoothed = smoother.smooth(poly, fit);
return smoothed.getExteriorRing();
} else {
return smoother.smooth((LineString) geom, fit);
}
}
private static Geometry smoothMultiLineString(GeometryFactory factory,
GeometrySmoother smoother, Geometry geom, double fit) {
final int N = geom.getNumGeometries();
LineString[] smoothed = new LineString[N];
for (int i = 0; i < N; i++) {
smoothed[i] = (LineString) smoothLineString(factory, smoother, geom.getGeometryN(i),
fit);
}
return factory.createMultiLineString(smoothed);
}
private static Geometry smoothMultiPolygon(GeometryFactory factory, GeometrySmoother smoother,
Geometry geom, double fit) {
final int N = geom.getNumGeometries();
Polygon[] smoothed = new Polygon[N];
for (int i = 0; i < N; i++) {
smoothed[i] = smoother.smooth((Polygon) geom.getGeometryN(i), fit);
}
return factory.createMultiPolygon(smoothed);
}
private static Geometry smoothGeometryCollection(GeometryFactory factory,
GeometrySmoother smoother, Geometry geom, double fit) {
final int N = geom.getNumGeometries();
Geometry[] smoothed = new Geometry[N];
for (int i = 0; i < N; i++) {
smoothed[i] = smooth(geom.getGeometryN(i), fit, factory, smoother);
}
return factory.createGeometryCollection(smoothed);
}
/**
* Removes collinear points from the provided linestring.
*
* @param ls the {@link LineString} to be simplified.
* @return a new version of the provided {@link LineString} with collinear points removed.
*/
static LineString removeCollinearVertices(final LineString ls) {
if (ls == null) {
throw new NullPointerException("The provided linestring is null");
}
final int N = ls.getNumPoints();
final boolean isLinearRing = ls instanceof LinearRing;
List<Coordinate> retain = new ArrayList<>();
retain.add(ls.getCoordinateN(0));
int i0 = 0;
int i1 = 1;
int i2 = 2;
Coordinate firstCoord = ls.getCoordinateN(i0);
Coordinate midCoord;
Coordinate lastCoord;
while (i2 < N) {
midCoord = ls.getCoordinateN(i1);
lastCoord = ls.getCoordinateN(i2);
final int orientation = Orientation.index(firstCoord, midCoord, lastCoord);
// Collinearity test
if (orientation != Orientation.COLLINEAR) {
// add midcoord and change head
retain.add(midCoord);
i0 = i1;
firstCoord = ls.getCoordinateN(i0);
}
i1++;
i2++;
}
retain.add(ls.getCoordinateN(N - 1));
//
// Return value
//
final int size = retain.size();
// nothing changed?
if (size == N) {
// free everything and return original
retain.clear();
return ls;
}
return isLinearRing ? ls.getFactory()
.createLinearRing(retain.toArray(new Coordinate[size])) : ls.getFactory()
.createLineString(retain.toArray(new Coordinate[size]));
}
/**
* Removes collinear vertices from the provided {@link Polygon}.
*
* @param polygon the instance of a {@link Polygon} to remove collinear vertices from.
* @return a new instance of the provided {@link Polygon} without collinear vertices.
*/
static Polygon removeCollinearVertices(final Polygon polygon) {
if (polygon == null) {
throw new NullPointerException("The provided Polygon is null");
}
// reuse existing factory
final GeometryFactory gf = polygon.getFactory();
// work on the exterior ring
LineString exterior = polygon.getExteriorRing();
LineString shell = removeCollinearVertices(exterior);
if ((shell == null) || shell.isEmpty()) {
return null;
}
// work on the holes
List<LineString> holes = new ArrayList<>();
final int size = polygon.getNumInteriorRing();
for (int i = 0; i < size; i++) {
LineString hole = polygon.getInteriorRingN(i);
hole = removeCollinearVertices(hole);
if ((hole != null) && !hole.isEmpty()) {
holes.add(hole);
}
}
return gf.createPolygon((LinearRing) shell, holes.toArray(new LinearRing[0]));
}
/**
* Removes collinear vertices from the provided {@link Geometry}.
*
* <p>
* For the moment this implementation only accepts, {@link Polygon}, {@link LineString} and {@link MultiPolygon} It will throw an exception if the
* geometry is not one of those types
*
* @param g the instance of a {@link Geometry} to remove collinear vertices from.
* @return a new instance of the provided {@link Geometry} without collinear vertices.
*/
public static Geometry removeCollinearVertices(final Geometry g) {
if (g == null) {
throw new NullPointerException("The provided Geometry is null");
}
if (g instanceof LineString lineString) {
return removeCollinearVertices(lineString);
} else if (g instanceof Polygon polygon) {
return removeCollinearVertices(polygon);
} else if (g instanceof MultiPolygon mp) {
Polygon[] parts = new Polygon[mp.getNumGeometries()];
for (int i = 0; i < mp.getNumGeometries(); i++) {
Polygon part = (Polygon) mp.getGeometryN(i);
part = removeCollinearVertices(part);
parts[i] = part;
}
return g.getFactory().createMultiPolygon(parts);
}
throw new IllegalArgumentException(
"This method can work on LineString, Polygon and Multipolygon: " + g.getClass());
}
/**
* Removes collinear vertices from the provided {@link Geometry} if the number of point exceeds the requested minPoints.
*
* <p>
* For the moment this implementation only accepts, {@link Polygon}, {@link LineString} and {@link MultiPolygon} It will throw an exception if the
* geometry is not one of those types
*
* @param geometry the instance of a {@link Geometry} to remove collinear vertices from.
* @param minPoints perform removal of collinear points if num of vertices exceeds minPoints.
* @return a new instance of the provided {@link Geometry} without collinear vertices.
*/
public static Geometry removeCollinearVertices(final Geometry geometry, int minPoints) {
ensureNonNull("geometry", geometry);
if ((minPoints <= 0) || (geometry.getNumPoints() < minPoints)) {
return geometry;
}
if (geometry instanceof LineString lineString) {
return removeCollinearVertices(lineString);
} else if (geometry instanceof Polygon polygon) {
return removeCollinearVertices(polygon);
} else if (geometry instanceof MultiPolygon mp) {
Polygon[] parts = new Polygon[mp.getNumGeometries()];
for (int i = 0; i < mp.getNumGeometries(); i++) {
Polygon part = (Polygon) mp.getGeometryN(i);
part = removeCollinearVertices(part);
parts[i] = part;
}
return geometry.getFactory().createMultiPolygon(parts);
}
throw new IllegalArgumentException(
"This method can work on LineString, Polygon and Multipolygon: "
+ geometry.getClass());
}
public static Polygon toGeometry(final Envelope env) {
return toGeometry(env, factory);
}
public static Polygon toGeometry(final Envelope env, GeometryFactory factory) {
ensureNonNull("env", env);
return factory.createPolygon(factory.createLinearRing(new Coordinate[]{
new Coordinate(env.getMinX(), env.getMinY()),
new Coordinate(env.getMaxX(), env.getMinY()),
new Coordinate(env.getMaxX(), env.getMaxY()),
new Coordinate(env.getMinX(), env.getMaxY()),
new Coordinate(env.getMinX(), env.getMinY())}), null);
}
}