This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 333c86323a51a5e419a67be43d4fe391071c2249 Author: jsorel <[email protected]> AuthorDate: Thu Oct 8 11:13:04 2026 +0200 feat(Geometry): move SIS wrapper to adapter package --- .../org/apache/sis/geometries/GeometryFactory.java | 156 +---- .../main/org/apache/sis/geometries/Wrapper.java | 337 --------- .../org/apache/sis/geometries/adapter/SIS.java | 638 +++++++++++++++++ .../org/apache/sis/geometries/adapter/Wrapper.java | 755 +++++++++++++++++++++ 4 files changed, 1403 insertions(+), 483 deletions(-) diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java index 65d817d631..7326b09e72 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java @@ -16,9 +16,8 @@ */ package org.apache.sis.geometries; -import java.nio.ByteBuffer; -import java.nio.DoubleBuffer; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.measure.Quantity; @@ -114,30 +113,23 @@ import org.apache.sis.geometries.surface.Polygon; import org.apache.sis.geometries.surface.PolyhedralSurface; import org.apache.sis.geometries.surface.TIN; import org.apache.sis.geometries.surface.Triangle; -import org.apache.sis.geometry.wrapper.Capability; -import org.apache.sis.geometry.wrapper.Dimensions; -import org.apache.sis.geometry.wrapper.GeometryType; -import org.apache.sis.geometry.wrapper.GeometryWrapper; import org.apache.sis.maths.Array; import org.apache.sis.maths.DataType; -import org.apache.sis.maths.NDArrays; import org.apache.sis.maths.SampleSystem; import org.apache.sis.maths.Vector; import org.apache.sis.measure.NumberRange; import org.apache.sis.measure.Range; -import org.apache.sis.setup.GeometryLibrary; /** * * @author Johann Sorel (Geomatys) */ -public final class GeometryFactory extends org.apache.sis.geometry.wrapper.Geometries<Geometry> { +public final class GeometryFactory { public static GeometryFactory INSTANCE = new GeometryFactory(); private GeometryFactory(){ - super(GeometryLibrary.SIS, Geometry.class, Point.class); } public static Empty createEmpty(CoordinateReferenceSystem crs) { @@ -656,142 +648,14 @@ public final class GeometryFactory extends org.apache.sis.geometry.wrapper.Geome return new ArrayDataPoints(attributes); } - // //////////////////////////////////////////////////////////////////////// - // org.apache.sis.geometry.wrapper.Geometries methods ///////////////////// - // //////////////////////////////////////////////////////////////////////// - - - @Override - public Class<?> getGeometryClass(GeometryType type) { - switch (type) { - case CIRCLE : return Circle.class; - case CIRCULARSTRING : return CircularString.class; - case CLOTHOID : return Clothoid.class; - case COMPOUNDCURVE : return CompoundCurve.class; - case CURVE : return Curve.class; - case CURVEPOLYGON : return CurvePolygon.class; - case GEOMETRY : return Geometry.class; - case GEOMETRYCOLLECTION : return GeometryCollection.class; - case LINESTRING : return LineString.class; - case MULTICURVE : return MultiCurve.class; - case MULTILINESTRING : return MultiLineString.class; - case MULTIPOINT : return MultiPoint.class; - case MULTIPOLYGON : return MultiPolygon.class; - case MULTISURFACE : return MultiSurface.class; - case POINT : return Point.class; - case POLYGON : return Polygon.class; - case POLYHEDRALSURFACE : return PolyhedralSurface.class; - case SURFACE : return Surface.class; - case TIN : return TIN.class; - case TRIANGLE : return Triangle.class; - //todo - case BREPSOLID : - case COMPOUNDSURFACE : - case ELLIPTICALCURVE : - case GEODESICSTRING : - case NURBSCURVE : - case SPIRALCURVE : - default: return Geometry.class; - } - } - - @Override - public GeometryType getGeometryType(Class<?> type) { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public GeometryWrapper castOrWrap(Object geometry) { - if (geometry instanceof Wrapper) return (GeometryWrapper) geometry; - return new Wrapper((Geometry) geometry); - } - - @Override - public GeometryWrapper parseWKT(String wkt) throws Exception { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public GeometryWrapper parseWKB(ByteBuffer data) throws Exception { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public boolean supports(Capability feature) { - switch (feature) { - case Z_COORDINATE : return true; - case M_COORDINATE : return true; - case SINGLE_PRECISION : return true; - default: return false; - } - } - - @Override - public Point createPoint(double x, double y) { - return new DefaultPoint(SampleSystem.ofSize(2), x, y); - } - - @Override - public Point createPoint(double x, double y, double z) { - return new DefaultPoint(SampleSystem.ofSize(3), x ,y, z); - } - - @Override - public Point createPoint(boolean isFloat, Dimensions dimensions, DoubleBuffer coordinates) { - final ArrayDataPoints points; - - if (!dimensions.hasZ) { - final SampleSystem ss = SampleSystem.ofSize(2); - if (isFloat) { - points = new ArrayDataPoints(NDArrays.of(ss, (float) coordinates.get(0), (float) coordinates.get(1))); - } else { - points = new ArrayDataPoints(NDArrays.of(ss, coordinates.get(0), coordinates.get(1))); - } - } else { - final SampleSystem ss = SampleSystem.ofSize(3); - if (isFloat) { - points = new ArrayDataPoints(NDArrays.of(ss, (float) coordinates.get(0), (float) coordinates.get(1), (float) coordinates.get(2))); - } else { - points = new ArrayDataPoints(NDArrays.of(ss, coordinates.get(0), coordinates.get(1), coordinates.get(2))); - } - } - - if (dimensions.hasM) { - final Array marray; - if (isFloat) { - marray = NDArrays.of(SampleSystem.ofSize(1), (float) coordinates.get(dimensions.hasZ ? 3 : 2)); - } else { - marray = NDArrays.of(SampleSystem.ofSize(1), coordinates.get(dimensions.hasZ ? 3 : 2)); - } - points.setAttribute(DataPointsType.ATT_M, marray); + /** + * Returns a copy of the given DataPoints. + */ + public static DataPoints copy(final DataPoints source) { + final Map<String,Array> attributes = new LinkedHashMap<>(); + for (final String name : source.getType().getAttributeNames()) { + attributes.put(name, source.getAttributeArray(name)); } - - return new DefaultPoint(points); - } - - @Override - public MultiPoint<?> createMultiPoint(boolean isFloat, Dimensions dimensions, DoubleBuffer coordinates) { - throw new UnsupportedOperationException("Not supported yet."); + return GeometryFactory.createSequence(attributes); } - - @Override - public Geometry createPolyline(boolean polygon, boolean isFloat, Dimensions dimensions, DoubleBuffer... coordinates) { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public GeometryWrapper createMultiPolygon(Object[] geometries) { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public GeometryWrapper createFromComponents(GeometryType type, Object components) { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - protected GeometryWrapper createWrapper(Geometry geometry) { - return new Wrapper(geometry); - } - } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Wrapper.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Wrapper.java deleted file mode 100644 index f43b92ea4d..0000000000 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Wrapper.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * 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.geometries; - -import java.awt.Shape; -import java.util.Iterator; -import java.util.OptionalInt; -import org.opengis.filter.DistanceOperatorName; -import org.opengis.filter.SpatialOperatorName; -import org.opengis.geometry.DirectPosition; -import org.opengis.referencing.crs.CoordinateReferenceSystem; -import org.opengis.referencing.operation.CoordinateOperation; -import org.opengis.referencing.operation.MathTransform; -import org.opengis.referencing.operation.TransformException; -import org.opengis.util.FactoryException; -import org.apache.sis.filter.sqlmm.SQLMM; -import org.apache.sis.geometries.adapter.JTSAdapter; -import org.apache.sis.geometries.curve.LineString; -import org.apache.sis.geometries.curve.LinearRing; -import org.apache.sis.geometries.curve.MultiLineString; -import org.apache.sis.geometries.surface.Polygon; -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.Debug; - - -/** - * The wrapper of SIS geometries. - * - * @author Johann Sorel (Geomatys) - */ -public final class Wrapper extends GeometryWrapper { - /** - * The wrapped implementation. - */ - private final Geometry geometry; - - /** - * Creates a new wrapper around the given geometry. - * - * @param geometry the geometry to wrap. - */ - Wrapper(final Geometry geometry) { - this.geometry = geometry; - crs = geometry.getCoordinateReferenceSystem(); - } - - /** - * Creates a new wrapper with the same <abbr>CRS</abbr> than the given wrapper. - * - * @param source the source wrapper from which is derived the geometry. - * @param geometry the geometry to wrap. - */ - private Wrapper(final Wrapper source, final Geometry geometry) { - this.geometry = geometry; - this.crs = source.crs; - } - - /** - * Returns the implementation-dependent factory of geometric object. - */ - @Override - protected Geometries<Geometry> factory() { - return GeometryFactory.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() { - return OptionalInt.empty(); - } - - /** - * 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) { - super.setCoordinateReferenceSystem(crs); - geometry.setCoordinateReferenceSystem(crs); - } - - /** - * Returns the dimension of the coordinates that define this geometry. - */ - @Override - public int getCoordinateDimension() { - return getCoordinatesDimension(geometry); - } - - /** - * Gets the number of dimensions of geometry vertex (sequence of coordinate tuples), which can be 2 or 3. - * - * @param geometry the geometry for which to get <em>vertex</em> (not topological) dimension. - * @return vertex dimension of the given geometry. - */ - private static int getCoordinatesDimension(final Geometry geometry) { - return geometry.getCoordinateReferenceSystem().getCoordinateSystem().getDimension(); - } - - /** - * Returns the envelope of SIS geometry. Never null, but may be empty. - */ - @Override - public GeneralEnvelope getEnvelope() { - return new GeneralEnvelope(geometry.getEnvelope()); - } - - /** - * Returns the centroid of the wrapped geometry as a direct position. - */ - @Override - public DirectPosition getCentroid() { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * 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() { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * Returns all coordinate tuples in the wrapped geometry. - * This method is currently used for testing purpose only. - */ - @Debug - @Override - public double[] getAllCoordinates() { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * Merges a sequence of points or paths after the wrapped geometry. - * - * @throws ClassCastException if an element in the iterator is not a SIS geometry. - */ - @Override - public Geometry mergePolylines(final Iterator<?> polylines) { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * 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> No operations are supported at this time.</p> - * - * @throws ClassCastException if the given wrapper is not for the same geometry library. - */ - @Override - protected boolean predicateSameCRS(final SpatialOperatorName type, final GeometryWrapper other) { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * 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) - { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * 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) { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * 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.</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) { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * 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 { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * 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. - * - * @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 { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * Transforms this geometry using the given transform. - * If the transform is {@code null}, then the geometry is returned unchanged. - * - * @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 FactoryException, TransformException { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * Returns a view over the SIS geometry as a Java2D shape. Changes in the SIS 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() { - throw new UnsupportedOperationException("Not supported yet."); - } - - /** - * Returns the WKT representation of the wrapped geometry. - */ - @Override - public String formatWKT(final double flatness) { - return geometry.asText(); - } - - /** - * View SIS Geometry as a JTS Geometry. - * Only the matching JTS geometry types are supported. - * The created geometry references the original geometry DataPoints, so modifications - * are forwarded to the original but all metadata change, like the CRS, will not be preserved if changed - * after the JTS view has been made. - * - * @param gf optional creation factory. - * @return JTS geometry view of the given geometry - */ - public org.locationtech.jts.geom.Geometry asJTS(org.locationtech.jts.geom.GeometryFactory gf) { - return asJTS(geometry, gf); - } - - /** - * View SIS Geometry as a JTS Geometry. - * Only the matching JTS geometry types are supported. - * The created geometry references the original geometry DataPoints, so modifications - * are forwarded to the original but all metadata change, like the CRS, will not be preserved if changed - * after the JTS view has been made. - * - * @param geometry to convert - * @param gf optional creation factory. - * @return JTS geometry view of the given geometry - */ - public static org.locationtech.jts.geom.Geometry asJTS(Geometry geometry, org.locationtech.jts.geom.GeometryFactory gf) { - if (gf == null) gf = new org.locationtech.jts.geom.GeometryFactory(); - return JTSAdapter.asJTS(geometry, false, gf); - } -} diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/SIS.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/SIS.java new file mode 100644 index 0000000000..ba3e0ffb41 --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/SIS.java @@ -0,0 +1,638 @@ +/* + * 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.geometries.adapter; + +import java.nio.ByteBuffer; +import java.nio.DoubleBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.geometries.Curve; +import org.apache.sis.geometries.DataPoints; +import org.apache.sis.geometries.DataPointsType; +import org.apache.sis.geometries.Geometry; +import org.apache.sis.geometries.GeometryCollection; +import org.apache.sis.geometries.GeometryFactory; +import org.apache.sis.geometries.Point; +import org.apache.sis.geometries.Surface; +import org.apache.sis.geometries.curve.Circle; +import org.apache.sis.geometries.curve.CircularString; +import org.apache.sis.geometries.curve.Clothoid; +import org.apache.sis.geometries.curve.CompoundCurve; +import org.apache.sis.geometries.curve.LineString; +import org.apache.sis.geometries.curve.LinearRing; +import org.apache.sis.geometries.curve.MultiCurve; +import org.apache.sis.geometries.curve.MultiLineString; +import org.apache.sis.geometries.internal.shared.ArrayDataPoints; +import org.apache.sis.geometries.internal.shared.DefaultPoint; +import org.apache.sis.geometries.point.MultiPoint; +import org.apache.sis.geometries.surface.CurvePolygon; +import org.apache.sis.geometries.surface.MultiPolygon; +import org.apache.sis.geometries.surface.MultiSurface; +import org.apache.sis.geometries.surface.Polygon; +import org.apache.sis.geometries.surface.PolyhedralSurface; +import org.apache.sis.geometries.surface.TIN; +import org.apache.sis.geometries.surface.Triangle; +import org.apache.sis.geometry.wrapper.Capability; +import org.apache.sis.geometry.wrapper.Dimensions; +import org.apache.sis.geometry.wrapper.GeometryType; +import org.apache.sis.geometry.wrapper.GeometryWrapper; +import org.apache.sis.maths.Array; +import org.apache.sis.maths.DataType; +import org.apache.sis.maths.NDArrays; +import org.apache.sis.maths.SampleSystem; +import org.apache.sis.maths.Tuple; +import org.apache.sis.setup.GeometryLibrary; +import org.apache.sis.util.Classes; +import org.apache.sis.util.internal.shared.Strings; +import org.apache.sis.util.resources.Errors; + + +/** + * SIS Geometry factory wrapper. + * Allows to do common operations on any geometry library. + * + * @author Johann Sorel (Geomatys) + */ +public final class SIS extends org.apache.sis.geometry.wrapper.Geometries<Geometry> { + + public static SIS INSTANCE = new SIS(); + + /** + * The geometry classes tested by {@link #getGeometryType(Class)}, in the order they are tested. + * A specialized class is listed before the class it specializes, so that the most specific type + * is the one which is reported. This is the converse of {@link #getGeometryClass(GeometryType)}, + * therefore both lists shall stay in sync. + * + * @see #TYPES + */ + private static final Class<?>[] CLASSES = { + Point.class, + MultiPoint.class, + Circle.class, + CircularString.class, + Clothoid.class, + CompoundCurve.class, + LineString.class, + MultiLineString.class, + MultiCurve.class, + Triangle.class, + Polygon.class, + CurvePolygon.class, + TIN.class, + PolyhedralSurface.class, + MultiPolygon.class, + MultiSurface.class, + GeometryCollection.class, + Curve.class, + Surface.class, + Geometry.class + }; + + /** + * The implementation-neutral types of the classes listed in the {@link #CLASSES} array. + */ + private static final GeometryType[] TYPES = { + GeometryType.POINT, + GeometryType.MULTIPOINT, + GeometryType.CIRCLE, + GeometryType.CIRCULARSTRING, + GeometryType.CLOTHOID, + GeometryType.COMPOUNDCURVE, + GeometryType.LINESTRING, + GeometryType.MULTILINESTRING, + GeometryType.MULTICURVE, + GeometryType.TRIANGLE, + GeometryType.POLYGON, + GeometryType.CURVEPOLYGON, + GeometryType.TIN, + GeometryType.POLYHEDRALSURFACE, + GeometryType.MULTIPOLYGON, + GeometryType.MULTISURFACE, + GeometryType.GEOMETRYCOLLECTION, + GeometryType.CURVE, + GeometryType.SURFACE, + GeometryType.GEOMETRY + }; + + private SIS(){ + super(GeometryLibrary.SIS, Geometry.class, Point.class); + } + + @Override + public Class<?> getGeometryClass(GeometryType type) { + switch (type) { + case CIRCLE : return Circle.class; + case CIRCULARSTRING : return CircularString.class; + case CLOTHOID : return Clothoid.class; + case COMPOUNDCURVE : return CompoundCurve.class; + case CURVE : return Curve.class; + case CURVEPOLYGON : return CurvePolygon.class; + case GEOMETRY : return Geometry.class; + case GEOMETRYCOLLECTION : return GeometryCollection.class; + case LINESTRING : return LineString.class; + case MULTICURVE : return MultiCurve.class; + case MULTILINESTRING : return MultiLineString.class; + case MULTIPOINT : return MultiPoint.class; + case MULTIPOLYGON : return MultiPolygon.class; + case MULTISURFACE : return MultiSurface.class; + case POINT : return Point.class; + case POLYGON : return Polygon.class; + case POLYHEDRALSURFACE : return PolyhedralSurface.class; + case SURFACE : return Surface.class; + case TIN : return TIN.class; + case TRIANGLE : return Triangle.class; + //todo + case BREPSOLID : + case COMPOUNDSURFACE : + case ELLIPTICALCURVE : + case GEODESICSTRING : + case NURBSCURVE : + case SPIRALCURVE : + default: return Geometry.class; + } + } + + /** + * Returns the implementation-neutral type of the given geometry class. If the given class is an + * array, then the collection type containing that kind of geometry is returned when there is one. + * + * @param type class of geometry for which the implementation-neutral type is desired. + * @return implementation-neutral type of the given class, or {@link GeometryType#GEOMETRY} if unknown. + */ + @Override + public GeometryType getGeometryType(final Class<?> type) { + Class<?> component = type.getComponentType(); + final boolean isArray = (component != null); + if (!isArray) { + component = type; + } + for (int i=0; i < CLASSES.length; i++) { + if (CLASSES[i].isAssignableFrom(component)) { + final GeometryType found = TYPES[i]; + if (isArray && !found.isCollection) { + final GeometryType collection = found.collection(); + if (collection != null) { + return collection; + } + } + return found; + } + } + return GeometryType.GEOMETRY; + } + + @Override + public GeometryWrapper castOrWrap(Object geometry) { + if (geometry == null || geometry instanceof Wrapper) return (Wrapper) geometry; + return new Wrapper((Geometry) geometry); + } + + /** + * Parses the given Well-Known Text. The text may be in the {@code EWKT} flavor, in which case + * the coordinate reference system it names is used. Otherwise an + * {@linkplain org.apache.sis.geometries.Geometries#getUndefinedCRS(int) undefined} system of + * as many dimensions as the text has ordinates per position is assigned to the geometry. + * + * @param wkt the Well-Known Text to parse. + * @return the geometry described by the given text. + * @throws IllegalArgumentException if the text is malformed or names an unsupported geometry type. + */ + @Override + public GeometryWrapper parseWKT(final String wkt) { + return new Wrapper(new WellKnownText().decode(wkt)); + } + + /** + * Reads the given bytes as a Well-Known Binary encoded geometry. The bytes may be in the + * {@code EWKB} flavor, in which case the coordinate reference system they name is used. + * This implementation does not change the buffer position when the buffer is backed by + * an accessible array. + * + * @param data the sequence of bytes to parse. + * @return the geometry described by the given bytes. + * @throws IllegalArgumentException if the bytes are malformed or name an unsupported geometry type. + */ + @Override + public GeometryWrapper parseWKB(final ByteBuffer data) { + byte[] array; + if (data.hasArray()) { + /* + * Try to use the underlying array without copy if possible. + * Copy only if the position or length does not match. + */ + array = data.array(); + int lower = data.arrayOffset(); + int upper = data.limit() + lower; + lower += data.position(); + if (lower != 0 || upper != array.length) { + array = Arrays.copyOfRange(array, lower, upper); + } + } else { + array = new byte[data.remaining()]; + data.get(array); + } + return new Wrapper(new WellKnownBinary().decode(array)); + } + + @Override + public boolean supports(Capability feature) { + switch (feature) { + case Z_COORDINATE : return true; + case M_COORDINATE : return true; + case SINGLE_PRECISION : return true; + default: return false; + } + } + + @Override + public Point createPoint(double x, double y) { + return new DefaultPoint(undefinedSystem(BIDIMENSIONAL), x, y); + } + + @Override + public Point createPoint(double x, double y, double z) { + return new DefaultPoint(undefinedSystem(TRIDIMENSIONAL), x ,y, z); + } + + @Override + public Point createPoint(boolean isFloat, Dimensions dimensions, DoubleBuffer coordinates) { + return new DefaultPoint(readDataPoints(isFloat, dimensions, 1, coordinates)); + } + + /** + * Creates a collection of points from the given coordinate values. + * The buffer position is advanced by {@code dimensions.count} × the number of points. + * + * @param isFloat whether to cast and store numbers to single-precision. + * @param dimensions the dimensions of the coordinate tuples. + * @param coordinates sequence of (x,y), (x,y,z), (x,y,m) or (x,y,z,m) coordinate tuples. + * @return the collection of points for the given coordinate values. + */ + @Override + public MultiPoint<?> createMultiPoint(final boolean isFloat, final Dimensions dimensions, final DoubleBuffer coordinates) { + final int count = coordinates.remaining() / dimensions.count; + return GeometryFactory.createMultiPoint(readDataPoints(isFloat, dimensions, count, coordinates)); + } + + /** + * Creates a polyline or a polygon from the given coordinate values. Each coordinate tuple having + * a {@link Double#NaN} <var>x</var> or <var>y</var> coordinate starts a new path, and a path of + * less than two positions is dropped. If a single path remains, it is returned directly instead + * of being wrapped in a collection. + * + * @param polygon whether to return the paths as polygons instead of polylines. + * @param isFloat whether to cast and store numbers to single-precision. + * @param dimensions the dimensions of the coordinate tuples. + * @param coordinates sequence of (x,y), (x,y,z), (x,y,m) or (x,y,z,m) coordinate tuples. + * @return the geometry for the given coordinate values. + * @throws IllegalArgumentException if a polygon was requested but a path is not a closed ring. + */ + @Override + public Geometry createPolyline(final boolean polygon, final boolean isFloat, + final Dimensions dimensions, final DoubleBuffer... coordinates) + { + final int spatial = spatialDimension(dimensions); + final var paths = new ArrayList<Geometry>(); + final var path = new ArrayList<double[]>(); + for (final DoubleBuffer buffer : coordinates) { + if (buffer == null) { + continue; + } + while (buffer.remaining() >= dimensions.count) { + final var tuple = new double[dimensions.count]; + buffer.get(tuple); + if (Double.isNaN(tuple[0]) || Double.isNaN(tuple[1])) { + addPath(paths, path, polygon, isFloat, dimensions, spatial); + } else { + path.add(tuple); + } + } + } + addPath(paths, path, polygon, isFloat, dimensions, spatial); + switch (paths.size()) { + case 0: return GeometryFactory.createEmpty(undefinedCRS(spatial)); + case 1: return paths.get(0); + default: { + // An ArrayStoreException here would be a bug in our use of the `polygon` flag. + return polygon ? GeometryFactory.createMultiPolygon(paths.toArray(Polygon[]::new)) + : GeometryFactory.createMultiLineString(paths.toArray(LineString[]::new)); + } + } + } + + /** + * Makes a line string or a polygon from the given positions, adds it to the given list, then + * clears the positions. Less than two positions cannot make a path, in which case they are + * dropped silently, as the libraries wrapped by the other implementations do. + * + * @param addTo where to add the created geometry, if such geometry is created. + * @param path positions of the geometry to create. This list is cleared by this method. + * @param polygon whether to create a polygon instead of a line string. + * @param isFloat whether to store the coordinates as single-precision numbers. + * @param dimensions the dimensions of the coordinate tuples. + * @param spatial the number of spatial dimensions of the coordinate tuples. + * @throws IllegalArgumentException if a polygon is requested but the path is not a closed ring. + */ + private static void addPath(final List<Geometry> addTo, final List<double[]> path, final boolean polygon, + final boolean isFloat, final Dimensions dimensions, final int spatial) + { + final int size = path.size(); + if (size >= 2) { + if (polygon && !Arrays.equals(path.get(0), 0, spatial, path.get(size-1), 0, spatial)) { + throw new IllegalArgumentException("Coordinates of a polygon shall make a closed ring."); + } + final DataPoints points = toDataPoints(path, isFloat, dimensions, spatial); + addTo.add(polygon ? GeometryFactory.createPolygon(GeometryFactory.createLinearRing(points), List.of()) + : GeometryFactory.createLineString(points)); + } + path.clear(); + } + + /** + * Creates a multi-polygon from an array of polygons, rings or line strings. + * The line strings which are not already rings are converted to rings. + * + * @param geometries the polygons, rings or line strings to put in a multi-polygon. + * @return the multi-polygon. + * @throws ClassCastException if an element of the array is neither a polygon nor a curve. + */ + @Override + public GeometryWrapper createMultiPolygon(final Object[] geometries) { + final var polygons = new Polygon[geometries.length]; + for (int i=0; i < geometries.length; i++) { + final Object component = implementation(geometries[i]); + if (component instanceof Polygon polygon) { + polygons[i] = polygon; + } else if (component instanceof LineString line) { + polygons[i] = GeometryFactory.createPolygon(toRing(line), List.of()); + } else { + throw new ClassCastException(Errors.format(Errors.Keys.IllegalArgumentClass_3, + Strings.bracket("geometries", i), Polygon.class, Classes.getClass(component))); + } + } + return new Wrapper(GeometryFactory.createMultiPolygon(polygons)); + } + + /** + * Creates a geometry from components. + * The expected {@code components} type depends on the target geometry type: + * <ul> + * <li>If {@code type} is a multi-geometry, then the components shall be a {@link Point}[], + * {@link Geometry}[], {@link LineString}[] or {@link Polygon}[] array, depending on the + * desired target type.</li> + * <li>Otherwise, if {@code type} is {@link GeometryType#POLYGON}, then the components shall be + * a {@link LineString}[] with the first ring taken as the shell and all other rings as holes.</li> + * <li>Otherwise, the components shall be a {@link DataPoints} sequence, an {@link Array} of + * positions, or an array or collection of {@link Point} or {@link Tuple} instances.</li> + * </ul> + * + * @param type type of geometry to create. + * @param components the components. Valid classes depend on the type of geometry to create. + * @return geometry built from the given components. + * @throws IllegalArgumentException if the given geometry type is not supported. + * @throws ArrayStoreException if {@code components} is an array with invalid component type. + */ + @Override + public GeometryWrapper createFromComponents(final GeometryType type, final Object components) { + final Geometry geometry; + switch (type) { + case POINT: { + final DataPoints points = toDataPoints(components); + geometry = (points.size() == 1) ? GeometryFactory.createPoint(points) + : GeometryFactory.createMultiPoint(points).getCentroid(); + break; + } + case LINESTRING: { + geometry = GeometryFactory.createLineString(toDataPoints(components)); + break; + } + case POLYGON: { + if (components instanceof LineString[] rings) { + if (rings.length == 0) { + // A polygon is defined by its exterior ring, which there is none to take here. + throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "components")); + } + LinearRing shell = null; + final var holes = new ArrayList<LinearRing>(rings.length - 1); + for (int i=0; i < rings.length; i++) { + final LinearRing ring = toRing(rings[i]); + if (i == 0) shell = ring; + else holes.add(ring); + } + geometry = GeometryFactory.createPolygon(shell, holes); + } else { + geometry = GeometryFactory.createPolygon( + GeometryFactory.createLinearRing(toDataPoints(components)), List.of()); + } + break; + } + case MULTIPOINT: { + geometry = (components instanceof Point[] points) + ? GeometryFactory.createMultiPoint(points) + : GeometryFactory.createMultiPoint(toDataPoints(components)); + break; + } + case MULTILINESTRING: { + geometry = GeometryFactory.createMultiLineString((LineString[]) components); + break; + } + case MULTIPOLYGON: { + geometry = GeometryFactory.createMultiPolygon((Polygon[]) components); + break; + } + case GEOMETRYCOLLECTION: { + geometry = GeometryFactory.createGeometryCollection((Geometry[]) components); + break; + } + case GEOMETRY: { + return createFromComponents(components); + } + default: { + throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedArgumentValue_1, type)); + } + } + return new Wrapper(geometry); + } + + @Override + protected GeometryWrapper createWrapper(Geometry geometry) { + return new Wrapper(geometry); + } + + // //////////////////////////////////////////////////////////////////////// + // Helper methods ///////////////////////////////////////////////////////// + // //////////////////////////////////////////////////////////////////////// + + /** + * Returns the number of dimensions of the <em>positions</em> of tuples having the given dimensions. + * The <var>m</var> coordinate is not one of them: it is stored as the + * {@value DataPointsType#ATT_M} attribute of the positions instead of as a coordinate. + */ + private static int spatialDimension(final Dimensions dimensions) { + return dimensions.hasZ ? TRIDIMENSIONAL : BIDIMENSIONAL; + } + + /** + * Returns the coordinate reference system to assign to coordinates given without one. + * The returned system cannot be converted to any other system, which makes the absence + * of a real system explicit instead of silently accepting an arbitrary one. + */ + private static CoordinateReferenceSystem undefinedCRS(final int dimension) { + return org.apache.sis.geometries.Geometries.getUndefinedCRS(dimension); + } + + /** + * Returns the sample system to assign to positions given without a coordinate reference system. + */ + private static SampleSystem undefinedSystem(final int dimension) { + return SampleSystem.of(undefinedCRS(dimension)); + } + + /** + * Reads the given number of coordinate tuples from the given buffer as a sequence of positions. + * The buffer position is advanced by {@code dimensions.count} × {@code count}. + * + * @param isFloat whether to store the coordinates as single-precision numbers. + * @param dimensions the dimensions of the coordinate tuples. + * @param count the number of coordinate tuples to read. + * @param coordinates the buffer to read. + * @return the positions read from the given buffer. + */ + private static ArrayDataPoints readDataPoints(final boolean isFloat, final Dimensions dimensions, + final int count, final DoubleBuffer coordinates) + { + final int spatial = spatialDimension(dimensions); + final var positions = new double[Math.multiplyExact(count, spatial)]; + final double[] measures = dimensions.hasM ? new double[count] : null; + for (int i=0, p=0; i<count; i++) { + for (int d=0; d<spatial; d++) { + positions[p++] = coordinates.get(); + } + if (measures != null) { + measures[i] = coordinates.get(); + } + } + return toDataPoints(isFloat, spatial, positions, measures); + } + + /** + * Returns the given coordinate tuples as a sequence of positions. + * + * @param tuples the coordinate tuples, each of {@code dimensions.count} values. + * @param isFloat whether to store the coordinates as single-precision numbers. + * @param dimensions the dimensions of the coordinate tuples. + * @param spatial the number of spatial dimensions of the coordinate tuples. + * @return the positions of the given coordinate tuples. + */ + private static ArrayDataPoints toDataPoints(final List<double[]> tuples, final boolean isFloat, + final Dimensions dimensions, final int spatial) + { + final int count = tuples.size(); + final var positions = new double[Math.multiplyExact(count, spatial)]; + final double[] measures = dimensions.hasM ? new double[count] : null; + for (int i=0, p=0; i<count; i++, p+=spatial) { + final double[] tuple = tuples.get(i); + System.arraycopy(tuple, 0, positions, p, spatial); + if (measures != null) { + measures[i] = tuple[spatial]; + } + } + return toDataPoints(isFloat, spatial, positions, measures); + } + + /** + * Returns the given coordinates as a sequence of positions carrying the measures, if any, + * as their {@value DataPointsType#ATT_M} attribute. + * + * @param isFloat whether to store the values as single-precision numbers. + * @param spatial the number of spatial dimensions of the positions. + * @param positions the spatial coordinates, as {@code spatial} values per position. + * @param measures one measure per position, or {@code null} if the positions have no measure. + * @return the sequence of the given positions. + */ + private static ArrayDataPoints toDataPoints(final boolean isFloat, final int spatial, + final double[] positions, final double[] measures) + { + final SampleSystem ss = undefinedSystem(spatial); + final var points = new ArrayDataPoints(isFloat ? NDArrays.of(ss, toFloat(positions)) + : NDArrays.of(ss, positions)); + if (measures != null) { + final SampleSystem ms = SampleSystem.ofSize(1); + points.setAttribute(DataPointsType.ATT_M, isFloat ? NDArrays.of(ms, toFloat(measures)) + : NDArrays.of(ms, measures)); + } + return points; + } + + /** + * Returns the given values cast to single-precision. + */ + private static float[] toFloat(final double[] values) { + final var copy = new float[values.length]; + for (int i=0; i<values.length; i++) { + copy[i] = (float) values[i]; + } + return copy; + } + + /** + * Returns the given components as a sequence of positions. The components can be a + * {@link DataPoints} sequence, an {@link Array} of positions, or an array or collection + * of {@link Point} or {@link Tuple} instances. + * + * @param components the components to read. + * @return the positions of the given components. + * @throws ClassCastException if the components are not of one of the expected kinds. + */ + private static DataPoints toDataPoints(final Object components) { + if (components instanceof DataPoints points) { + return points; + } + if (components instanceof Array positions) { + return GeometryFactory.createSequence(positions); + } + // The ClassCastException that may happen here is part of method contract. + final Collection<?> source = (components instanceof Collection<?> c) ? c : Arrays.asList((Object[]) components); + final var tuples = new ArrayList<Tuple<?>>(source.size()); + for (final Object element : source) { + final Object component = implementation(element); + // The ClassCastException that may happen here is part of method contract. + tuples.add((component instanceof Point point) ? point.getPosition() : (Tuple<?>) component); + } + if (tuples.isEmpty()) { + /* + * No position to take a sample system from. The number of dimensions is unknown, + * so the two-dimensional case is assumed, as everywhere else in the absence of + * a coordinate reference system. + */ + return GeometryFactory.createSequence(NDArrays.of(undefinedSystem(BIDIMENSIONAL), DataType.DOUBLE, 0)); + } + final Tuple<?> first = tuples.get(0); + return GeometryFactory.createSequence(NDArrays.of(tuples, first.getSampleSystem(), first.getDataType())); + } + + /** + * Returns the given curve as a ring, converting it if it is not already one. + * The ring shares the positions of the given curve, no copy is performed. + */ + private static LinearRing toRing(final LineString line) { + return (line instanceof LinearRing ring) ? ring : GeometryFactory.createLinearRing(line.getDataPoints()); + } +} diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java new file mode 100644 index 0000000000..b1ef4bb647 --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java @@ -0,0 +1,755 @@ +/* + * 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.geometries.adapter; + +import java.awt.Shape; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.OptionalInt; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.IntFunction; +import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; +import org.locationtech.jts.simplify.TopologyPreservingSimplifier; +import org.opengis.filter.DistanceOperatorName; +import org.opengis.filter.SpatialOperatorName; +import org.opengis.geometry.DirectPosition; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.CoordinateOperation; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.TransformException; +import org.opengis.util.FactoryException; +import org.apache.sis.filter.sqlmm.SQLMM; +import org.apache.sis.geometries.Curve; +import org.apache.sis.geometries.DE9IM; +import org.apache.sis.geometries.DataPoints; +import org.apache.sis.geometries.DataPointsType; +import org.apache.sis.geometries.Geometry; +import org.apache.sis.geometries.GeometryCollection; +import org.apache.sis.geometries.GeometryFactory; +import org.apache.sis.geometries.Point; +import org.apache.sis.geometries.Surface; +import org.apache.sis.geometries.curve.LineString; +import org.apache.sis.geometries.curve.LinearRing; +import org.apache.sis.geometries.curve.MultiLineString; +import org.apache.sis.geometries.operation.GeometryProcessor; +import org.apache.sis.geometries.point.MultiPoint; +import org.apache.sis.geometries.surface.MultiPolygon; +import org.apache.sis.geometries.surface.Polygon; +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.maths.Array; +import org.apache.sis.maths.NDArrays; +import org.apache.sis.maths.Tuple; +import org.apache.sis.maths.Vectors; +import org.apache.sis.measure.Quantities; +import org.apache.sis.measure.Units; +import org.apache.sis.referencing.CRS; +import org.apache.sis.referencing.operation.transform.MathTransforms; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.Debug; +import org.apache.sis.util.UnconvertibleObjectException; +import org.apache.sis.util.resources.Errors; + + +/** + * The wrapper of SIS geometries. + * + * @author Johann Sorel (Geomatys) + */ +public final class Wrapper extends GeometryWrapper { + + + /** + * The types of SIS geometries to be recognized by the SQLMM {@code ST_GeometryType} operation. + * The collections are tested before {@link GeometryCollection} itself, and the specialized + * geometries before the type they specialize. + */ + 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" + }; + + /** + * All predicates recognized by {@link #predicateSameCRS(SpatialOperatorName, GeometryWrapper)}. + * 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::equal; + 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; + } + + /** + * The wrapped implementation. + */ + private final Geometry geometry; + + /** + * Creates a new wrapper around the given geometry. + * + * @param geometry the geometry to wrap. + */ + Wrapper(final Geometry geometry) { + this.geometry = geometry; + crs = geometry.getCoordinateReferenceSystem(); + } + + /** + * Creates a new wrapper with the same <abbr>CRS</abbr> than the given wrapper. + * + * @param source the source wrapper from which is derived the geometry. + * @param geometry the geometry to wrap. + */ + private Wrapper(final Wrapper source, final Geometry geometry) { + this.geometry = geometry; + this.crs = source.crs; + } + + /** + * Returns the implementation-dependent factory of geometric object. + */ + @Override + protected Geometries<Geometry> factory() { + return SIS.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() { + return OptionalInt.empty(); + } + + /** + * 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) { + super.setCoordinateReferenceSystem(crs); + geometry.setCoordinateReferenceSystem(crs); + } + + /** + * Returns the dimension of the coordinates that define this geometry. + */ + @Override + public int getCoordinateDimension() { + return getCoordinatesDimension(geometry); + } + + /** + * Gets the number of dimensions of geometry vertex (sequence of coordinate tuples), which can be 2 or 3. + * + * @param geometry the geometry for which to get <em>vertex</em> (not topological) dimension. + * @return vertex dimension of the given geometry. + */ + private static int getCoordinatesDimension(final Geometry geometry) { + return geometry.getCoordinateReferenceSystem().getCoordinateSystem().getDimension(); + } + + /** + * Returns the envelope of SIS geometry. Never null, but may be empty. + */ + @Override + public GeneralEnvelope getEnvelope() { + return new GeneralEnvelope(geometry.getEnvelope()); + } + + /** + * Returns the centroid of the wrapped geometry as a direct position. + */ + @Override + public DirectPosition getCentroid() { + final Point centroid = geometry.getCentroid(); + if (centroid == null) { + // The centroid of an empty geometry is the empty geometry, which is no position. + return null; + } + return new GeneralDirectPosition(Vectors.asDirectPostion(centroid.getPosition())); + } + + /** + * 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 point)) { + return null; + } + return point.getPosition().toArrayDouble(); + } + + /** + * Returns all coordinate tuples in the wrapped geometry. + * This method is currently used for testing purpose only. + */ + @Debug + @Override + public double[] getAllCoordinates() { + final DataPoints points = geometry.getDataPoints(); + final int dimension = points.getDimension(); + final int size = points.size(); + final var coordinates = new double[Math.multiplyExact(size, dimension)]; + for (int i=0; i<size; i++) { + points.getPosition(i).toArrayDouble(coordinates, i * dimension); + } + return coordinates; + } + + /** + * Merges a sequence of points or paths after the wrapped geometry. + * + * @throws ClassCastException if an element in the iterator is not a SIS geometry. + */ + @Override + public Geometry mergePolylines(final Iterator<?> polylines) { + final var lines = new ArrayList<LineString>(); + final var coordinates = new ArrayList<Tuple<?>>(); +add: for (Geometry next = geometry;;) { + if (next instanceof Point point) { + final Tuple<?> position = point.getPosition(); + if (isDefined(position)) { + coordinates.add(position); + } else { + addPolyline(lines, coordinates); + } + } else if (next instanceof GeometryCollection<?> collection) { + for (int i=0, n=collection.getNumGeometries(); i<n; i++) { + addPolyline(lines, coordinates, (LineString) collection.getGeometryN(i)); + } + } else { + addPolyline(lines, coordinates, (LineString) next); + } + /* + * `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); + } + addPolyline(lines, coordinates); + switch (lines.size()) { + case 0: return GeometryFactory.createEmpty(crs); + case 1: return lines.get(0); + default: return GeometryFactory.createMultiLineString(crs, lines.toArray(LineString[]::new)); + } + } + + /** + * Returns whether all the coordinates of the given position are defined. A position having at + * least one {@link Double#NaN} coordinate separates two paths in {@link #mergePolylines(Iterator)}. + */ + private static boolean isDefined(final Tuple<?> position) { + for (int i=0, n=position.getDimension(); i<n; i++) { + if (Double.isNaN(position.get(i))) { + return false; + } + } + return true; + } + + /** + * Makes a line string from the given positions and adds it to the given list, then clears the + * positions. Less than two positions cannot make a line string, in which case they are dropped. + * + * @param lines where to add the line string, if such object is created. + * @param coordinates positions of the line string to create. This list is cleared by this method. + */ + private static void addPolyline(final List<LineString> lines, final List<Tuple<?>> coordinates) { + if (coordinates.size() >= 2) { + final Tuple<?> first = coordinates.get(0); + final Array positions = NDArrays.of(coordinates, first.getSampleSystem(), first.getDataType()); + lines.add(GeometryFactory.createLineString(GeometryFactory.createSequence(positions))); + } + coordinates.clear(); + } + + /** + * Appends the given line string to the polyline under construction. If no position has been + * accumulated yet, the given line string is added as-is. Otherwise its positions continue the + * accumulated ones and the whole makes a single line string. + * + * @param lines where to add the line strings. + * @param coordinates positions accumulated so far, cleared by this method if they are used. + * @param line the line string to append. + */ + private static void addPolyline(final List<LineString> lines, final List<Tuple<?>> coordinates, final LineString line) { + if (coordinates.isEmpty()) { + lines.add(line); + } else { + final DataPoints points = line.getDataPoints(); + for (int i=0, n=points.size(); i<n; i++) { + coordinates.add(points.getPosition(i)); + } + addPolyline(lines, coordinates); + } + } + + /** + * 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). + * + * @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); + } + + /** + * 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) + { + final boolean reverse = (type != DistanceOperatorName.WITHIN); + if (reverse && type != DistanceOperatorName.BEYOND) { + return super.predicateSameCRS(type, other, distance); + } + /* + * The distance is already expressed in the units of the coordinate system axes, + * which the dimensionless quantity tells the operation to take as-is. + */ + return geometry.withinDistance(((Wrapper) other).geometry, + Quantities.create(distance, Units.UNITY)) ^ 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) { + switch (operation) { + case ST_Dimension: return geometry.getTopologicDimension(); + case ST_CoordDim: return geometry.getDimension(); + case ST_Is3D: return geometry.is3D(); + case ST_SRID: return getSRID().orElse(0); + 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: return geometry.getBoundary(); + case ST_ConvexHull: return geometry.convexHull(); + case ST_Buffer: return geometry.buffer(((Number) argument).doubleValue()); + case ST_Intersection: return geometry.intersection (((Wrapper) other).geometry); + case ST_Union: return geometry.union (((Wrapper) other).geometry); + case ST_Difference: return geometry.difference (((Wrapper) other).geometry); + case ST_SymDifference: return geometry.symDifference(((Wrapper) other).geometry); + case ST_Distance: return geometry.distance (((Wrapper) other).geometry).getValue(); + case ST_Equals: return geometry.equal (((Wrapper) other).geometry); + 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_Relate: return geometry.relate(((Wrapper) other).geometry, DE9IM.valueOf(argument.toString())); + case ST_AsText: return geometry.asText(); + case ST_AsBinary: return geometry.asBinary(); + case ST_Centroid: return geometry.getCentroid(); + case ST_PointOnSurface: return geometry.getRepresentativePoint(); + case ST_X: return ((Point) geometry).getPosition().get(0); + case ST_Y: return ((Point) geometry).getPosition().get(1); + case ST_ExplicitPoint: return ((Point) geometry).getPosition().toArrayDouble(); + case ST_Length: return ((Curve) geometry).getLength().getValue(); + case ST_StartPoint: return ((Curve) geometry).getStartPoint(); + case ST_EndPoint: return ((Curve) geometry).getEndPoint(); + case ST_IsClosed: return ((Curve) geometry).isClosed(); + case ST_IsRing: return ((Curve) geometry).isRing(); + case ST_NumPoints: return ((LineString) geometry).getNumPoints(); + case ST_PointN: return ((LineString) geometry).getPointN(toIndex(argument)); + case ST_Area: return ((Surface) geometry).getArea().getValue(); + case ST_Perimeter: return ((Surface) geometry).getPerimeter().getValue(); + case ST_ExteriorRing: return ((Polygon) geometry).getExteriorRing(); + case ST_InteriorRingN: return ((Polygon) geometry).getInteriorRingN(toIndex(argument)); + case ST_NumInteriorRings: return ((Polygon) geometry).getNumInteriorRing(); + case ST_NumGeometries: return ((GeometryCollection<?>) geometry).getNumGeometries(); + case ST_GeometryN: return ((GeometryCollection<?>) geometry).getGeometryN(toIndex(argument)); + case ST_Z: { + // A geometry of a two-dimensional system has no z value to report. + final Tuple<?> position = ((Point) geometry).getPosition(); + return (position.getDimension() > Geometries.BIDIMENSIONAL) ? position.get(2) : Double.NaN; + } + case ST_IsMeasured: { + final DataPointsType type = geometry.getDataPointsType(); + return (type != null) && type.getAttributeNames().contains(DataPointsType.ATT_M); + } + case ST_GeometryType: { + for (int i=0; i < TYPES.length; i++) { + if (TYPES[i].isInstance(geometry)) { + return SQLMM_NAMES[i]; + } + } + return null; + } + case ST_ToLineString: + case ST_ToPoint: + case ST_ToPolygon: + case ST_ToMultiPoint: + case ST_ToMultiLine: + case ST_ToMultiPolygon: + case ST_ToGeomColl: { + final GeometryType target = operation.getGeometryType().get(); + if (factory().getGeometryClass(target).isInstance(geometry)) { + return geometry; + } + return convert(target); + } + case ST_Simplify: { + final double distance = ((Number) argument).doubleValue(); + return fromJTS(DouglasPeuckerSimplifier.simplify(asJTS(null), distance)); + } + case ST_SimplifyPreserveTopology: { + final double distance = ((Number) argument).doubleValue(); + return fromJTS(TopologyPreservingSimplifier.simplify(asJTS(null), distance)); + } + default: return super.operationSameCRS(operation, other, argument); + } + } + + /** + * 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.</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 (!factory().getGeometryClass(target).isInstance(geometry)) { + final Geometry result = convert(target); + if (result != geometry) { + return new Wrapper(this, result); + } + } + return this; + } + + /** + * Converts the wrapped geometry to the specified type without wrapper. + * This is the implementation of {@link #toGeometryType(GeometryType)}. + * + * @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) { + switch (target) { + case POINT: { + return geometry.getCentroid(); + } + case LINESTRING: { + if (isCollection(geometry)) break; + return GeometryFactory.createLineString(GeometryFactory.copy(geometry.getDataPoints())); + } + case POLYGON: { + if (!geometry.isEmpty() && geometry instanceof MultiLineString lines) { + // SQLMM `ST_BdMPolyFromText` and `ST_BdMPolyFromWKB` behavior. + final int count = lines.getNumGeometries(); + final LinearRing exterior = toRing(lines.getGeometryN(0)); + final var interiors = new ArrayList<LinearRing>(count - 1); + for (int i=1; i<count; i++) { + interiors.add(toRing(lines.getGeometryN(i))); + } + return GeometryFactory.createPolygon(exterior, interiors); + } + if (isCollection(geometry)) break; + return GeometryFactory.createPolygon(toRing(geometry), List.of()); + } + case MULTIPOINT: { + if (geometry instanceof Point point) { + return GeometryFactory.createMultiPoint(point); + } + return GeometryFactory.createMultiPoint(GeometryFactory.copy(geometry.getDataPoints())); + } + case MULTILINESTRING: { + return GeometryFactory.createMultiLineString(crs, + components(LineString.class, LineString[]::new, GeometryFactory::createLineString)); + } + case MULTIPOLYGON: { + return GeometryFactory.createMultiPolygon(crs, + components(Polygon.class, Polygon[]::new, + (points) -> GeometryFactory.createPolygon(GeometryFactory.createLinearRing(points), List.of()))); + } + case GEOMETRYCOLLECTION: { + if (geometry instanceof Point point) { + return GeometryFactory.createMultiPoint(point); + } else if (geometry instanceof LineString line) { + return GeometryFactory.createMultiLineString(line); + } else if (geometry instanceof Polygon polygon) { + return GeometryFactory.createMultiPolygon(polygon); + } + break; + } + } + throw new UnconvertibleObjectException(Errors.format(Errors.Keys.CanNotConvertFromType_2, + geometry.getClass(), factory().getGeometryClass(target))); + } + + /** + * Returns the components of the wrapped geometry as geometries of the given type, for building + * a collection of that type. A geometry which is not a collection provides a single component. + * + * @param <T> the compile-time value of {@code type}. + * @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}. + * @return the components to put in a geometry collection. + * @throws IllegalArgumentException if a geometry collection contains nested collection. + */ + private <T extends Geometry> T[] components(final Class<T> type, final IntFunction<T[]> newArray, + final Function<DataPoints,T> newComponent) + { + final GeometryCollection<?> source = (geometry instanceof GeometryCollection<?> c) ? c : null; + final T[] components = newArray.apply((source != null) ? source.getNumGeometries() : 1); + for (int i=0; i<components.length; i++) { + final Geometry element = (source != null) ? source.getGeometryN(i) : geometry; + if (type.isInstance(element)) { + components[i] = type.cast(element); + } else if (isCollection(element)) { + throw new IllegalArgumentException(Errors.format(Errors.Keys.NestedElementNotAllowed_1, GeometryCollection.class)); + } else { + components[i] = newComponent.apply(GeometryFactory.copy(element.getDataPoints())); + } + } + return components; + } + + /** + * Returns {@code true} if the given geometry is a collection other than {@link MultiPoint}. + * A multi-point is excluded because its positions make a polyline or a ring as well as the + * positions of a single geometry do. + */ + private static boolean isCollection(final Geometry candidate) { + return (candidate instanceof GeometryCollection<?> collection) + && !(candidate instanceof MultiPoint<?>) + && collection.getNumGeometries() >= 2; + } + + /** + * Returns a ring having the positions of the given geometry. The positions are copied, + * so that the returned ring does not share its data with the given geometry. + */ + private static LinearRing toRing(final Geometry source) { + return GeometryFactory.createLinearRing(GeometryFactory.copy(source.getDataPoints())); + } + + /** + * 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 { + if (operation == null) { + return this; + } + MathTransform mt = operation.getMathTransform(); + if (validate && crs != null) { + final CoordinateOperation step = CRS.findOperation(crs, operation.getSourceCRS(), null); + mt = MathTransforms.concatenate(step.getMathTransform(), mt); + } + return new Wrapper(new GeometryProcessor().transform(geometry, operation.getTargetCRS(), mt)); + } + + /** + * 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. + * + * @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 { + if (targetCRS == null || targetCRS == crs || crs == null) { + return this; + } + try { + return transform(CRS.findOperation(crs, targetCRS, null), false); + } catch (FactoryException e) { + throw new TransformException(e); + } + } + + /** + * Transforms this geometry using the given transform. + * If the transform is {@code null}, then the geometry is returned unchanged. + * + * @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 FactoryException, TransformException { + if (transform == null || transform.isIdentity()) { + return this; + } + return new Wrapper(this, new GeometryProcessor().transform(geometry, null, transform)); + } + + /** + * Returns a view over the SIS geometry as a Java2D shape. Changes in the SIS 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 new ShapeAdapter(geometry); + } + + /** + * Returns the WKT representation of the wrapped geometry. + */ + @Override + public String formatWKT(final double flatness) { + return geometry.asText(); + } + + /** + * View SIS Geometry as a JTS Geometry. + * Only the matching JTS geometry types are supported. + * The created geometry references the original geometry DataPoints, so modifications + * are forwarded to the original but all metadata change, like the CRS, will not be preserved if changed + * after the JTS view has been made. + * + * @param gf optional creation factory. + * @return JTS geometry view of the given geometry + */ + public org.locationtech.jts.geom.Geometry asJTS(org.locationtech.jts.geom.GeometryFactory gf) { + return asJTS(geometry, gf); + } + + /** + * View SIS Geometry as a JTS Geometry. + * Only the matching JTS geometry types are supported. + * The created geometry references the original geometry DataPoints, so modifications + * are forwarded to the original but all metadata change, like the CRS, will not be preserved if changed + * after the JTS view has been made. + * + * @param geometry to convert + * @param gf optional creation factory. + * @return JTS geometry view of the given geometry + */ + public static org.locationtech.jts.geom.Geometry asJTS(Geometry geometry, org.locationtech.jts.geom.GeometryFactory gf) { + if (gf == null) gf = new org.locationtech.jts.geom.GeometryFactory(); + return JTSAdapter.asJTS(geometry, false, gf); + } + + private Geometry fromJTS(final org.locationtech.jts.geom.Geometry result) { + result.setUserData(crs); + return JTSAdapter.fromJTS(result, true); + } +}
