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 7fd587c143600a00a0beee6734464ca69d53dae5
Author: jsorel <[email protected]>
AuthorDate: Wed Oct 14 12:14:16 2026 +0200

    feat(Geometry): implement getEnvelope on spline type geometries and 
getBoundary on BBox and OBBox
---
 .../main/org/apache/sis/geometries/BBox.java       | 81 +++++++++++++++++++++-
 .../org/apache/sis/geometries/GeometryFactory.java |  7 +-
 .../main/org/apache/sis/geometries/OBBox.java      | 14 +++-
 .../main/org/apache/sis/geometries/Solid.java      |  8 +--
 .../main/org/apache/sis/geometries/Surface.java    |  7 +-
 .../internal/shared/AbstractGeometry.java          | 56 +++++++++++++++
 .../internal/shared/DefaultBSplineCurve.java       |  6 +-
 .../internal/shared/DefaultBSplineSolid.java       | 18 +++--
 .../internal/shared/DefaultBSplineSurface.java     | 17 +++--
 .../internal/shared/DefaultBilinearGrid.java       | 17 +++--
 .../internal/shared/DefaultCylinder.java           |  6 --
 .../internal/shared/DefaultEllipsoid.java          |  3 +-
 .../internal/shared/DefaultNurbSurface.java        | 14 ++--
 .../internal/shared/DefaultOffsetCurve.java        | 33 ++++++++-
 .../geometries/internal/shared/DefaultPrism.java   | 48 ++++++++++++-
 .../geometries/internal/shared/DefaultSphere.java  |  3 +-
 .../org/apache/sis/geometries/solid/Frustrum.java  | 17 ++++-
 .../sis/geometries/solid/ParametricCurveSolid.java |  8 +--
 .../apache/sis/geometries/surface/Cylinder.java    | 25 +++++++
 .../geometries/surface/ParametricCurveSurface.java |  7 +-
 20 files changed, 335 insertions(+), 60 deletions(-)

diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/BBox.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/BBox.java
index f188a6c71a..50a71fd685 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/BBox.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/BBox.java
@@ -16,14 +16,17 @@
  */
 package org.apache.sis.geometries;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 import org.opengis.coordinate.MismatchedDimensionException;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometries.internal.shared.IndexedPoint;
+import org.apache.sis.geometries.surface.Polygon;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.maths.DataType;
+import org.apache.sis.maths.NDArrays;
 import org.apache.sis.maths.ReadOnly;
 import org.apache.sis.maths.SampleSystem;
 import org.apache.sis.maths.Vectors;
@@ -127,9 +130,85 @@ public final class BBox extends GeneralEnvelope implements 
Geometry {
         return this.clone();
     }
 
+    /**
+     * Returns the boundary of this box: its two end points in 1 dimension, a 
counterclockwise ring
+     * in 2 dimensions, or its six faces oriented outward in 3 dimensions. An 
empty box has an empty
+     * boundary.
+     *
+     * @throws UnsupportedOperationException if this box has more than 3 
dimensions.
+     */
     @Override
     public Geometry getBoundary() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        final int dim = getDimension();
