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 ac5b23c3f8dd82aed5767a3a17ebcc0a3b2b1c04
Author: jsorel <[email protected]>
AuthorDate: Thu Sep 3 14:52:15 2026 +0200

    feat(Geometry): implement several geometry types
---
 .../org/apache/sis/geometries/CompoundCurve.java   |  86 ++++++++++++-
 .../org/apache/sis/geometries/GeometryFactory.java | 111 +++++++++++++++++
 .../org/apache/sis/geometries/GeometryVisitor.java |   2 +-
 .../org/apache/sis/geometries/MultiSurface.java    |   9 +-
 .../main/org/apache/sis/geometries/Orientable.java |  11 +-
 .../main/org/apache/sis/geometries/Polyhedron.java |   2 +-
 .../main/org/apache/sis/geometries/Solid.java      |  36 ++++--
 .../sis/geometries/conics/CircularString.java      |  76 +++++++++++-
 .../internal/shared/AbstractGeometry.java          |  30 +++++
 .../internal/shared/DefaultCircularString.java     |  65 ++++++++++
 .../internal/shared/DefaultCompoundCurve.java      | 106 ++++++++++++++++
 .../internal/shared/DefaultCurvePolygon.java       |  97 +++++++++++++++
 .../internal/shared/DefaultGeometryCollection.java |  25 +++-
 .../internal/shared/DefaultMultiCurve.java         |  28 +----
 .../internal/shared/DefaultMultiLineString.java    |   5 +
 .../internal/shared/DefaultMultiPolygon.java       |   5 +
 ...ltiPolygon.java => DefaultMultiPolyhedron.java} |  17 ++-
 .../internal/shared/DefaultMultiSurface.java       |  33 +----
 .../internal/shared/DefaultPolyhedralSurface.java  | 107 ++++++++++++++++
 .../internal/shared/DefaultPolyhedron.java         | 137 +++++++++++++++++++++
 .../internal/shared/DefaultRawMultiPoint.java      |  28 +----
 .../internal/shared/DefaultReversedCurve.java      | 132 ++++++++++++++++++++
 .../internal/shared/DefaultReversedSurface.java    | 107 ++++++++++++++++
 ...iCurve.java => DefaultTriangulatedSurface.java} |  40 +++---
 24 files changed, 1173 insertions(+), 122 deletions(-)

diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/CompoundCurve.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/CompoundCurve.java
index 28cb6ffc2b..019f8b6ca3 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/CompoundCurve.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/CompoundCurve.java
@@ -16,10 +16,14 @@
  */
 package org.apache.sis.geometries;
 
+import java.util.ArrayList;
+import java.util.List;
 
 /**
+ * A curve made of several curves joined end to end, each of which may use a 
different
+ * interpolation. The end point of each component is the start point of the 
next one, so a compound
+ * curve is a single connected curve and not a collection — that is {@link 
MultiCurve}.
  *
- * @todo
  * @author Johann Sorel (Geomatys
  * @see https://docs.ogc.org/DRAFTS/21-045r1.html#compound_curve
  */
@@ -31,4 +35,84 @@ public interface CompoundCurve extends Curve {
     public default String getGeometryType() {
         return TYPE;
     }
+
+    /**
+     * Returns the number of curves this compound curve is made of.
+     *
+     * @return number of components, zero or more.
+     */
+    int getNumCurves();
+
+    /**
+     * Returns the component curve at the given index.
+     *
+     * @param  n  index of the component to return, from 0 inclusive to {@link 
#getNumCurves()} exclusive.
+     * @return the component curve at the given index.
+     * @throws IndexOutOfBoundsException if the index is out of bounds.
+     */
+    Curve getCurveN(int n);
+
+    /**
+     * Returns all the component curves, in order.
+     *
+     * @return the components, in the order in which they are traversed.
+     */
+    default List<Curve> getCurves() {
+        final int n = getNumCurves();
+        final List<Curve> curves = new ArrayList<>(n);
+        for (int i = 0; i < n; i++) {
+            curves.add(getCurveN(i));
+        }
+        return curves;
+    }
+
+    /**
+     * Returns {@link CurveInterpolation#COMPOSITE_CURVE}: the interpolation 
of a compound curve is
+     * not a single one, it is whatever each component declares.
+     */
+    @Override
+    default CurveInterpolation getInterpolation() {
+        return CurveInterpolation.COMPOSITE_CURVE;
+    }
+
+    /**
+     * Returns the start point of the first component.
+     */
+    @Override
+    default Point getStartPoint() {
+        return getCurveN(0).getStartPoint();
+    }
+
+    /**
+     * Returns the end point of the last component.
+     */
+    @Override
+    default Point getEndPoint() {
+        return getCurveN(getNumCurves() - 1).getEndPoint();
+    }
+
+    /**
+     * Returns the sum of the lengths of the components.
+     */
+    @Override
+    default double getLength() {
+        double length = 0;
+        for (int i = 0, n = getNumCurves(); i < n; i++) {
+            length += getCurveN(i).getLength();
+        }
+        return length;
+    }
+
+    /**
+     * Returns whether the last component ends where the first one starts.
+     * An empty compound curve is not closed.
+     */
+    @Override
+    default boolean isClosed() {
+        final int n = getNumCurves();
+        if (n == 0) {
+            return false;
+        }
+        return 
getStartPoint().getPosition().equals(getEndPoint().getPosition(), 0);
+    }
 }
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 73743486aa..b8748a9692 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
@@ -28,18 +28,28 @@ import org.apache.sis.geometries.math.SampleSystem;
 import org.apache.sis.geometries.math.NDArrays;
 import org.apache.sis.geometries.math.Array;
 import org.apache.sis.geometries.internal.shared.ArraySequence;
+import org.apache.sis.geometries.internal.shared.DefaultCircularString;
+import org.apache.sis.geometries.internal.shared.DefaultCompoundCurve;
+import org.apache.sis.geometries.internal.shared.DefaultCurvePolygon;
 import org.apache.sis.geometries.internal.shared.DefaultEmpty;
 import org.apache.sis.geometries.internal.shared.DefaultGeometryCollection;
 import org.apache.sis.geometries.internal.shared.DefaultLineString;
 import org.apache.sis.geometries.internal.shared.DefaultLinearRing;
