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);
+    }
+}

Reply via email to