+        CoordinateReferenceSystem crs = getCoordinateReferenceSystem();
+        if (crs == null) {
+            crs = Geometries.getUndefinedCRS(dim);
+        }
+        final GeometryFactory factory = GeometryFactory.DEFAULT;
+        if (isEmpty()) {
+            return factory.createEmpty(crs);
+        }
+        switch (dim) {
+            case 1: {
+                return factory.createMultiPoint(crs, factory.createPoint(crs, 
getMinimum(0)),
+                                                     factory.createPoint(crs, 
getMaximum(0)));
+            }
+            case 2: {
+                final double xmin = getMinimum(0), xmax = getMaximum(0);
+                final double ymin = getMinimum(1), ymax = getMaximum(1);
+                return 
factory.createLinearRing(factory.createDataPoints(NDArrays.of(SampleSystem.of(crs),
+                        xmin, ymin,  xmax, ymin,  xmax, ymax,  xmin, ymax,  
xmin, ymin)));
+            }
+            case 3: {
+                final var corners = new Vector<?>[8];
+                for (int i = 0; i < corners.length; i++) {
+                    final Vector<?> corner = Vectors.create(crs, 
DataType.DOUBLE);
+                    for (int k = 0; k < 3; k++) {
+                        corner.set(k, ((i & (1 << k)) != 0) ? getMaximum(k) : 
getMinimum(k));
+                    }
+                    corners[i] = corner;
+                }
+                return boxFaces(crs, corners, false);
+            }
+            default: {
+                throw new UnsupportedOperationException("The boundary of a box 
is computed in 3 dimensions at most.");
+            }
+        }
+    }
+
+    /**
+     * Returns the six faces of a 3-dimensional box, each one a quadrilateral 
whose vertices are in
+     * counterclockwise order when seen from outside the box, so that its 
up-normal points outward.
+     *
+     * @param  crs      reference system of the corners.
+     * @param  corners  the 8 corners of the box. The corner at index 
<var>i</var> is on the upper side
+     *                  of the box axis <var>k</var> if the bit <var>k</var> 
of <var>i</var> is set.
+     * @param  reverse  whether the box axes are a left-handed system, which 
reverses the orientation.
+     * @return the six faces of the box.
+     */
+    static GeometryCollection<Polygon> boxFaces(final 
CoordinateReferenceSystem crs,
+            final ReadOnly.Vector<?>[] corners, final boolean reverse)
+    {
+        final int[][] faces = {
+            {0, 4, 6, 2},   // Lower side of axis 0.
+            {1, 3, 7, 5},   // Upper side of axis 0.
+            {0, 1, 5, 4},   // Lower side of axis 1.
+            {2, 6, 7, 3},   // Upper side of axis 1.
+            {0, 2, 3, 1},   // Lower side of axis 2.
+            {4, 5, 7, 6}    // Upper side of axis 2.
+        };
+        final GeometryFactory factory = GeometryFactory.DEFAULT;
+        final var polygons = new Polygon[faces.length];
+        for (int f = 0; f < faces.length; f++) {
+            final var ring = new ArrayList<ReadOnly.Vector<?>>(5);
+            for (int k = 0; k < 4; k++) {
+                ring.add(corners[faces[f][reverse ? 3 - k : k]]);
+            }
+            ring.add(ring.get(0));
+            polygons[f] = 
factory.createPolygon(factory.createLinearRing(factory.createDataPoints(
+                    NDArrays.of(ring, SampleSystem.of(crs), 
DataType.DOUBLE))), null);
+        }
+        return factory.createMultiPolygon(crs, polygons);
     }
 
     @Override
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 cbb8e18d00..3ab912a8e3 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
@@ -22,7 +22,6 @@ import java.util.List;
 import java.util.Map;
 import javax.measure.Quantity;
 import javax.measure.Unit;
-import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.SingleCRS;
 import org.apache.sis.geometries.cs.Bearing;