+import org.apache.sis.geometries.internal.shared.DefaultMultiCurve;
 import org.apache.sis.geometries.internal.shared.DefaultMultiLineString;
 import org.apache.sis.geometries.internal.shared.DefaultMultiPoint;
 import org.apache.sis.geometries.internal.shared.DefaultMultiPolygon;
+import org.apache.sis.geometries.internal.shared.DefaultMultiPolyhedron;
 import org.apache.sis.geometries.internal.shared.DefaultMultiSurface;
 import org.apache.sis.geometries.internal.shared.DefaultPoint;
 import org.apache.sis.geometries.internal.shared.DefaultPolygon;
+import org.apache.sis.geometries.internal.shared.DefaultPolyhedralSurface;
+import org.apache.sis.geometries.internal.shared.DefaultPolyhedron;
 import org.apache.sis.geometries.internal.shared.DefaultRawMultiPoint;
+import org.apache.sis.geometries.internal.shared.DefaultReversedCurve;
+import org.apache.sis.geometries.internal.shared.DefaultReversedSurface;
 import org.apache.sis.geometries.internal.shared.DefaultTriangle;
+import org.apache.sis.geometries.internal.shared.DefaultTriangulatedSurface;
 import org.apache.sis.geometries.math.DataType;
 import org.apache.sis.geometries.spirals.Clothoid;
 import org.apache.sis.geometry.wrapper.Capability;
@@ -115,6 +125,10 @@ public final class GeometryFactory extends 
org.apache.sis.geometry.wrapper.Geome
         return new DefaultMultiLineString(geometries);
     }
 
+    public static <T extends Curve> MultiCurve<T> createMultiCurve(T ... 
geometries) {
+        return new DefaultMultiCurve<>(geometries);
+    }
+
     public static MultiPolygon createMultiPolygon(Polygon ... geometries) {
         return new DefaultMultiPolygon(geometries);
     }
@@ -127,6 +141,103 @@ public final class GeometryFactory extends 
org.apache.sis.geometry.wrapper.Geome
         return new DefaultGeometryCollection<>(geometries);
     }
 
+    /*
+     * Variants taking an explicit coordinate reference system, used when the 
collection may be
+     * empty. An aggregate normally reports the CRS of its first element; with 
no element there is
+     * nothing to report, so the CRS has to be supplied by the caller.
+     */
+
+    public static MultiPoint createMultiPoint(CoordinateReferenceSystem crs, 
Point ... geometries) {
+        return new DefaultRawMultiPoint(crs, geometries);
+    }
+
+    public static MultiLineString 
createMultiLineString(CoordinateReferenceSystem crs, LineString ... geometries) 
{
+        return new DefaultMultiLineString(crs, geometries);
+    }
+
+    public static <T extends Curve> MultiCurve<T> 
createMultiCurve(CoordinateReferenceSystem crs, T ... geometries) {
+        return new DefaultMultiCurve<>(crs, geometries);
+    }
+
+    public static MultiPolygon createMultiPolygon(CoordinateReferenceSystem 
crs, Polygon ... geometries) {
+        return new DefaultMultiPolygon(crs, geometries);
+    }
+
+    public static <T extends Surface> MultiSurface<T> 
createMultiSurface(CoordinateReferenceSystem crs, T ... geometries) {
+        return new DefaultMultiSurface<>(crs, geometries);
+    }
+
+    public static <T extends Geometry> GeometryCollection<T> 
createGeometryCollection(CoordinateReferenceSystem crs, T ... geometries) {
+        return new DefaultGeometryCollection<>(crs, geometries);
+    }
+
+    /*
+     * Curves and surfaces beyond the linear ones, and solids. These are what 
the GML 3 constructs
+     * `gml:Curve`, `gml:CompositeCurve`, `gml:Ring`, `gml:ArcString`, 
`gml:Surface`,
+     * `gml:CompositeSurface`, `gml:Solid`, `gml:CompositeSolid` and the two 
`gml:Orientable*`
+     * elements map onto.
+     */
+
+    public static CompoundCurve createCompoundCurve(Curve ... curves) {
+        return new DefaultCompoundCurve(curves);
+    }
+
+    public static CompoundCurve createCompoundCurve(CoordinateReferenceSystem 
crs, Curve ... curves) {
+        return new DefaultCompoundCurve(crs, curves);
+    }
+
+    public static CircularString createCircularString(PointSequence sequence) {
+        return new DefaultCircularString(sequence);
+    }
+
+    public static CurvePolygon createCurvePolygon(Curve exterior, List<Curve> 
interiors) {
+        return new DefaultCurvePolygon(exterior, interiors);
+    }
+
+    public static <T extends Polygon> PolyhedralSurface<T> 
createPolyhedralSurface(T ... patches) {
+        return new DefaultPolyhedralSurface<>(null, patches);
+    }
+
+    public static <T extends Polygon> PolyhedralSurface<T> 
createPolyhedralSurface(CoordinateReferenceSystem crs, T[] patches) {
+        return new DefaultPolyhedralSurface<>(crs, patches);
+    }
+
+    public static TIN createTIN(Triangle ... patches) {
+        return new DefaultTriangulatedSurface(patches);
+    }
+
+    public static TIN createTIN(CoordinateReferenceSystem crs, Triangle[] 
patches) {
+        return new DefaultTriangulatedSurface(crs, patches);
+    }
+
+    public static Polyhedron createPolyhedron(MultiPolygon exteriorShell, 
List<MultiPolygon> interiorShells) {
+        return new DefaultPolyhedron(exteriorShell, interiorShells);
+    }
+
+    public static MultiPolyhedron createMultiPolyhedron(Polyhedron ... solids) 
{
+        return new DefaultMultiPolyhedron(solids);
+    }
+
+    public static MultiPolyhedron 
createMultiPolyhedron(CoordinateReferenceSystem crs, Polyhedron ... solids) {
+        return new DefaultMultiPolyhedron(crs, solids);
+    }
+
+    /**
+     * Returns a curve traversed in the opposite direction to the given one.
+     * This is what a GML {@code gml:OrientableCurve} with {@code 
orientation="-"} describes.
+     */
+    public static Curve createReversed(Curve base) {
+        return new DefaultReversedCurve(base);
+    }
+
+    /**
+     * Returns a surface whose up-normal points the opposite way to the given 
one's.
+     * This is what a GML {@code gml:OrientableSurface} with {@code 
orientation="-"} describes.
+     */
+    public static Surface createReversed(Surface base) {
+        return new DefaultReversedSurface(base);
+    }
+
     public static PointSequence createSequence(Array positions) {
         return 
createSequence(Collections.singletonMap(AttributesType.ATT_POSITION, 
positions));
     }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryVisitor.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryVisitor.java
