| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.geometry.wrapper.jts; |
| |
| import java.awt.Shape; |
| import java.util.List; |
| import java.util.Arrays; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.OptionalInt; |
| import java.util.function.BiFunction; |
| import java.util.function.BiPredicate; |
| import java.util.function.IntFunction; |
| import org.locationtech.jts.geom.Coordinate; |
| import org.locationtech.jts.geom.CoordinateSequence; |
| import org.locationtech.jts.geom.Envelope; |
| import org.locationtech.jts.geom.Geometry; |
| import org.locationtech.jts.geom.GeometryCollection; |
| import org.locationtech.jts.geom.GeometryFactory; |
| import org.locationtech.jts.geom.LineString; |
| import org.locationtech.jts.geom.LinearRing; |
| import org.locationtech.jts.geom.MultiLineString; |
| import org.locationtech.jts.geom.MultiPoint; |
| import org.locationtech.jts.geom.MultiPolygon; |
| import org.locationtech.jts.geom.Point; |
| import org.locationtech.jts.geom.Polygon; |
| import org.locationtech.jts.io.WKTWriter; |
| import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; |
| import org.locationtech.jts.simplify.TopologyPreservingSimplifier; |
| import org.opengis.util.FactoryException; |
| import org.opengis.geometry.DirectPosition; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.operation.CoordinateOperation; |
| import org.opengis.referencing.operation.TransformException; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.apache.sis.referencing.privy.ReferencingUtilities; |
| import org.apache.sis.geometry.DirectPosition2D; |
| import org.apache.sis.geometry.GeneralDirectPosition; |
| import org.apache.sis.geometry.GeneralEnvelope; |
| import org.apache.sis.geometry.wrapper.Geometries; |
| import org.apache.sis.geometry.wrapper.GeometryType; |
| import org.apache.sis.geometry.wrapper.GeometryWrapper; |
| import org.apache.sis.util.UnconvertibleObjectException; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.ArraysExt; |
| import org.apache.sis.util.Debug; |
| import org.apache.sis.util.collection.BackingStoreException; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.filter.sqlmm.SQLMM; |
| |
| // Specific to the geoapi-3.1 and geoapi-4.0 branches: |
| import org.opengis.filter.SpatialOperatorName; |
| import org.opengis.filter.DistanceOperatorName; |
| |
| |
| /** |
| * The wrapper of Java Topology Suite (JTS) geometries. |
| * |
| * @author Johann Sorel (Geomatys) |
| * @author Martin Desruisseaux (Geomatys) |
| * @author Alexis Manin (Geomatys) |
| */ |
| final class Wrapper extends GeometryWrapper { |
| /** |
| * The wrapped implementation. |
| */ |
| private final Geometry geometry; |
| |
| /** |
| * Creates a new wrapper around the given geometry. |
| */ |
| Wrapper(final Geometry geometry) { |
| this.geometry = geometry; |
| } |
| |
| /** |
| * Returns the given geometry in new wrapper, |
| * or {@code this} if {@code g} is same as current geometry. |
| * |
| * @param result the geometry computed by a JTS operation. |
| * @return wrapper for the given geometry. May be {@code this}. |
| */ |
| private Wrapper rewrap(final Geometry result) { |
| return (result != geometry) ? new Wrapper(result) : this; |
| } |
| |
| /** |
| * Returns the implementation-dependent factory of geometric object. |
| */ |
| @Override |
| protected Geometries<Geometry> factory() { |
| return Factory.INSTANCE; |
| } |
| |
| /** |
| * Returns the geometry specified at construction time. |
| */ |
| @Override |
| protected Object implementation() { |
| return geometry; |
| } |
| |
| /** |
| * Returns the Spatial Reference System Identifier (SRID) if available. |
| * This is <em>not</em> necessarily an EPSG code, even it is common practice to use |
| * the same numerical values as EPSG. Note that the absence of SRID does not mean |
| * that {@link #getCoordinateReferenceSystem()} would return no CRS. |
| */ |
| @Override |
| public OptionalInt getSRID() { |
| final int srid = geometry.getSRID(); |
| return (srid != 0) ? OptionalInt.of(srid) : OptionalInt.empty(); |
| } |
| |
| /** |
| * Returns the geometry coordinate reference system, or {@code null} if none. |
| * |
| * @return the coordinate reference system, or {@code null} if none. |
| * @throws BackingStoreException if the CRS cannot be created from the SRID code. |
| */ |
| @Override |
| public CoordinateReferenceSystem getCoordinateReferenceSystem() { |
| try { |
| return JTS.getCoordinateReferenceSystem(geometry); |
| } catch (FactoryException e) { |
| throw new BackingStoreException(e); |
| } |
| } |
| |
| /** |
| * Sets the coordinate reference system. This method overwrites any previous user object. |
| * This is okay for the context in which Apache SIS uses this method, which is only for |
| * newly created geometries. |
| */ |
| @Override |
| public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs) { |
| ArgumentChecks.ensureDimensionMatches("crs", getCoordinatesDimension(geometry), crs); |
| JTS.setCoordinateReferenceSystem(geometry, crs); |
| } |
| |
| /** |
| * Gets the number of dimensions of geometry vertex (sequence of coordinate tuples), which can be 2 or 3. |
| * Note that this is different than the {@linkplain Geometry#getDimension() geometry topological dimension}, |
| * which can be 0, 1 or 2. |
| * |
| * @param geometry the geometry for which to get <em>vertex</em> (not topological) dimension. |
| * @return vertex dimension of the given geometry. |
| * @throws IllegalArgumentException if the type of the given geometry is not recognized. |
| */ |
| private static int getCoordinatesDimension(final Geometry geometry) { |
| final CoordinateSequence cs; |
| if (geometry instanceof Point) { |
| // Most efficient method (no allocation) in JTS 1.18. |
| cs = ((Point) geometry).getCoordinateSequence(); |
| } else if (geometry instanceof LineString) { |
| // Most efficient method (no allocation) in JTS 1.18. |
| cs = ((LineString) geometry).getCoordinateSequence(); |
| } else if (geometry instanceof Polygon) { |
| return getCoordinatesDimension(((Polygon) geometry).getExteriorRing()); |
| } else if (geometry instanceof GeometryCollection) { |
| final GeometryCollection gc = (GeometryCollection) geometry; |
| final int n = gc.getNumGeometries(); |
| if (n == 0) { |
| return Factory.TRIDIMENSIONAL; // Undefined coordinates, JTS assumes 3 for empty geometries. |
| } |
| for (int i=0; i<n; i++) { |
| // If at least one geometry is 3D, consider the whole geometry as 3D. |
| final int d = getCoordinatesDimension(gc.getGeometryN(i)); |
| if (d > Factory.BIDIMENSIONAL) return d; |
| } |
| return Factory.BIDIMENSIONAL; |
| } else { |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.UnknownType_1, geometry.getGeometryType())); |
| } |
| return cs.getDimension(); |
| } |
| |
| /** |
| * Returns the envelope of the wrapped JTS geometry. Never null, but may be empty. |
| * In current implementation, <var>z</var> values of three-dimensional envelopes |
| * are {@link Double#NaN}. It may change in a future version if we have a way to |
| * get those <var>z</var> values from a JTS object. |
| */ |
| @Override |
| public GeneralEnvelope getEnvelope() { |
| final Envelope bounds = geometry.getEnvelopeInternal(); |
| final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(); |
| final var env = (crs != null) ? new GeneralEnvelope(crs) : new GeneralEnvelope(Factory.BIDIMENSIONAL); |
| env.setToNaN(); |
| if (!bounds.isNull()) { |
| env.setRange(0, bounds.getMinX(), bounds.getMaxX()); |
| env.setRange(1, bounds.getMinY(), bounds.getMaxY()); |
| } |
| return env; |
| } |
| |
| /** |
| * Returns the centroid of the wrapped geometry as a direct position. |
| */ |
| @Override |
| public DirectPosition getCentroid() { |
| final Coordinate c = geometry.getCentroid().getCoordinate(); |
| final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(); |
| if (crs == null) { |
| final double z = c.getZ(); |
| if (!Double.isNaN(z)) { |
| return new GeneralDirectPosition(c.x, c.y, z); |
| } |
| } else if (ReferencingUtilities.getDimension(crs) != Factory.BIDIMENSIONAL) { |
| final GeneralDirectPosition point = new GeneralDirectPosition(crs); |
| point.setCoordinate(0, c.x); |
| point.setCoordinate(1, c.y); |
| point.setCoordinate(2, c.getZ()); |
| return point; |
| } |
| return new DirectPosition2D(crs, c.x, c.y); |
| } |
| |
| /** |
| * If the wrapped geometry is a point, returns its coordinates. Otherwise returns {@code null}. |
| * If non-null, the returned array may have a length of 2 or 3. |
| */ |
| @Override |
| public double[] getPointCoordinates() { |
| if (!(geometry instanceof Point)) { |
| return null; |
| } |
| final Coordinate pt = ((Point) geometry).getCoordinate(); |
| final double z = pt.getZ(); |
| final double[] coord; |
| if (Double.isNaN(z)) { |
| coord = new double[Factory.BIDIMENSIONAL]; |
| } else { |
| coord = new double[Factory.TRIDIMENSIONAL]; |
| coord[2] = z; |
| } |
| coord[1] = pt.y; |
| coord[0] = pt.x; |
| return coord; |
| } |
| |
| /** |
| * Returns all coordinate tuples in the wrapped geometry. |
| * This method is currently used for testing purpose only. |
| */ |
| @Debug |
| @Override |
| public double[] getAllCoordinates() { |
| final Coordinate[] points = geometry.getCoordinates(); |
| final double[] coordinates = new double[points.length * Factory.BIDIMENSIONAL]; |
| int i = 0; |
| for (final Coordinate p : points) { |
| coordinates[i++] = p.x; |
| coordinates[i++] = p.y; |
| } |
| return coordinates; |
| } |
| |
| /** |
| * Merges a sequence of points or paths after the wrapped geometry. |
| * |
| * @throws ClassCastException if an element in the iterator is not a JTS geometry. |
| */ |
| @Override |
| public Geometry mergePolylines(final Iterator<?> polylines) { |
| final List<Coordinate> coordinates = new ArrayList<>(); |
| final List<Geometry> lines = new ArrayList<>(); |
| boolean isFloat = true; |
| add: for (Geometry next = geometry;;) { |
| if (next instanceof Point) { |
| final Coordinate pt = ((Point) next).getCoordinate(); |
| if (!Double.isNaN(pt.x) && !Double.isNaN(pt.y)) { |
| isFloat = Factory.isFloat(isFloat, (Point) next); |
| coordinates.add(pt); |
| } else { |
| Factory.INSTANCE.toLineString(coordinates, lines, false, isFloat); |
| coordinates.clear(); |
| isFloat = true; |
| } |
| } else { |
| final int n = next.getNumGeometries(); |
| for (int i=0; i<n; i++) { |
| final LineString ls = (LineString) next.getGeometryN(i); |
| if (coordinates.isEmpty()) { |
| lines.add(ls); |
| } else { |
| if (isFloat) isFloat = Factory.isFloat(ls.getCoordinateSequence()); |
| coordinates.addAll(Arrays.asList(ls.getCoordinates())); |
| Factory.INSTANCE.toLineString(coordinates, lines, false, isFloat); |
| coordinates.clear(); |
| isFloat = true; |
| } |
| } |
| } |
| /* |
| * `polylines.hasNext()` check is conceptually part of `for` instruction, |
| * except that we need to skip this condition during the first iteration. |
| */ |
| do if (!polylines.hasNext()) break add; |
| while ((next = (Geometry) polylines.next()) == null); |
| } |
| Factory.INSTANCE.toLineString(coordinates, lines, false, isFloat); |
| return Factory.INSTANCE.toGeometry(lines, false, isFloat); |
| } |
| |
| /** |
| * Applies a filter predicate between this geometry and another geometry. |
| * This method assumes that the two geometries are in the same CRS (this is not verified). |
| * |
| * <p><b>Note:</b> {@link SpatialOperatorName#BBOX} is implemented by {@code NOT DISJOINT}. |
| * It is caller's responsibility to ensure that one of the geometries is rectangular, |
| * for example by a call to {@link Geometry#getEnvelope()}.</p> |
| * |
| * @throws ClassCastException if the given wrapper is not for the same geometry library. |
| */ |
| @Override |
| protected boolean predicateSameCRS(final SpatialOperatorName type, final GeometryWrapper other) { |
| final int ordinal = type.ordinal(); |
| if (ordinal >= 0 && ordinal < PREDICATES.length) { |
| final BiPredicate<Geometry,Geometry> op = PREDICATES[ordinal]; |
| if (op != null) { |
| return op.test(geometry, ((Wrapper) other).geometry); |
| } |
| } |
| return super.predicateSameCRS(type, other); |
| } |
| |
| /** |
| * All predicates recognized by {@link #predicate(SpatialOperatorName, Geometry)}. |
| * Array indices are {@link SpatialOperatorName#ordinal()} values. |
| */ |
| @SuppressWarnings({"unchecked","rawtypes"}) |
| private static final BiPredicate<Geometry,Geometry>[] PREDICATES = |
| new BiPredicate[SpatialOperatorName.OVERLAPS.ordinal() + 1]; |
| static { |
| PREDICATES[SpatialOperatorName.BBOX .ordinal()] = (a,b) -> !a.disjoint(b); |
| PREDICATES[SpatialOperatorName.EQUALS .ordinal()] = Geometry::equalsTopo; |
| PREDICATES[SpatialOperatorName.DISJOINT .ordinal()] = Geometry::disjoint; |
| PREDICATES[SpatialOperatorName.INTERSECTS.ordinal()] = Geometry::intersects; |
| PREDICATES[SpatialOperatorName.TOUCHES .ordinal()] = Geometry::touches; |
| PREDICATES[SpatialOperatorName.CROSSES .ordinal()] = Geometry::crosses; |
| PREDICATES[SpatialOperatorName.WITHIN .ordinal()] = Geometry::within; |
| PREDICATES[SpatialOperatorName.CONTAINS .ordinal()] = Geometry::contains; |
| PREDICATES[SpatialOperatorName.OVERLAPS .ordinal()] = Geometry::overlaps; |
| } |
| |
| /** |
| * Applies a filter predicate between this geometry and another geometry within a given distance. |
| * This method assumes that the two geometries are in the same CRS and that the unit of measurement |
| * is the same for {@code distance} than for axes (this is not verified). |
| * |
| * @throws ClassCastException if the given wrapper is not for the same geometry library. |
| */ |
| @Override |
| protected boolean predicateSameCRS(final DistanceOperatorName type, |
| final GeometryWrapper other, final double distance) |
| { |
| boolean reverse = (type != DistanceOperatorName.WITHIN); |
| if (reverse && type != DistanceOperatorName.BEYOND) { |
| return super.predicateSameCRS(type, other, distance); |
| } |
| return geometry.isWithinDistance(((Wrapper) other).geometry, distance) ^ reverse; |
| } |
| |
| /** |
| * Applies a SQLMM operation on this geometry. |
| * |
| * @param operation the SQLMM operation to apply. |
| * @param other the other geometry, or {@code null} if the operation requires only one geometry. |
| * @param argument an operation-specific argument, or {@code null} if not applicable. |
| * @return result of the specified operation. |
| * @throws ClassCastException if the operation can only be executed on some specific argument types |
| * (for example geometries that are polylines) and one of the argument is not of that type. |
| */ |
| @Override |
| protected Object operationSameCRS(final SQLMM operation, final GeometryWrapper other, final Object argument) { |
| /* |
| * For all operation producing a geometry, the result is collected for post-processing. |
| * For all other kinds of value, the result is returned directly in the switch statement. |
| */ |
| final Geometry result; |
| switch (operation) { |
| case ST_IsMeasured: return Boolean.FALSE; |
| case ST_Dimension: return geometry.getDimension(); |
| case ST_SRID: return geometry.getSRID(); |
| case ST_IsEmpty: return geometry.isEmpty(); |
| case ST_IsSimple: return geometry.isSimple(); |
| case ST_IsValid: return geometry.isValid(); |
| case ST_Envelope: return getEnvelope(); |
| case ST_Boundary: result = geometry.getBoundary(); break; |
| case ST_ConvexHull: result = geometry.convexHull(); break; |
| case ST_Buffer: result = geometry.buffer(((Number) argument).doubleValue()); break; |
| case ST_Intersection: result = geometry.intersection (((Wrapper) other).geometry); break; |
| case ST_Union: result = geometry.union (((Wrapper) other).geometry); break; |
| case ST_Difference: result = geometry.difference (((Wrapper) other).geometry); break; |
| case ST_SymDifference: result = geometry.symDifference(((Wrapper) other).geometry); break; |
| case ST_Distance: return geometry.distance (((Wrapper) other).geometry); |
| case ST_Equals: return geometry.equalsTopo (((Wrapper) other).geometry); |
| case ST_Relate: return geometry.relate (((Wrapper) other).geometry, argument.toString()); |
| case ST_Disjoint: return geometry.disjoint (((Wrapper) other).geometry); |
| case ST_Intersects: return geometry.intersects (((Wrapper) other).geometry); |
| case ST_Touches: return geometry.touches (((Wrapper) other).geometry); |
| case ST_Crosses: return geometry.crosses (((Wrapper) other).geometry); |
| case ST_Within: return geometry.within (((Wrapper) other).geometry); |
| case ST_Contains: return geometry.contains (((Wrapper) other).geometry); |
| case ST_Overlaps: return geometry.overlaps (((Wrapper) other).geometry); |
| case ST_AsText: return new WKTWriter().write(geometry); // WKTWriter() constructor is cheap. |
| case ST_AsBinary: return FilteringContext.writeWKB(geometry); |
| case ST_X: return ((Point) geometry).getX(); |
| case ST_Y: return ((Point) geometry).getY(); |
| case ST_Z: return ((Point) geometry).getCoordinate().getZ(); |
| case ST_ToLineString: return geometry; // JTS does not have curves. |
| case ST_NumGeometries: return geometry.getNumGeometries(); |
| case ST_NumPoints: return geometry.getNumPoints(); |
| case ST_PointN: result = ((LineString) geometry).getPointN(toIndex(argument)); break; |
| case ST_StartPoint: result = ((LineString) geometry).getStartPoint(); break; |
| case ST_EndPoint: result = ((LineString) geometry).getEndPoint(); break; |
| case ST_IsClosed: return ((LineString) geometry).isClosed(); |
| case ST_IsRing: return ((LineString) geometry).isRing(); |
| case ST_Perimeter: // Fallthrough: length is the perimeter for polygons. |
| case ST_Length: return geometry.getLength(); |
| case ST_Area: return geometry.getArea(); |
| case ST_Centroid: result = geometry.getCentroid(); break; |
| case ST_PointOnSurface: result = geometry.getInteriorPoint(); break; |
| case ST_ExteriorRing: result = ((Polygon) geometry).getExteriorRing(); break; |
| case ST_InteriorRingN: result = ((Polygon) geometry).getInteriorRingN(toIndex(argument)); break; |
| case ST_NumInteriorRings: return ((Polygon) geometry).getNumInteriorRing(); |
| case ST_GeometryN: result = geometry.getGeometryN(toIndex(argument)); break; |
| case ST_ToPoint: |
| case ST_ToPolygon: |
| case ST_ToMultiPoint: |
| case ST_ToMultiLine: |
| case ST_ToMultiPolygon: |
| case ST_ToGeomColl: { |
| final GeometryType target = operation.getGeometryType().get(); |
| final Class<?> type = getGeometryClass(target); |
| if (type.isInstance(geometry)) { |
| return geometry; |
| } |
| result = convert(target); |
| break; |
| } |
| case ST_Is3D: { |
| final Coordinate c = geometry.getCoordinate(); |
| return (c != null) ? !Double.isNaN(c.z) : null; |
| } |
| case ST_CoordDim: { |
| final Coordinate c = geometry.getCoordinate(); |
| return (c != null) ? Double.isNaN(c.z) ? 2 : 3 : null; |
| } |
| case ST_GeometryType: { |
| for (int i=0; i < TYPES.length; i++) { |
| if (TYPES[i].isInstance(geometry)) { |
| return SQLMM_NAMES[i]; |
| } |
| } |
| return null; |
| } |
| case ST_ExplicitPoint: { |
| final Coordinate c = ((Point) geometry).getCoordinate(); |
| if (c == null) return ArraysExt.EMPTY_DOUBLE; |
| final double x = c.getX(); |
| final double y = c.getY(); |
| final double z = c.getZ(); |
| return Double.isNaN(z) ? new double[] {x, y} : new double[] {x, y, z}; |
| } |
| case ST_Simplify: { |
| final double distance = ((Number) argument).doubleValue(); |
| result = DouglasPeuckerSimplifier.simplify(geometry, distance); |
| break; |
| } |
| case ST_SimplifyPreserveTopology: { |
| final double distance = ((Number) argument).doubleValue(); |
| result = TopologyPreservingSimplifier.simplify(geometry, distance); |
| break; |
| } |
| default: return super.operationSameCRS(operation, other, argument); |
| } |
| JTS.copyMetadata(geometry, result); |
| return result; |
| } |
| |
| /** |
| * The types of JTS objects to be recognized by the SQLMM {@code ST_GeometryType} operation. |
| */ |
| private static final Class<?>[] TYPES = { |
| Point.class, LineString.class, Polygon.class, |
| MultiPoint.class, MultiLineString.class, MultiPolygon.class, |
| GeometryCollection.class, Geometry.class, |
| }; |
| |
| /** |
| * The SQLMM names for the types listed in the {@link #TYPES} array. |
| */ |
| private static final String[] SQLMM_NAMES = { |
| "ST_Point", "ST_LineString", "ST_Polygon", |
| "ST_MultiPoint", "ST_MultiLineString", "ST_MultiPolygon", |
| "ST_GeomCollection", "ST_Geometry" |
| }; |
| |
| /** |
| * Converts the given argument to a zero-based index. |
| * |
| * @throws ClassCastException if the argument is not a string or a number. |
| * @throws NumberFormatException if the argument is an unparseable string. |
| * @throws IllegalArgumentException if the argument is zero or negative. |
| */ |
| private static int toIndex(final Object argument) { |
| final int i = (argument instanceof CharSequence) |
| ? Integer.parseInt(argument.toString()) |
| : ((Number) argument).intValue(); // ClassCastException is part of this method contract. |
| ArgumentChecks.ensureStrictlyPositive("index", i); |
| return i - 1; |
| } |
| |
| /** |
| * Converts the wrapped geometry to the specified type. |
| * If the geometry is already of that type, it is returned unchanged. |
| * Otherwise coordinates are copied in a new geometry of the requested type. |
| * |
| * <p>The following conversions are illegal and will cause an {@link IllegalArgumentException} to be thrown:</p> |
| * <ul> |
| * <li>From point to polyline or polygon (exception thrown by JTS itself).</li> |
| * <li>From geometry collection (except multi-point) to polyline.</li> |
| * <li>From geometry collection (except multi-point and multi-line string) to polygon.</li> |
| * <li>From geometry collection containing nested collections.</li> |
| * </ul> |
| * |
| * The conversion from {@link MultiLineString} to {@link Polygon} is defined as following: |
| * the first {@link LineString} is taken as the exterior {@link LinearRing} and all others |
| * {@link LineString}s are interior {@link LinearRing}s. |
| * This rule is defined by some SQLMM operations. |
| * |
| * @param target the desired type. |
| * @return the converted geometry. |
| * @throws IllegalArgumentException if the geometry cannot be converted to the specified type. |
| */ |
| @Override |
| public GeometryWrapper toGeometryType(final GeometryType target) { |
| if (!getGeometryClass(target).isInstance(geometry)) { |
| final Geometry result = convert(target); |
| if (result != geometry) { |
| JTS.copyMetadata(geometry, result); |
| return new Wrapper(result); |
| } |
| } |
| return this; |
| } |
| |
| /** |
| * Returns the geometry class of the given instance. |
| * |
| * @param type type of geometry for which the class is desired. |
| * @return implementation class for the geometry of the specified type. |
| */ |
| static Class<?> getGeometryClass(final GeometryType type) { |
| switch (type) { |
| default: return Geometry.class; |
| case POINT: return Point.class; |
| case LINESTRING: return LineString.class; |
| case POLYGON: return Polygon.class; |
| case MULTIPOINT: return MultiPoint.class; |
| case MULTILINESTRING: return MultiLineString.class; |
| case MULTIPOLYGON: return MultiPolygon.class; |
| } |
| } |
| |
| /** |
| * Converts the given geometry to the specified type without wrapper. |
| * This is the implementation of {@link #toGeometryType(GeometryType)}. |
| * Caller should invoke {@link JTS#copyMetadata(Geometry, Geometry)} after this method. |
| * |
| * @param target the desired type. |
| * @return the converted geometry. |
| * @throws IllegalArgumentException if the geometry cannot be converted to the specified type. |
| */ |
| private Geometry convert(final GeometryType target) { |
| final GeometryFactory factory = geometry.getFactory(); |
| switch (target) { |
| case POINT: { |
| return geometry.getCentroid(); |
| } |
| case LINESTRING: { |
| if (isCollection(geometry)) break; |
| return factory.createLineString(geometry.getCoordinates()); |
| } |
| case POLYGON: { |
| if (!geometry.isEmpty() && geometry instanceof MultiLineString) { |
| // SQLMM `ST_BdMPolyFromText` and `ST_BdMPolyFromWKB` behavior. |
| final MultiLineString lines = (MultiLineString) geometry; |
| final LinearRing exterior = factory.createLinearRing(lines.getGeometryN(0).getCoordinates()); |
| final LinearRing[] interiors = new LinearRing[lines.getNumGeometries() - 1]; |
| for (int i=0; i < interiors.length;) { |
| interiors[i] = factory.createLinearRing(lines.getGeometryN(++i).getCoordinates()); |
| } |
| return factory.createPolygon(exterior, interiors); |
| } |
| if (isCollection(geometry)) break; |
| return factory.createPolygon(geometry.getCoordinates()); |
| } |
| case MULTIPOINT: { |
| return (geometry instanceof Point) |
| ? factory.createMultiPoint(new Point[] {(Point) geometry}) |
| : factory.createMultiPointFromCoords(geometry.getCoordinates()); |
| } |
| case MULTILINESTRING: { |
| return toCollection(factory, |
| LineString.class, LineString[]::new, |
| GeometryFactory::createLineString, |
| GeometryFactory::createMultiLineString); |
| } |
| case MULTIPOLYGON: { |
| return toCollection(factory, |
| Polygon.class, Polygon[]::new, |
| GeometryFactory::createPolygon, |
| GeometryFactory::createMultiPolygon); |
| } |
| case GEOMETRYCOLLECTION: { |
| if (geometry instanceof Point) { |
| return factory.createMultiPoint(new Point[] {(Point) geometry}); |
| } else if (geometry instanceof LineString) { |
| return factory.createMultiLineString(new LineString[] {(LineString) geometry}); |
| } else if (geometry instanceof Polygon) { |
| return factory.createMultiPolygon(new Polygon[] {(Polygon) geometry}); |
| } |
| break; |
| } |
| } |
| throw new UnconvertibleObjectException(Errors.format(Errors.Keys.CanNotConvertFromType_2, |
| geometry.getClass(), getGeometryClass(target))); |
| } |
| |
| /** |
| * Converts a single geometry or a geometry collection to a collection of another type. |
| * This is a helper method for {@link #toGeometryType(GeometryType)}. |
| * |
| * @param <T> the compile-time value of {@code type}. |
| * @param factory the factory to use for creating new geometries. |
| * @param type the type of geometry components to put in a collection. |
| * @param newArray constructor for a new array of given {@code type}. |
| * @param newComponent constructor for a geometry component of given {@code type}. |
| * @param newCollection constructor for a geometry collection from an array of components/ |
| * @return the geometry collection created from the given type. |
| * @throws IllegalArgumentException if a geometry collection contains nested collection. |
| */ |
| private <T extends Geometry> GeometryCollection toCollection( |
| final GeometryFactory factory, |
| final Class<T> type, final IntFunction<T[]> newArray, |
| final BiFunction<GeometryFactory,Coordinate[],T> newComponent, |
| final BiFunction<GeometryFactory,T[],GeometryCollection> newCollection) |
| { |
| final T[] components = newArray.apply(geometry.getNumGeometries()); |
| for (int i=0; i<components.length; i++) { |
| final Geometry c = geometry.getGeometryN(i); |
| if (type.isInstance(c)) { |
| components[i] = type.cast(c); |
| } else if (isCollection(c)) { |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.NestedElementNotAllowed_1, GeometryCollection.class)); |
| } else { |
| components[i] = newComponent.apply(factory, c.getCoordinates()); |
| } |
| } |
| return newCollection.apply(factory, components); |
| } |
| |
| /** |
| * Returns {@code true} if the given geometry is a collection other than {@link MultiPoint}. |
| * Collections are handled recursively by {@code getLineStrings(…)} and {@code getPolygons(…)}. |
| */ |
| private static boolean isCollection(final Geometry geometry) { |
| return (geometry.getNumGeometries() >= 2) && !(geometry instanceof MultiPoint); |
| } |
| |
| /** |
| * Transforms this geometry using the given coordinate operation. |
| * If the operation is {@code null}, then the geometry is returned unchanged. |
| * If the geometry uses a different CRS than the source CRS of the given operation |
| * and {@code validate} is {@code true}, |
| * then a new operation to the target CRS will be automatically computed. |
| * |
| * @param operation the coordinate operation to apply, or {@code null}. |
| * @param validate whether to validate the operation source CRS. |
| * @throws FactoryException if transformation to the target CRS cannot be found. |
| * @throws TransformException if the geometry cannot be transformed. |
| */ |
| @Override |
| public GeometryWrapper transform(final CoordinateOperation operation, final boolean validate) |
| throws FactoryException, TransformException |
| { |
| return rewrap(JTS.transform(geometry, operation, validate)); |
| } |
| |
| /** |
| * Transforms this geometry to the specified Coordinate Reference System (CRS). |
| * If the given CRS is null or is the same CRS as current one, the geometry is returned unchanged. |
| * If the geometry has no Coordinate Reference System, then the geometry is returned unchanged. |
| * |
| * @param targetCRS the target coordinate reference system, or {@code null}. |
| * @return the transformed geometry (may be the same geometry instance), or {@code null}. |
| * @throws TransformException if this geometry cannot be transformed. |
| */ |
| @Override |
| public GeometryWrapper transform(final CoordinateReferenceSystem targetCRS) throws TransformException { |
| try { |
| return rewrap(JTS.transform(geometry, targetCRS)); |
| } catch (FactoryException e) { |
| /* |
| * We wrap that exception because `Geometry.transform(…)` does not declare `FactoryException`. |
| * We may revisit in a future version if `Geometry.transform(…)` method declaration is updated. |
| */ |
| throw new TransformException(e); |
| } |
| } |
| |
| /** |
| * Transforms this geometry using the given transform. |
| * If the transform is {@code null}, then the geometry is returned unchanged. |
| * Otherwise, a new geometry is returned without CRS. |
| * |
| * @param transform the math transform to apply, or {@code null}. |
| * @return the transformed geometry (may be the same geometry instance, but never {@code null}). |
| * @throws TransformException if the geometry cannot be transformed. |
| */ |
| @Override |
| public GeometryWrapper transform(final MathTransform transform) throws TransformException { |
| return rewrap(JTS.transform(geometry, transform)); |
| } |
| |
| /** |
| * Returns a view over the JTS geometry as a Java2D shape. Changes in the JTS geometry |
| * after this method call may be reflected in the returned shape in an unspecified way. |
| * |
| * @return a view over the geometry as a Java2D shape. |
| */ |
| @Override |
| public Shape toJava2D() { |
| return JTS.asShape(geometry); |
| } |
| |
| /** |
| * Returns {@code true} if the given geometry use the same CRS as this geometry, or conservatively |
| * returns {@code false} in case of doubt. This method should perform only a cheap test; it is used |
| * as a way to filter rapidly if {@link #transform(CoordinateReferenceSystem)} needs to be invoked. |
| * |
| * @param other the second geometry. |
| * @return {@code true} if the two geometries use equivalent CRS or if the CRS is undefined on both side, |
| * or {@code false} in case of doubt. |
| * @throws ClassCastException if the given wrapper is not for the same geometry library. |
| */ |
| @Override |
| public boolean isSameCRS(final GeometryWrapper other) { |
| return JTS.isSameCRS(geometry, ((Wrapper) other).geometry); |
| } |
| |
| /** |
| * Returns the WKT representation of the wrapped geometry. |
| */ |
| @Override |
| public String formatWKT(final double flatness) { |
| return geometry.toText(); |
| } |
| } |