@@ -573,7 +572,7 @@ public final class GeometryFactory {
      * @param  columns        number of columns in the parameter grid.
      * @param  knots          knot values, one sequence per surface parameter, 
or {@code null} if none.
      */
-    public BilinearGrid createBilinearGrid(DataPoints points, 
List<DirectPosition> controlPoints,
+    public BilinearGrid createBilinearGrid(DataPoints points, Array 
controlPoints,
             int rows, int columns, List<double[]> knots)
     {
         return new DefaultBilinearGrid(points, controlPoints, rows, columns, 
knots);
@@ -597,7 +596,7 @@ public final class GeometryFactory {
      * @param  polynomial     {@code true} if the surface is polynomial, 
{@code false} if the control
      *                        points are expressed in homogeneous coordinates, 
making it rational.
      */
-    public BSplineSurface createBSplineSurface(DataPoints points, 
List<DirectPosition> controlPoints,
+    public BSplineSurface createBSplineSurface(DataPoints points, Array 
controlPoints,
             int rows, int columns, List<double[]> knots, int degree, KnotType 
knotSpec,
             BSplineSurfaceForm surfaceForm, boolean polynomial)
     {
@@ -732,7 +731,7 @@ public final class GeometryFactory {
      * @param  columns        number of vertical columns in the parameter grid.
      * @param  files          number of depth files in the parameter grid.
      */
-    public BSplineSolid createBSolidSpline(DataPoints points, 
List<DirectPosition> controlPoints,
+    public BSplineSolid createBSolidSpline(DataPoints points, Array 
controlPoints,
             int rows, int columns, int files)
     {
         return new DefaultBSplineSolid(points, controlPoints, rows, columns, 
files);
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/OBBox.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/OBBox.java
index 5ebb9b4567..5af435198a 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/OBBox.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/OBBox.java
@@ -141,9 +141,21 @@ public final class OBBox extends AbstractGeometry {
         return env;
     }
 
+    /**
+     * Returns the six faces of this box, oriented outward.
+     */
     @Override
     public Geometry getBoundary() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        final var corners = new Vector<?>[8];
+        for (int i = 0; i < corners.length; i++) {
+            final Vector<?> corner = center.copy();
+            corner.add(((i & 1) != 0) ? xAxis : xAxis.copy().scale(-1));
+            corner.add(((i & 2) != 0) ? yAxis : yAxis.copy().scale(-1));
+            corner.add(((i & 4) != 0) ? zAxis : zAxis.copy().scale(-1));
+            corners[i] = corner;
+        }
+        final boolean leftHanded = xAxis.dot(yAxis.cross(zAxis)) < 0;
+        return BBox.boxFaces(getCoordinateReferenceSystem(), corners, 
leftHanded);
     }
 
     /**
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 11f965cc22..4c9bf77492 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
@@ -21,9 +21,9 @@ import javax.measure.Quantity;
 import org.apache.sis.geometries.internal.shared.DefaultSolid;
 import static org.opengis.annotation.Specification.ISO_19107;
 import org.opengis.annotation.UML;
-import org.opengis.geometry.DirectPosition;
 import org.apache.sis.geometries.solid.ParametricCurveSolid;
 import org.apache.sis.geometries.solid.Polyhedron;
+import org.apache.sis.maths.Array;
 
 
 /**
@@ -161,13 +161,13 @@ public sealed interface Solid extends Primitive
      *       {@linkplain #getBoundary() boundary} of this solid.</li>
      * </ul>
      *
-     * @return solid control points, possibly empty.
+     * @return solid control points, or {@code null} if none.
      *
      * @see ISO 19107:2019 - 6.4.28.6
      */
     @UML(identifier="controlPoint", specification=ISO_19107)
-    default List<DirectPosition> getControlPoints() {
-        return List.of();
+    default Array getControlPoints() {
+        return null;
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Surface.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Surface.java
index 00f3850044..7e10f416be 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Surface.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Surface.java
@@ -28,6 +28,7 @@ import org.apache.sis.geometries.surface.Cylinder;
 import org.apache.sis.geometries.surface.ParametricCurveSurface;
 import org.apache.sis.geometries.surface.Polygon;
 import org.apache.sis.geometries.surface.PolyhedralSurface;
+import org.apache.sis.maths.Array;
 import org.apache.sis.maths.Vector;
 import org.apache.sis.measure.Quantities;
 
@@ -266,13 +267,13 @@ public sealed interface Surface extends Orientable
      * Positions used to build the geometry of this surface, the way they are 
used depending on the interpolation.
      * Control points do not necessarily lie on the surface.
      *
-     * @return surface control points, possibly empty.
+     * @return surface control points, or {@code null} if none.
      *
      * @see ISO 19107:2019 - 6.4.25.9
      */
     @UML(identifier="controlPoint", specification=ISO_19107)
-    default List<DirectPosition> getControlPoints() {
-        return List.of();
+    default Array getControlPoints() {
+        return null;
     }
 
     /**
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 88d7c74fac..af03496c60 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
@@ -16,12 +16,18 @@
  */
 package org.apache.sis.geometries.internal.shared;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import org.opengis.geometry.Envelope;
+import org.apache.sis.geometries.DataPoints;
 import org.apache.sis.geometries.Geometries;
 import org.apache.sis.geometries.Geometry;
+import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.maths.Array;
 
 
 /**
@@ -91,6 +97,56 @@ public abstract non-sealed class AbstractGeometry implements 
Geometry {
         return union;
     }
 
+    /**
+     * Returns the envelope of the given data points and control points, or 
{@code null} if both are empty.
+     * A spline whose basis functions are a partition of unity, such as a 
b-spline, stays inside the convex
+     * hull of its control points, so the returned envelope contains it.
+     *
+     * @param  points         data points of the spline, or {@code null} if 
none.
+     * @param  controlPoints  coordinates of the control points, possibly 
empty.
+     * @param  homogeneous    whether the control points have one more 
coordinate than the reference system,
+     *                        the weight by which the other coordinates are 
divided. Weights are assumed positive.
+     * @return envelope of the data points and of the control points, or 
{@code null} if there is none.
+     */
+    protected static Envelope controlHullEnvelope(final DataPoints points, 
final List<double[]> controlPoints,
+                                                  final boolean homogeneous)
+    {
+        GeneralEnvelope envelope = (points == null || points.isEmpty()) ? null 
: new GeneralEnvelope(points.getEnvelope());
+        for (double[] coordinates : controlPoints) {
+            if (homogeneous) {
+                final double weight = coordinates[coordinates.length - 1];
+                coordinates = Arrays.copyOf(coordinates, coordinates.length - 
1);
+                for (int i = 0; i < coordinates.length; i++) {
+                    coordinates[i] /= weight;
+                }
+            }
+            final var position = new GeneralDirectPosition(coordinates);
+            if (envelope == null) {
+                envelope = new GeneralEnvelope(position, position);
+                if (points != null) {
+                    
envelope.setCoordinateReferenceSystem(points.getCoordinateReferenceSystem());
+                }
+            } else {
+                envelope.add(position);
+            }
+        }
+        return envelope;
+    }
+
+    /**
+     * Returns the coordinates of the given control points, or an empty list 
if {@code null}.
+     */
+    protected static List<double[]> coordinates(final Array controlPoints) {
+        if (controlPoints == null) {
+            return List.of();
+        }
+        final var coordinates = new ArrayList<double[]>();
+        for (long i = 0, n = controlPoints.getLength(); i < n; i++) {
+            coordinates.add(controlPoints.get(i).toArrayDouble());
+        }
+        return coordinates;
+    }
+
     /**
      * 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.
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineCurve.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineCurve.java
index 661ab93fe1..97c27f2e9d 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineCurve.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineCurve.java
@@ -173,9 +173,13 @@ public non-sealed class DefaultBSplineCurve extends 
AbstractGeometry implements
         return points.isEmpty();
     }
 
+    /**
+     * Returns the envelope of the data points and of the control points, 
which contains this curve
+     * since a b-spline stays inside the convex hull of its control points.
+     */
     @Override
     public Envelope getEnvelope() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        return controlHullEnvelope(points, coordinates(controlPoints), 
rational);
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineSolid.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineSolid.java
index 0cdd9b695d..95a6520574 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineSolid.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineSolid.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.geometries.internal.shared;
 
-import java.util.List;
 import java.util.Objects;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.Envelope;
@@ -29,6 +28,7 @@ import org.apache.sis.geometries.DataPointsType;
 import org.apache.sis.geometries.GeometryCollection;
 import org.apache.sis.geometries.Surface;
 import org.apache.sis.geometries.solid.BSplineSolid;
+import org.apache.sis.maths.Array;
 
 
 /**
@@ -44,9 +44,9 @@ public non-sealed class DefaultBSplineSolid extends 
AbstractGeometry implements
     protected final DataPoints points;
 
     /**
-     * Control points of the section curves, in row-major order, or an empty 
list if none.
+     * Control points in row-major order, or {@code null} if none.
      */
-    protected final List<DirectPosition> controlPoints;
+    protected final Array controlPoints;
 
     /**
      * Number of horizontal rows in the parameter grid.
@@ -73,11 +73,11 @@ public non-sealed class DefaultBSplineSolid extends 
AbstractGeometry implements
      * @param  columns        number of vertical columns in the parameter grid.
      * @param  files          number of depth files in the parameter grid.
      */
-    public DefaultBSplineSolid(final DataPoints points, final 
List<DirectPosition> controlPoints,
+    public DefaultBSplineSolid(final DataPoints points, final Array 
controlPoints,
             final int rows, final int columns, final int files)
     {
         this.points        = Objects.requireNonNull(points);
-        this.controlPoints = (controlPoints == null) ? List.of() : 
List.copyOf(controlPoints);
+        this.controlPoints = controlPoints;
         this.rows          = rows;
         this.columns       = columns;
         this.files         = files;
@@ -104,7 +104,7 @@ public non-sealed class DefaultBSplineSolid extends 
AbstractGeometry implements
     }
 
     @Override
-    public List<DirectPosition> getControlPoints() {
+    public Array getControlPoints() {
         return controlPoints;
     }
 
@@ -172,9 +172,13 @@ public non-sealed class DefaultBSplineSolid extends 
AbstractGeometry implements
         return points.isEmpty();
     }
 
+    /**
+     * Returns the envelope of the data points and of the control points, 
which contains this solid
+     * since a b-spline stays inside the convex hull of its control points.
+     */
     @Override
     public Envelope getEnvelope() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        return controlHullEnvelope(points, coordinates(controlPoints), false);
     }
 
     @Override
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineSurface.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineSurface.java
index 943d4c8207..2db202201e 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineSurface.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBSplineSurface.java
@@ -27,6 +27,7 @@ import org.apache.sis.geometries.Geometry;
 import org.apache.sis.geometries.curve.KnotType;
 import org.apache.sis.geometries.surface.BSplineSurface;
 import org.apache.sis.geometries.surface.BSplineSurfaceForm;
+import org.apache.sis.maths.Array;
 
 
 /**
@@ -46,9 +47,9 @@ public non-sealed class DefaultBSplineSurface extends 
AbstractGeometry implement
     protected final DataPoints points;
 
     /**
-     * Control points of the section curves, in row-major order, or an empty 
list if none.
+     * Control points in row-major order, or {@code null} if none.
      */
-    protected final List<DirectPosition> controlPoints;
+    protected final Array controlPoints;
 
     /**
      * Number of rows in the parameter grid.
@@ -102,12 +103,12 @@ public non-sealed class DefaultBSplineSurface extends 
AbstractGeometry implement
      *                        control points are expressed in homogeneous 
coordinates, which makes
      *                        it rational.
      */
-    public DefaultBSplineSurface(final DataPoints points, final 
List<DirectPosition> controlPoints,
+    public DefaultBSplineSurface(final DataPoints points, final Array 
controlPoints,
             final int rows, final int columns, final List<double[]> knots, 
final int degree,
             final KnotType knotSpec, final BSplineSurfaceForm surfaceForm, 
final boolean polynomial)
     {
         this.points        = Objects.requireNonNull(points);
-        this.controlPoints = (controlPoints == null) ? List.of() : 
List.copyOf(controlPoints);
+        this.controlPoints = controlPoints;
         this.rows          = rows;
         this.columns       = columns;
         this.knots         = (knots == null) ? List.of() : List.copyOf(knots);
@@ -158,7 +159,7 @@ public non-sealed class DefaultBSplineSurface extends 
AbstractGeometry implement
     }
 
     @Override
-    public List<DirectPosition> getControlPoints() {
+    public Array getControlPoints() {
         return controlPoints;
     }
 
@@ -182,9 +183,13 @@ public non-sealed class DefaultBSplineSurface extends 
AbstractGeometry implement
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
+    /**
+     * Returns the envelope of the data points and of the control points, 
which contains this surface
+     * since a b-spline stays inside the convex hull of its control points.
+     */
     @Override
     public Envelope getEnvelope() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        return controlHullEnvelope(points, coordinates(controlPoints), 
!polynomial);
     }
 
     @Override
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBilinearGrid.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBilinearGrid.java
index 92dedded85..f744d08321 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBilinearGrid.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBilinearGrid.java
@@ -28,6 +28,7 @@ import org.apache.sis.geometries.Geometry;
 import org.apache.sis.geometries.GeometryType;
 import org.apache.sis.geometries.DataPointsType;
 import org.apache.sis.geometries.surface.BilinearGrid;
+import org.apache.sis.maths.Array;
 
 
 /**
@@ -44,9 +45,9 @@ public non-sealed class DefaultBilinearGrid extends 
AbstractGeometry implements
     protected final DataPoints points;
 
     /**
-     * Control points of the section curves, in row-major order, or an empty 
list if none.
+     * Control points of the section curves, in row-major order, or {@code 
null} if none.
      */
-    protected final List<DirectPosition> controlPoints;
+    protected final Array controlPoints;
 
     /**
      * Number of rows in the parameter grid.
@@ -75,11 +76,11 @@ public non-sealed class DefaultBilinearGrid extends 
AbstractGeometry implements
      * @param  columns        number of columns in the parameter grid.
      * @param  knots          knot values, one sequence per surface parameter, 
or {@code null} if none.
      */
-    public DefaultBilinearGrid(final DataPoints points, final 
List<DirectPosition> controlPoints,
+    public DefaultBilinearGrid(final DataPoints points, final Array 
controlPoints,
             final int rows, final int columns, final List<double[]> knots)
     {
         this.points        = Objects.requireNonNull(points);
-        this.controlPoints = (controlPoints == null) ? List.of() : 
List.copyOf(controlPoints);
+        this.controlPoints = controlPoints;
         this.rows          = rows;
         this.columns       = columns;
         this.knots         = (knots == null) ? List.of() : List.copyOf(knots);
@@ -101,7 +102,7 @@ public non-sealed class DefaultBilinearGrid extends 
AbstractGeometry implements
     }
 
     @Override
-    public List<DirectPosition> getControlPoints() {
+    public Array getControlPoints() {
         return controlPoints;
     }
 
@@ -166,9 +167,13 @@ public non-sealed class DefaultBilinearGrid extends 
AbstractGeometry implements
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
+    /**
+     * Returns the envelope of the grid points. Each cell is a bilinear 
interpolation of its four
+     * corners with positive weights, so it stays inside the box of those 
corners.
+     */
     @Override
     public Envelope getEnvelope() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        return points.isEmpty() ? null : points.getEnvelope();
     }
 
     @Override
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCylinder.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCylinder.java
index cffd3f385f..46077c3f8f 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCylinder.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCylinder.java
@@ -17,7 +17,6 @@
 package org.apache.sis.geometries.internal.shared;
 
 import java.util.Objects;
-import javax.measure.Quantity;
 import org.apache.sis.geometries.DataPoints;
 import org.apache.sis.geometries.GeometryFactory;
 import org.apache.sis.geometries.surface.Cylinder;
@@ -120,11 +119,6 @@ public non-sealed class DefaultCylinder extends 
AbstractGeometry implements Cyli
         this.radiusBottom = radius;
     }
 
-    @Override
-    public Quantity<?> getArea() {
-        throw new UnsupportedOperationException("Not supported yet.");
-    }
-
     @Override
     public boolean isClosed() {
         return closed;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultEllipsoid.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultEllipsoid.java
index 40b491e35b..c4bb81d492 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultEllipsoid.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultEllipsoid.java
@@ -26,6 +26,7 @@ import org.apache.sis.geometries.DataPoints;
 import org.apache.sis.geometries.Geometries;
 import org.apache.sis.geometries.GeometryFactory;
 import org.apache.sis.geometries.surface.Ellipsoid;
+import org.apache.sis.maths.Array;
 import org.apache.sis.maths.DataType;
 import org.apache.sis.maths.NDArrays;
 import org.apache.sis.maths.ReadOnly;
@@ -205,7 +206,7 @@ public non-sealed class DefaultEllipsoid extends 
AbstractGeometry implements Ell
     }
 
     @Override
-    public List<DirectPosition> getControlPoints() {
+    public Array getControlPoints() {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultNurbSurface.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultNurbSurface.java
index e541aad4bb..6c57b652fb 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultNurbSurface.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultNurbSurface.java
@@ -29,6 +29,7 @@ import org.apache.sis.geometries.curve.KnotType;
 import org.apache.sis.geometries.operation.DeBoorAlgorithm;
 import org.apache.sis.geometries.surface.BSplineSurfaceForm;
 import org.apache.sis.geometries.surface.NurbSurface;
+import org.apache.sis.maths.Array;
 import org.apache.sis.maths.NDArrays;
 import org.apache.sis.maths.Vector;
 import org.apache.sis.maths.Vectors;
@@ -371,17 +372,16 @@ public non-sealed class DefaultNurbSurface extends 
AbstractGeometry implements N
     }
 
     /**
-     * Returns views of the control points in row-major order, without their 
weights.
+     * Returns a copy of the control points in row-major order, without their 
weights.
      */
     @Override
-    public List<DirectPosition> getControlPoints() {
-        final var positions = new ArrayList<DirectPosition>(getRows() * 
getColumns());
+    public Array getControlPoints() {
+        final var positions = new ArrayList<Vector<?>>(getRows() * 
getColumns());
         for (final Vector<?>[] row : controlPoints) {
-            for (final Vector<?> p : row) {
-                positions.add(Vectors.asDirectPostion(p));
-            }
+            positions.addAll(Arrays.asList(row));
         }
-        return positions;
+        final Vector<?> first = controlPoints[0][0];
+        return NDArrays.of(positions, first.getSampleSystem(), 
first.getDataType());
     }
 
     @Override
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultOffsetCurve.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultOffsetCurve.java
index ccb47a4c91..c92ef3f4da 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultOffsetCurve.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultOffsetCurve.java
@@ -17,7 +17,9 @@
 package org.apache.sis.geometries.internal.shared;
 
 import java.util.Objects;
+import javax.measure.IncommensurableException;
 import javax.measure.Quantity;
+import javax.measure.Unit;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometries.Curve;
@@ -25,7 +27,9 @@ import org.apache.sis.geometries.DataPoints;
 import org.apache.sis.geometries.DataPointsType;
 import org.apache.sis.geometries.cs.Bearing;
 import org.apache.sis.geometries.curve.OffsetCurve;
+import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.maths.Array;
+import org.apache.sis.measure.Units;
 
 
 /**
@@ -114,9 +118,36 @@ public non-sealed class DefaultOffsetCurve extends 
AbstractGeometry implements O
         return baseCurve.isEmpty();
     }
 
+    /**
+     * Returns the envelope of the base curve expanded by the offset distance 
on every axis.
+     * This envelope contains this curve, but may be larger than needed since 
the curve is
+     * offset on one side only and not along every axis.
+     *
+     * @throws UnsupportedOperationException if the distance unit cannot be 
converted to the unit of an axis.
+     */
     @Override
     public Envelope getEnvelope() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        final Envelope baseEnvelope = baseCurve.getEnvelope();
+        if (baseEnvelope == null) {
+            return null;
+        }
+        final var envelope = new GeneralEnvelope(baseEnvelope);
+        final CoordinateReferenceSystem crs = 
envelope.getCoordinateReferenceSystem();
+        final double value = Math.abs(distance.getValue().doubleValue());
+        for (int i = 0, n = envelope.getDimension(); i < n; i++) {
+            double d = value;
+            final Unit<?> axisUnit = (crs != null) ? 
crs.getCoordinateSystem().getAxis(i).getUnit() : null;
+            if (axisUnit != null && !Units.UNITY.equals(distance.getUnit())) {
+                try {
+                    d = 
distance.getUnit().getConverterToAny(axisUnit).convert(value);
+                } catch (IncommensurableException e) {
+                    throw new UnsupportedOperationException("The offset 
distance in " + distance.getUnit()
+                            + " cannot be converted to the unit of axis " + i 
+ " (" + axisUnit + ").", e);
+                }
+            }
+            envelope.setRange(i, envelope.getMinimum(i) - d, 
envelope.getMaximum(i) + d);
+        }
+        return envelope;
     }
 
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPrism.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPrism.java
index 90aeba25b4..ea00a68ebc 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPrism.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPrism.java
@@ -20,9 +20,13 @@ import java.util.Objects;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.util.FactoryException;
 import org.apache.sis.geometries.Geometry;
 import org.apache.sis.geometries.Prism;
+import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.measure.NumberRange;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.util.Utilities;
 
 
 /**
@@ -90,9 +94,51 @@ public non-sealed class DefaultPrism extends 
AbstractGeometry implements Prism {
         return base.isEmpty();
     }
 
+    /**
+     * Returns the envelope of the base extended by the extrusion range. If 
the reference system of the base
+     * contains the extrusion reference system, the range extends the 
corresponding axis. Otherwise the
+     * extrusion adds a dimension, and the envelope is in the compound of the 
two reference systems.
+     */
     @Override
     public Envelope getEnvelope() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        final Envelope baseEnvelope = base.getEnvelope();
+        if (baseEnvelope == null) {
+            return null;
+        }
+        final double min = extrusionRange.getMinDouble();
+        final double max = extrusionRange.getMaxDouble();
+        final CoordinateReferenceSystem baseCRS = 
base.getCoordinateReferenceSystem();
+        if (baseCRS != null && extrusionCrs != null) {
+            int offset = 0;
+            for (final SingleCRS component : CRS.getSingleComponents(baseCRS)) 
{
+                final int dimension = 
component.getCoordinateSystem().getDimension();
+                if (Utilities.equalsIgnoreMetadata(component, extrusionCrs)) {
+                    final var envelope = new GeneralEnvelope(baseEnvelope);
+                    for (int i = offset; i < offset + dimension; i++) {
+                        envelope.setRange(i, Math.min(envelope.getMinimum(i), 
min),
+                                             Math.max(envelope.getMaximum(i), 
max));
+                    }
+                    return envelope;
+                }
+                offset += dimension;
+            }
+        }
+        final int n = baseEnvelope.getDimension();
+        final GeneralEnvelope envelope;
+        if (baseCRS != null && extrusionCrs != null) {
+            try {
+                envelope = new GeneralEnvelope(CRS.compound(baseCRS, 
extrusionCrs));
+            } catch (FactoryException e) {
+                throw new IllegalStateException("Cannot combine the base and 
extrusion reference systems.", e);
+            }
+        } else {
+            envelope = new GeneralEnvelope(n + 1);
+        }
+        for (int i = 0; i < n; i++) {
+            envelope.setRange(i, baseEnvelope.getMinimum(i), 
baseEnvelope.getMaximum(i));
+        }
+        envelope.setRange(n, min, max);
+        return envelope;
     }
 
     @Override
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultSphere.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultSphere.java
index 90a9c7d109..a25f66c120 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultSphere.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultSphere.java
@@ -24,6 +24,7 @@ import org.apache.sis.geometries.DataPoints;
 import org.apache.sis.geometries.Geometries;
 import org.apache.sis.geometries.GeometryFactory;
 import org.apache.sis.geometries.surface.Sphere;
+import org.apache.sis.maths.Array;
 import org.apache.sis.maths.DataType;
 import org.apache.sis.maths.NDArrays;
 import org.apache.sis.maths.ReadOnly;
@@ -137,7 +138,7 @@ public non-sealed class DefaultSphere extends 
AbstractGeometry implements Sphere
     }
 
     @Override
-    public List<DirectPosition> getControlPoints() {
+    public Array getControlPoints() {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/Frustrum.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/Frustrum.java
index da5c29511c..4cc00bc52b 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/Frustrum.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/Frustrum.java
@@ -55,19 +55,30 @@ public final class Frustrum extends AbstractGeometry {
         return planes;
     }
 
+    /**
+     * Returns the reference system of the planes, taken from the first one.
+     */
     @Override
     public CoordinateReferenceSystem getCoordinateReferenceSystem() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        return planes[0].getCoordinateReferenceSystem();
     }
 
+    /**
+     * Sets the reference system of all the planes.
+     */
     @Override
     public void setCoordinateReferenceSystem(CoordinateReferenceSystem crs) 
throws IllegalArgumentException {
-        throw new UnsupportedOperationException("Not supported yet.");
+        for (final Plane plane : planes) {
+            plane.setCoordinateReferenceSystem(crs);
+        }
     }
 
+    /**
+     * Returns the type of the data points of the planes, taken from the first 
one.
+     */
     @Override
     public DataPointsType getDataPointsType() {
-        throw new UnsupportedOperationException("Not supported yet.");
+        return planes[0].getDataPointsType();
     }
 
     @Override
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/ParametricCurveSolid.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/ParametricCurveSolid.java
index 15aa333019..52e2d5582a 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/ParametricCurveSolid.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/ParametricCurveSolid.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.geometries.solid;
 
-import java.util.List;
 import static org.opengis.annotation.Specification.ISO_19107;
 import org.opengis.annotation.UML;
 import org.opengis.geometry.DirectPosition;
@@ -25,6 +24,7 @@ import org.apache.sis.geometries.DataPoints;
 import org.apache.sis.geometries.GeometryType;
 import org.apache.sis.geometries.Solid;
 import org.apache.sis.geometries.SolidInterpolation;
+import org.apache.sis.maths.Array;
 
 
 /**
@@ -163,18 +163,18 @@ public sealed interface ParametricCurveSolid extends Solid
      *
      * <p>Constraints:</p>
      * <ul>
-     *   <li>The list holds {@link #getRows()} × {@link #getColumns()} × 
{@link #getFiles()}
+     *   <li>The array holds {@link #getRows()} × {@link #getColumns()} × 
{@link #getFiles()}
      *       positions, stored in row-major order.</li>
      *   <li>Control points do not necessarily lie inside this solid.</li>
      * </ul>
      *
-     * @return solid control points, possibly empty.
+     * @return solid control points, or {@code null} if none.
      *
      * @see ISO 19107:2019 - 9.3.1.3
      */
     @UML(identifier="controlPoints", specification=ISO_19107)
     @Override
-    List<DirectPosition> getControlPoints();
+    Array getControlPoints();
 
     /**
      * Returns the curve traversing this solid horizontally at the given 
vertical and depth parameters.
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Cylinder.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Cylinder.java
index 73b4b4d232..08f6f758d2 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Cylinder.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Cylinder.java
@@ -18,11 +18,13 @@ package org.apache.sis.geometries.surface;
 
 import java.util.ArrayList;
 import java.util.List;
+import javax.measure.Quantity;
 import static org.opengis.annotation.Specification.ISO_12113;
 import org.opengis.annotation.UML;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometries.BBox;
 import org.apache.sis.geometries.DataPointsType;
+import org.apache.sis.geometries.Geometries;
 import org.apache.sis.geometries.Geometry;
 import org.apache.sis.geometries.GeometryFactory;
 import org.apache.sis.geometries.GeometryType;
@@ -35,6 +37,7 @@ import org.apache.sis.maths.NDArrays;
 import org.apache.sis.maths.ReadOnly;
 import org.apache.sis.maths.Vectors;
 import org.apache.sis.maths.Vector;
+import org.apache.sis.measure.Quantities;
 
 
 /**
@@ -218,6 +221,28 @@ public sealed interface Cylinder extends Surface
         return bbox;
     }
 
+    /**
+     * Returns the area of the lateral surface, π(r₁ + r₂)⋅√(h² + (r₁ − r₂)²) 
where <var>h</var> is the
+     * {@linkplain #getHeight() height}, plus the areas π<var>r</var>² of the 
two end disks if this
+     * cylinder is {@linkplain #isClosed() closed}.
+     *
+     * @throws UnsupportedOperationException if this cylinder is not 
3-dimensional.
+     */
+    @Override
+    default Quantity<?> getArea() {
+        if (getDataPoints().getDimension() != 3) {
+            throw new UnsupportedOperationException("The area of a cylinder is 
computed in 3 dimensions only.");
+        }
+        final double r1 = getRadiusBottom();
+        final double r2 = getRadiusTop();
+        final double h  = getHeight();
+        double area = Math.PI * (r1 + r2) * Math.hypot(h, r1 - r2);
+        if (isClosed()) {
+            area += Math.PI * (r1*r1 + r2*r2);
+        }
+        return Quantities.create(area, Geometries.getAreaUnit(this));
+    }
+
     /**
      * Returns the empty geometry if this cylinder is {@linkplain #isClosed() 
closed},
      * or the circles at its ends otherwise.
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/ParametricCurveSurface.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/ParametricCurveSurface.java
index 6e61608751..0d0bd0e803 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/ParametricCurveSurface.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/ParametricCurveSurface.java
@@ -27,6 +27,7 @@ import org.apache.sis.geometries.DataPoints;
 import org.apache.sis.geometries.GeometryType;
 import org.apache.sis.geometries.Surface;
 import org.apache.sis.geometries.SurfaceInterpolation;
+import org.apache.sis.maths.Array;
 
 // Specific to the geoapi-4.0 branch:
 import org.opengis.metadata.Identifier;
@@ -110,18 +111,18 @@ public sealed interface ParametricCurveSurface extends 
Surface, ReferenceSystem
      *
      * <p>Constraints:</p>
      * <ul>
-     *   <li>The list holds {@link #getRows()} × {@link #getColumns()} 
positions,
+     *   <li>The array holds {@link #getRows()} × {@link #getColumns()} 
positions,
      *       stored in row-major order.</li>
      *   <li>Control points do not necessarily lie on this surface.</li>
      * </ul>
      *
-     * @return surface control points, possibly empty.
+     * @return surface control points, or {@code null} if none.
      *
      * @see ISO 19107:2019 - 8.3.2.6
      */
     @UML(identifier="controlPoints", specification=ISO_19107)
     @Override
-    List<DirectPosition> getControlPoints();
+    Array getControlPoints();
 
     /**
      * Positions of this surface at the knots of the parameter grid, so that 
the data point at

Reply via email to