index deac5dbc57..6ea87869e8 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryVisitor.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryVisitor.java
@@ -32,9 +32,9 @@ public class GeometryVisitor {
 
         else if (geometry instanceof Triangle candidate) visit(candidate);
         else if (geometry instanceof Polygon candidate) visit(candidate);
-        else if (geometry instanceof Surface candidate) visit(candidate);
         else if (geometry instanceof TIN candidate) visit(candidate);
         else if (geometry instanceof PolyhedralSurface candidate) 
visit(candidate);
+        else if (geometry instanceof Surface candidate) visit(candidate);
 
         else if (geometry instanceof MultiPolygon candidate) visit(candidate);
         else if (geometry instanceof MultiLineString candidate) 
visit(candidate);
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/MultiSurface.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/MultiSurface.java
index 069f454581..01ce887dcc 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/MultiSurface.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/MultiSurface.java
@@ -36,6 +36,13 @@ package org.apache.sis.geometries;
  */
 public interface MultiSurface<T extends Surface> extends GeometryCollection<T>{
 
+    public static final String TYPE = "MULTISURFACE";
+
+    @Override
+    public default String getGeometryType() {
+        return TYPE;
+    }
+
     /**
      * The area of this MultiSurface, as measured in the spatial reference 
system of this MultiSurface.
      *
@@ -45,7 +52,7 @@ public interface MultiSurface<T extends Surface> extends 
GeometryCollection<T>{
     default double getArea() {
         double area = 0.0;
         for (int i = 0, n = getNumGeometries(); i < n; i++) {
-            area += getGeometryN(n).getArea();
+            area += getGeometryN(i).getArea();
         }
         return area;
     }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Orientable.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Orientable.java
index 6f821aa5b8..feabb3c8e1 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Orientable.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Orientable.java
@@ -32,10 +32,17 @@ public interface Orientable extends Primitive {
         NEGATIVE
     }
 
+    /**
+     * Returns whether this geometry is traversed in the direction of its 
underlying primitive
+     * ({@link Sign#POSITIVE}) or in the opposite direction ({@link 
Sign#NEGATIVE}).
+     *
+     * <p>The default is {@link Sign#POSITIVE}, which is what a geometry 
defined directly by its own
+     * coordinates is: only a geometry that stands for the reverse of another 
one reports
+     * {@link Sign#NEGATIVE}.</p>
+     */
     @UML(identifier="orientation", specification=ISO_19107) // section 6.4.15.2
     default Sign getOrientationSign(){
-        //TODO
-        throw new UnsupportedOperationException();
+        return Sign.POSITIVE;
     }
 
     @UML(identifier="proxy", specification=ISO_19107) // section 6.4.15.3
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Polyhedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Polyhedron.java
index 4c6562e391..77f244f756 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Polyhedron.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Polyhedron.java
@@ -33,7 +33,7 @@ import java.util.List;
  * @author Johann Sorel (Geomatys)
  * @see https://docs.ogc.org/DRAFTS/21-045r1.html#polyhedron
  */
-public interface Polyhedron extends Geometry {
+public interface Polyhedron extends Solid {
 
     public static final String TYPE = "POLYHEDRON";
 
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Solid.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Solid.java
index fd9c662293..2dddda6053 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Solid.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Solid.java
@@ -31,25 +31,47 @@ import org.opengis.geometry.DirectPosition;
 @UML(identifier="Solid", specification=ISO_19107) // section 6.4.28
 public interface Solid extends Primitive {
 
+    /**
+     * Returns 3: a polyhedron bounds a volume.
+     */
+    @Override
+    public default int getTopologicDimension() {
+        return 3;
+    }
+
     @UML(identifier="boundary", specification=ISO_19107) // section 6.4.28.2
-    Geometry getBoundary();
+    default Geometry getBoundary() {
+        throw new UnsupportedOperationException();
+    }
 
     @UML(identifier="area", specification=ISO_19107) // section 6.4.28.3
-    Area getArea();
+    default Area getArea() {
+        throw new UnsupportedOperationException();
+    }
 
     @UML(identifier="volume", specification=ISO_19107) // section 6.4.28.4
-    Volume getVolume();
+    default Volume getVolume() {
+        throw new UnsupportedOperationException();
+    }
 
     @UML(identifier="dataPoint", specification=ISO_19107) // section 6.4.28.5
-    List<DirectPosition> getDataPoints();
+    default List<DirectPosition> getDataPoints() {
+        throw new UnsupportedOperationException();
+    }
 
     @UML(identifier="controlPoint", specification=ISO_19107) // section 
6.4.28.6
-    List<DirectPosition> getControlPoints();
+    default List<DirectPosition> getControlPoints() {
+        throw new UnsupportedOperationException();
+    }
 
     @UML(identifier="interpolation", specification=ISO_19107) // section 
6.4.28.7
-    SolidInterpolation getInterpolation();
+    default SolidInterpolation getInterpolation() {
+        throw new UnsupportedOperationException();
+    }
 
     @UML(identifier="knot", specification=ISO_19107) // section 6.4.28.8
-    List<Knot> getKnots();
+    default List<Knot> getKnots() {
+        throw new UnsupportedOperationException();
+    }
 
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/conics/CircularString.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/conics/CircularString.java
index b7f19733d4..5f79396621 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/conics/CircularString.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/conics/CircularString.java
@@ -16,13 +16,23 @@
  */
 package org.apache.sis.geometries.conics;
 
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.AttributesType;
 import org.apache.sis.geometries.Curve;
+import org.apache.sis.geometries.CurveInterpolation;
+import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.PointSequence;
 
 
 /**
- * @todo
+ * A curve made of circular arcs joined end to end, each arc being defined by 
three points: its
+ * start, a point somewhere along it, and its end. Consecutive arcs share a 
point, so a circular
+ * string of <var>k</var> arcs has 2·<var>k</var>+1 points — always an odd 
number, and at least
+ * three.
+ *
  * @author Johann Sorel (Geomatys)
  * @see https://docs.ogc.org/DRAFTS/21-045r1.html#circular_string
+ * @see GML ArcString
  */
 public interface CircularString extends Curve {
 
@@ -32,4 +42,68 @@ public interface CircularString extends Curve {
     public default String getGeometryType() {
         return TYPE;
     }
+
+    /**
+     * Returns the control points of this circular string: for each arc, its 
start point, a point
+     * on the arc and its end point, with consecutive arcs sharing a point.
+     *
+     * @return the control points, never null. Its size is odd and at least 3, 
or 0 if empty.
+     */
+    PointSequence getPoints();
+
+    /**
+     * Returns the number of arcs this circular string is made of.
+     */
+    default int getNumArcs() {
+        final int size = getPoints().size();
+        return (size == 0) ? 0 : (size - 1) / 2;
+    }
+
+    /**
+     * Returns {@link CurveInterpolation#CIRCULAR}.
+     */
+    @Override
+    default CurveInterpolation getInterpolation() {
+        return CurveInterpolation.CIRCULAR;
+    }
+
+    @Override
+    default CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return getPoints().getCoordinateReferenceSystem();
+    }
+
+    @Override
+    default void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
+        getPoints().setCoordinateReferenceSystem(cs);
+    }
+
+    @Override
+    default AttributesType getAttributesType() {
+        return getPoints().getAttributesType();
+    }
+
+    @Override
+    default boolean isEmpty() {
+        return getPoints().isEmpty();
+    }
+
+    @Override
+    default Point getStartPoint() {
+        return getPoints().getPoint(0);
+    }
+
+    @Override
+    default Point getEndPoint() {
+        return getPoints().getPoint(getPoints().size() - 1);
+    }
+
+    @Override
+    default boolean isClosed() {
+        final PointSequence points = getPoints();
+        final int size = points.size();
+        if (size == 0) {
+            return false;
+        }
+        return points.getPosition(0).equals(points.getPosition(size - 1), 0);
+    }
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/AbstractGeometry.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/AbstractGeometry.java
index a58e7f5b82..d765953328 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/AbstractGeometry.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/AbstractGeometry.java
@@ -23,6 +23,8 @@ import org.apache.sis.geometries.PointSequence;
 import org.apache.sis.geometries.math.Tuple;
 import org.apache.sis.geometries.math.Cursor;
 import org.apache.sis.geometries.math.Array;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.opengis.geometry.Envelope;
 
 
 /**
@@ -78,4 +80,32 @@ public abstract class AbstractGeometry implements Geometry {
             toText(sb, array.getPosition(i));
         }
     }
+
+    /**
+     * Returns the union of the envelopes of the given geometries,
+     * or {@code null} if there is nothing to compute a union of.
+     */
+    protected static Envelope envUnion(final Geometry... geometries) {
+        GeneralEnvelope union = null;
+        for (final Geometry geometry : geometries) {
+            union = add(union, geometry);
+        }
+        return union;
+    }
+
+    /**
+     * Adds the envelope of the given geometry to the given union, creating it 
if needed.
+     * Geometries with no envelope at all (an empty one, typically) are 
skipped.
+     */
+    private static GeneralEnvelope add(GeneralEnvelope union, final Geometry 
geometry) {
+        final Envelope envelope = geometry.getEnvelope();
+        if (envelope != null) {
+            if (union == null) {
+                union = new GeneralEnvelope(envelope);
+            } else {
+                union.add(envelope);
+            }
+        }
+        return union;
+    }
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCircularString.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCircularString.java
new file mode 100644
index 0000000000..a96a1f265f
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCircularString.java
@@ -0,0 +1,65 @@
+/*
+ * 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.internal.shared;
+
+import org.apache.sis.geometries.PointSequence;
+import org.apache.sis.geometries.conics.CircularString;
+import org.opengis.geometry.Envelope;
+
+/**
+ * A curve made of circular arcs, each defined by three of the control points.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultCircularString extends AbstractGeometry implements 
CircularString {
+
+    private final PointSequence points;
+
+    /**
+     * Creates a circular string from the given control points.
+     *
+     * @param  points  start point, mid point and end point of each arc, 
consecutive arcs sharing a
+     *                 point. Its size must be odd and at least 3, or 0 for an 
empty circular string.
+     * @throws IllegalArgumentException if the number of points cannot 
describe a whole number of arcs.
+     */
+    public DefaultCircularString(PointSequence points) {
+        final int size = points.size();
+        if (size != 0 && (size < 3 || (size % 2) == 0)) {
+            throw new IllegalArgumentException("A circular string needs an odd 
number of at least 3 points "
+                    + "(start, mid and end of each arc, consecutive arcs 
sharing a point), but got " + size + '.');
+        }
+        this.points = points;
+    }
+
+    @Override
+    public PointSequence getPoints() {
+        return points;
+    }
+
+    @Override
+    public Envelope getEnvelope() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public String asText() {
+        final StringBuilder sb = new StringBuilder(TYPE).append(" (");
+        toText(sb, points);
+        return sb.append(')').toString();
+    }
+
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCompoundCurve.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCompoundCurve.java
new file mode 100644
index 0000000000..91509203e0
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCompoundCurve.java
@@ -0,0 +1,106 @@
+/*
+ * 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.internal.shared;
+
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.CompoundCurve;
+import org.apache.sis.geometries.Curve;
+
+
+/**
+ * A curve made of several curves joined end to end.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultCompoundCurve extends AbstractGeometry implements 
CompoundCurve {
+
+    private final Curve[] curves;
+
+    /**
+     * The coordinate reference system to report when this curve has no 
component.
+     * Ignored otherwise, in which case the first component is authoritative. 
May be {@code null}.
+     */
+    private CoordinateReferenceSystem fallbackCRS;
+
+    public DefaultCompoundCurve(Curve... curves) {
+        this(null, curves);
+    }
+
+    /**
+     * Creates a compound curve which reports the given coordinate reference 
system
+     * when {@code curves} is empty.
+     */
+    public DefaultCompoundCurve(CoordinateReferenceSystem fallbackCRS, 
Curve... curves) {
+        this.curves      = curves;
+        this.fallbackCRS = fallbackCRS;
+    }
+
+    @Override
+    public int getNumCurves() {
+        return curves.length;
+    }
+
+    @Override
+    public Curve getCurveN(int n) {
+        return curves[n];
+    }
+
+    @Override
+    public boolean isEmpty() {
+        for (final Curve c : curves) {
+            if (!c.isEmpty()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return (curves.length != 0) ? curves[0].getCoordinateReferenceSystem() 
: fallbackCRS;
+    }
+
+    @Override
+    public void setCoordinateReferenceSystem(CoordinateReferenceSystem crs) 
throws IllegalArgumentException {
+        this.fallbackCRS = crs;
+        for (final Curve c : curves) {
+            c.setCoordinateReferenceSystem(crs);
+        }
+    }
+
+    @Override
+    public AttributesType getAttributesType() {
+        return (curves.length != 0) ? curves[0].getAttributesType() : 
AttributesType.EMPTY;
+    }
+
+    @Override
+    public Envelope getEnvelope() {
+        return envUnion(curves);
+    }
+
+    @Override
+    public String asText() {
+        final StringBuilder sb = new StringBuilder(TYPE).append(" (");
+        for (int i = 0; i < curves.length; i++) {
+            if (i != 0) sb.append(", ");
+            sb.append(curves[i].asText());
+        }
+        return sb.append(')').toString();
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java
new file mode 100644
index 0000000000..05a9a7bb89
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java
@@ -0,0 +1,97 @@
+/*
+ * 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.internal.shared;
+
+import java.util.List;
+import java.util.Objects;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.Curve;
+import org.apache.sis.geometries.CurvePolygon;
+import org.apache.sis.geometries.Geometries;
+
+
+/**
+ * A planar surface whose boundary rings may use any interpolation, not only 
the linear one that a
+ * {@link org.apache.sis.geometries.Polygon} is restricted to.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultCurvePolygon extends AbstractGeometry implements 
CurvePolygon {
+
+    protected final Curve exterior;
+    protected final List<Curve> interiors;
+
+    public DefaultCurvePolygon(Curve exterior) {
+        this(exterior, null);
+    }
+
+    public DefaultCurvePolygon(Curve exterior, List<Curve> interiors) {
+        this.exterior = Objects.requireNonNull(exterior);
+        this.interiors = (interiors == null) ? List.of() : 
List.copyOf(interiors);
+        for (final Curve interior : this.interiors) {
+            Geometries.ensureSameAttributes(exterior.getAttributesType(), 
interior.getAttributesType());
+        }
+    }
+
+    @Override
+    public Curve getExteriorRing() {
+        return exterior;
+    }
+
+    @Override
+    public List<Curve> getInteriorRings() {
+        return interiors;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return exterior.isEmpty();
+    }
+
+    @Override
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return exterior.getCoordinateReferenceSystem();
+    }
+
+    @Override
+    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
+        exterior.setCoordinateReferenceSystem(cs);
+        for (final Curve interior : interiors) {
+            interior.setCoordinateReferenceSystem(cs);
+        }
+    }
+
+    @Override
+    public Envelope getEnvelope() {
+        return exterior.getEnvelope();
+    }
+
+    @Override
+    public double getArea() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public String asText() {
+        final StringBuilder sb = new StringBuilder(TYPE).append(" 
(").append(exterior.asText());
+        for (final Curve interior : interiors) {
+            sb.append(", ").append(interior.asText());
+        }
+        return sb.append(')').toString();
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultGeometryCollection.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultGeometryCollection.java
index e327209a35..7fd7ba5fd5 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultGeometryCollection.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultGeometryCollection.java
@@ -29,19 +29,36 @@ public class DefaultGeometryCollection<T extends Geometry> 
extends AbstractGeome
 
     private final T[] geometries;
 
+    /**
+     * The coordinate reference system to report when this collection is empty.
+     * Ignored when at least one geometry is present, in which case the first
+     * geometry is authoritative. May be {@code null}.
+     */
+    private CoordinateReferenceSystem fallbackCRS;
+
     public DefaultGeometryCollection(T[] geometries) {
-        this.geometries = geometries;
+        this(null, geometries);
+    }
+
+    /**
+     * Creates a collection which reports the given coordinate reference system
+     * when {@code geometries} is empty.
+     */
+    public DefaultGeometryCollection(CoordinateReferenceSystem fallbackCRS, 
T[] geometries) {
+        this.geometries  = geometries;
+        this.fallbackCRS = fallbackCRS;
     }
 
     @Override
     public CoordinateReferenceSystem getCoordinateReferenceSystem() {
-        return geometries[0].getCoordinateReferenceSystem();
+        return (geometries.length != 0) ? 
geometries[0].getCoordinateReferenceSystem() : fallbackCRS;
     }
 
     @Override
-    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
+    public void setCoordinateReferenceSystem(CoordinateReferenceSystem crs) 
throws IllegalArgumentException {
+        fallbackCRS = crs;
         for (T c : geometries) {
-            c.setCoordinateReferenceSystem(cs);
+            c.setCoordinateReferenceSystem(crs);
         }
     }
 
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiCurve.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiCurve.java
index 1085be62aa..3da9597157 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiCurve.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiCurve.java
@@ -25,34 +25,14 @@ import org.apache.sis.geometries.MultiCurve;
  *
  * @author Johann Sorel (Geomatys)
  */
-public class DefaultMultiCurve<T extends Curve> extends AbstractGeometry 
implements MultiCurve<T> {
-
-    private final T[] curves;
+public class DefaultMultiCurve<T extends Curve> extends 
DefaultGeometryCollection<T> implements MultiCurve<T> {
 
     public DefaultMultiCurve(T[] geometries) {
-        this.curves = geometries;
-    }
-
-    @Override
-    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
-        return curves[0].getCoordinateReferenceSystem();
-    }
-
-    @Override
-    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
-        for (Curve c : curves) {
-            c.setCoordinateReferenceSystem(cs);
-        }
-    }
-
-    @Override
-    public int getNumGeometries() {
-        return curves.length;
+        this(null, geometries);
     }
 
-    @Override
-    public T getGeometryN(int n) {
-        return curves[n];
+    public DefaultMultiCurve(CoordinateReferenceSystem fallbackCRS, T[] 
geometries) {
+        super(fallbackCRS, geometries);
     }
 
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiLineString.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiLineString.java
index cbeaba3959..bee4876af6 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiLineString.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiLineString.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.geometries.internal.shared;
 
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometries.LineString;
 import org.apache.sis.geometries.MultiLineString;
 
@@ -30,4 +31,8 @@ public class DefaultMultiLineString extends 
DefaultMultiCurve<LineString> implem
         super(geometries);
     }
 
+    public DefaultMultiLineString(CoordinateReferenceSystem fallbackCRS, 
LineString... geometries) {
+        super(fallbackCRS, geometries);
+    }
+
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolygon.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolygon.java
index 9fc249ca7c..cff7054b0d 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolygon.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolygon.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.geometries.internal.shared;
 
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometries.MultiPolygon;
 import org.apache.sis.geometries.Polygon;
 
@@ -30,4 +31,8 @@ public class DefaultMultiPolygon extends 
DefaultMultiSurface<Polygon> implements
         super(geometries);
     }
 
+    public DefaultMultiPolygon(CoordinateReferenceSystem fallbackCRS, 
Polygon[] geometries) {
+        super(fallbackCRS, geometries);
+    }
+
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolygon.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolyhedron.java
similarity index 60%
copy from 
incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolygon.java
copy to 
incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolyhedron.java
index 9fc249ca7c..fb52a4c671 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolygon.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPolyhedron.java
@@ -16,18 +16,25 @@
  */
 package org.apache.sis.geometries.internal.shared;
 
-import org.apache.sis.geometries.MultiPolygon;
-import org.apache.sis.geometries.Polygon;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.MultiPolyhedron;
+import org.apache.sis.geometries.Polyhedron;
 
 
 /**
+ * A collection of solids, each defined by its bounding shells.
  *
  * @author Johann Sorel (Geomatys)
+ * @see GML CompositeSolid, MultiSolid
  */
-public class DefaultMultiPolygon extends DefaultMultiSurface<Polygon> 
implements MultiPolygon {
+public class DefaultMultiPolyhedron extends 
DefaultGeometryCollection<Polyhedron> implements MultiPolyhedron {
 
-    public DefaultMultiPolygon(Polygon[] geometries) {
-        super(geometries);
+    public DefaultMultiPolyhedron(Polyhedron... solids) {
+        this(null, solids);
+    }
+
+    public DefaultMultiPolyhedron(CoordinateReferenceSystem fallbackCRS, 
Polyhedron... solids) {
+        super(fallbackCRS, solids);
     }
 
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiSurface.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiSurface.java
index 211d72a4d2..a9ba2b8711 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiSurface.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiSurface.java
@@ -25,39 +25,14 @@ import org.apache.sis.geometries.Surface;
  *
  * @author Johann Sorel (Geomatys)
  */
-public class DefaultMultiSurface<T extends Surface> extends AbstractGeometry 
implements MultiSurface<T> {
-
-    private final T[] surfaces;
+public class DefaultMultiSurface<T extends Surface> extends 
DefaultGeometryCollection<T> implements MultiSurface<T> {
 
     public DefaultMultiSurface(T[] geometries) {
-        this.surfaces = geometries;
-    }
-
-    @Override
-    public String getGeometryType() {
-        return "MULTISURFACE";
-    }
-
-    @Override
-    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
-        return surfaces[0].getCoordinateReferenceSystem();
-    }
-
-    @Override
-    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
-        for (T c : surfaces) {
-            c.setCoordinateReferenceSystem(cs);
-        }
-    }
-
-    @Override
-    public int getNumGeometries() {
-        return surfaces.length;
+        this(null, geometries);
     }
 
-    @Override
-    public T getGeometryN(int n) {
-        return surfaces[n];
+    public DefaultMultiSurface(CoordinateReferenceSystem fallbackCRS, T[] 
geometries) {
+        super(fallbackCRS, geometries);
     }
 
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedralSurface.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedralSurface.java
new file mode 100644
index 0000000000..1f458071c8
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedralSurface.java
@@ -0,0 +1,107 @@
+/*
+ * 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.internal.shared;
+
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.Polygon;
+import org.apache.sis.geometries.PolyhedralSurface;
+
+
+/**
+ * A surface made of polygonal patches which share their common boundary 
segments.
+ *
+ * @author Johann Sorel (Geomatys)
+ * @see GML Surface patches PolygonPatch CompositeSurface
+ */
+public class DefaultPolyhedralSurface<T extends Polygon> extends 
AbstractGeometry implements PolyhedralSurface<T> {
+
+    private final T[] patches;
+
+    /**
+     * The coordinate reference system to report when this surface has no 
patch.
+     * Ignored otherwise, in which case the first patch is authoritative. May 
be {@code null}.
+     */
+    private final CoordinateReferenceSystem fallbackCRS;
+
+    @SafeVarargs
+    public DefaultPolyhedralSurface(T... patches) {
+        this(null, patches);
+    }
+
+    /**
+     * Creates a surface which reports the given coordinate reference system
+     * when {@code patches} is empty.
+     */
+    public DefaultPolyhedralSurface(CoordinateReferenceSystem fallbackCRS, T[] 
patches) {
+        this.patches     = patches;
+        this.fallbackCRS = fallbackCRS;
+    }
+
+    @Override
+    public int getNumPatches() {
+        return patches.length;
+    }
+
+    @Override
+    public T getPatchN(int n) {
+        return patches[n];
+    }
+
+    @Override
+    public boolean isEmpty() {
+        for (final T patch : patches) {
+            if (!patch.isEmpty()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return (patches.length != 0) ? 
patches[0].getCoordinateReferenceSystem() : fallbackCRS;
+    }
+
+    @Override
+    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
+        for (final T patch : patches) {
+            patch.setCoordinateReferenceSystem(cs);
+        }
+    }
+
+    @Override
+    public AttributesType getAttributesType() {
+        return (patches.length != 0) ? patches[0].getAttributesType() : 
AttributesType.EMPTY;
+    }
+
+    @Override
+    public Envelope getEnvelope() {
+        return envUnion(patches);
+    }
+
+    @Override
+    public String asText() {
+        final StringBuilder sb = new 
StringBuilder(PolyhedralSurface.TYPE).append(" (");
+        for (int i = 0; i < patches.length; i++) {
+            if (i != 0) sb.append(", ");
+            sb.append(patches[i].asText());
+        }
+        return sb.append(')').toString();
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedron.java
new file mode 100644
index 0000000000..c96b04d20d
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolyhedron.java
@@ -0,0 +1,137 @@
+/*
+ * 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.internal.shared;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import javax.measure.quantity.Area;
+import javax.measure.quantity.Volume;
+import org.apache.sis.geometries.Geometry;
+import org.apache.sis.geometries.Knot;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.MultiPolygon;
+import org.apache.sis.geometries.Polyhedron;
+import org.apache.sis.geometries.SolidInterpolation;
+import org.opengis.geometry.DirectPosition;
+
+
+/**
+ * A solid defined by its bounding shells, each shell being a closed set of 
polygons.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultPolyhedron extends AbstractGeometry implements Polyhedron {
+
+    private final MultiPolygon exterior;
+    private final List<MultiPolygon> interiors;
+
+    public DefaultPolyhedron(MultiPolygon exterior) {
+        this(exterior, null);
+    }
+
+    public DefaultPolyhedron(MultiPolygon exterior, List<MultiPolygon> 
interiors) {
+        this.exterior  = Objects.requireNonNull(exterior);
+        this.interiors = (interiors == null) ? List.of() : 
List.copyOf(interiors);
+    }
+
+    @Override
+    public MultiPolygon getExteriorShell() {
+        return exterior;
+    }
+
+    @Override
+    public List<MultiPolygon> getInteriorShells() {
+        return interiors;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return exterior.isEmpty();
+    }
+
+    @Override
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return exterior.getCoordinateReferenceSystem();
+    }
+
+    @Override
+    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
+        exterior.setCoordinateReferenceSystem(cs);
+        for (final MultiPolygon interior : interiors) {
+            interior.setCoordinateReferenceSystem(cs);
+        }
+    }
+
+    /**
+     * Returns the envelope of the exterior shell. The interior shells are 
voids inside it,
+     * so they cannot extend it.
+     */
+    @Override
+    public Envelope getEnvelope() {
+        return exterior.getEnvelope();
+    }
+
+    @Override
+    public String asText() {
+        final List<MultiPolygon> shells = new ArrayList<>(interiors.size() + 
1);
+        shells.add(exterior);
+        shells.addAll(interiors);
+        final StringBuilder sb = new StringBuilder(TYPE).append(" (");
+        for (int i = 0; i < shells.size(); i++) {
+            if (i != 0) sb.append(", ");
+            sb.append(shells.get(i).asText());
+        }
+        return sb.append(')').toString();
+    }
+
+    @Override
+    public Geometry getBoundary() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public Area getArea() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public Volume getVolume() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public List<DirectPosition> getDataPoints() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public List<DirectPosition> getControlPoints() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public SolidInterpolation getInterpolation() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public List<Knot> getKnots() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRawMultiPoint.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRawMultiPoint.java
index ad9d28f720..b1ddf3cfbb 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRawMultiPoint.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRawMultiPoint.java
@@ -25,34 +25,14 @@ import org.apache.sis.geometries.Point;
  *
  * @author Johann Sorel (Geomatys)
  */
-public class DefaultRawMultiPoint extends AbstractGeometry implements 
MultiPoint<Point> {
-
-    private final Point[] geometries;
+public class DefaultRawMultiPoint extends DefaultGeometryCollection<Point> 
implements MultiPoint<Point> {
 
     public DefaultRawMultiPoint(Point[] geometries) {
-        this.geometries = geometries;
-    }
-
-    @Override
-    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
-        return geometries[0].getCoordinateReferenceSystem();
-    }
-
-    @Override
-    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
-        for (Point c : geometries) {
-            c.setCoordinateReferenceSystem(cs);
-        }
-    }
-
-    @Override
-    public int getNumGeometries() {
-        return geometries.length;
+        this(null, geometries);
     }
 
-    @Override
-    public Point getGeometryN(int n) {
-        return geometries[n];
+    public DefaultRawMultiPoint(CoordinateReferenceSystem fallbackCRS, Point[] 
geometries) {
+        super(fallbackCRS, geometries);
     }
 
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java
new file mode 100644
index 0000000000..fd1541ebb2
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java
@@ -0,0 +1,132 @@
+/*
+ * 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.internal.shared;
+
+import java.util.Objects;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.Curve;
+import org.apache.sis.geometries.CurveInterpolation;
+import org.apache.sis.geometries.Orientable;
+import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.Primitive;
+
+
+/**
+ * A curve traversed in the opposite direction to the one it is defined in.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultReversedCurve extends AbstractGeometry implements Curve {
+
+    private final Curve base;
+
+    public DefaultReversedCurve(Curve base) {
+        this.base = Objects.requireNonNull(base);
+    }
+
+    @Override
+    public Sign getOrientationSign() {
+        return Sign.NEGATIVE;
+    }
+
+    @Override
+    public Primitive getPrimitive() {
+        return base;
+    }
+
+    @Override
+    public Orientable getProxy() {
+        return base;
+    }
+
+    @Override
+    public Curve getReverse() {
+        return base;
+    }
+
+    /**
+     * Returns the geometry type of the base curve: reversing a curve does not 
change what kind of
+     * curve it is.
+     */
+    @Override
+    public String getGeometryType() {
+        return base.getGeometryType();
+    }
+
+    /**
+     * Returns the end point of the base curve, which is where this curve 
starts.
+     */
+    @Override
+    public Point getStartPoint() {
+        return base.getEndPoint();
+    }
+
+    /**
+     * Returns the start point of the base curve, which is where this curve 
ends.
+     */
+    @Override
+    public Point getEndPoint() {
+        return base.getStartPoint();
+    }
+
+    @Override
+    public CurveInterpolation getInterpolation() {
+        return base.getInterpolation();
+    }
+
+    @Override
+    public double getLength() {
+        return base.getLength();
+    }
+
+    @Override
+    public boolean isClosed() {
+        return base.isClosed();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return base.isEmpty();
+    }
+
+    @Override
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return base.getCoordinateReferenceSystem();
+    }
+
+    @Override
+    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
+        base.setCoordinateReferenceSystem(cs);
+    }
+
+    @Override
+    public AttributesType getAttributesType() {
+        return base.getAttributesType();
+    }
+
+    @Override
+    public Envelope getEnvelope() {
+        return base.getEnvelope();
+    }
+
+    @Override
+    public String asText() {
+        return base.asText();
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedSurface.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedSurface.java
new file mode 100644
index 0000000000..c7cf50c7a1
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedSurface.java
@@ -0,0 +1,107 @@
+/*
+ * 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.internal.shared;
+
+import java.util.Objects;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.Orientable;
+import org.apache.sis.geometries.Primitive;
+import org.apache.sis.geometries.Surface;
+
+
+/**
+ * A surface whose up-normal points the opposite way to the one it is defined 
with.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultReversedSurface extends AbstractGeometry implements 
Surface {
+
+    private final Surface base;
+
+    public DefaultReversedSurface(Surface base) {
+        this.base = Objects.requireNonNull(base);
+    }
+
+    @Override
+    public Sign getOrientationSign() {
+        return Sign.NEGATIVE;
+    }
+
+    @Override
+    public Primitive getPrimitive() {
+        return base;
+    }
+
+    @Override
+    public Orientable getProxy() {
+        return base;
+    }
+
+    @Override
+    public Orientable getReverse() {
+        return base;
+    }
+
+    /**
+     * Returns the geometry type of the base surface: reversing a surface does 
not change what kind
+     * of surface it is.
+     */
+    @Override
+    public String getGeometryType() {
+        return base.getGeometryType();
+    }
+
+    /**
+     * Returns the area of the base surface. Area is unsigned, so the 
orientation does not affect it.
+     */
+    @Override
+    public double getArea() {
+        return base.getArea();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return base.isEmpty();
+    }
+
+    @Override
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return base.getCoordinateReferenceSystem();
+    }
+
+    @Override
+    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
+        base.setCoordinateReferenceSystem(cs);
+    }
+
+    @Override
+    public AttributesType getAttributesType() {
+        return base.getAttributesType();
+    }
+
+    @Override
+    public Envelope getEnvelope() {
+        return base.getEnvelope();
+    }
+
+    @Override
+    public String asText() {
+        return base.asText();
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiCurve.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangulatedSurface.java
similarity index 56%
copy from 
incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiCurve.java
copy to 
incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangulatedSurface.java
index 1085be62aa..f9b3458733 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiCurve.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangulatedSurface.java
@@ -17,42 +17,38 @@
 package org.apache.sis.geometries.internal.shared;
 
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.geometries.Curve;
-import org.apache.sis.geometries.MultiCurve;
+import org.apache.sis.geometries.TIN;
+import org.apache.sis.geometries.Triangle;
 
 
 /**
+ * A surface made entirely of triangular patches.
  *
  * @author Johann Sorel (Geomatys)
  */
-public class DefaultMultiCurve<T extends Curve> extends AbstractGeometry 
implements MultiCurve<T> {
+public class DefaultTriangulatedSurface extends 
DefaultPolyhedralSurface<Triangle> implements TIN {
 
-    private final T[] curves;
-
-    public DefaultMultiCurve(T[] geometries) {
-        this.curves = geometries;
-    }
-
-    @Override
-    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
-        return curves[0].getCoordinateReferenceSystem();
+    public DefaultTriangulatedSurface(Triangle... patches) {
+        super(null, patches);
     }
 
-    @Override
-    public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
-        for (Curve c : curves) {
-            c.setCoordinateReferenceSystem(cs);
-        }
+    public DefaultTriangulatedSurface(CoordinateReferenceSystem fallbackCRS, 
Triangle[] patches) {
+        super(fallbackCRS, patches);
     }
 
+    /**
+     * Returns {@value TIN#TYPE}, not {@code "POLYHEDRALSURFACE"}.
+     */
     @Override
-    public int getNumGeometries() {
-        return curves.length;
+    public String getGeometryType() {
+        return TIN.TYPE;
     }
 
+    /**
+     * Delegates to {@link TIN#asText()}, which formats the triangles as a WKT 
{@code TIN}.
+     */
     @Override
-    public T getGeometryN(int n) {
-        return curves[n];
+    public String asText() {
+        return TIN.super.asText();
     }
-
 }

Reply via email to