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 024fa636e6f768381f6d29d3ce9902feb9e39eab
Author: jsorel <[email protected]>
AuthorDate: Fri Oct 16 10:41:05 2026 +0200

    feat(Geometry): implement Prism.toExtrusion()
---
 .../main/org/apache/sis/geometries/Prism.java      |  27 ++
 .../geometries/internal/shared/DefaultPrism.java   | 321 +++++++++++++++++++++
 .../geometries/operation/GeometryProcessor.java    |   4 +
 .../test/org/apache/sis/geometries/PrismTest.java  | 211 ++++++++++++++
 .../sis/geometries/operation/ToPrimitiveTest.java  |  39 +++
 5 files changed, 602 insertions(+)

diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Prism.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Prism.java
index 51a38718ee..0b0779d589 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Prism.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Prism.java
@@ -76,4 +76,31 @@ public sealed interface Prism extends Geometry
      */
     SingleCRS getExtrusionCrs();
 
+    /**
+     * Returns the geometry covered by the extrusion of the base, which 
depends on the type of the base:
+     *
+     * <ul>
+     *   <li>A point becomes the line string going from the lower limit to the 
upper limit.</li>
+     *   <li>A line string becomes a polyhedral surface made of one 
quadrilateral for each segment.</li>
+     *   <li>A polygon becomes a polyhedron whose exterior shell is made of 
the polygon at the lower limit,
+     *       the polygon at the upper limit, and one quadrilateral for each 
segment of its rings.
+     *       All the faces are oriented outward.</li>
+     *   <li>A geometry collection becomes the collection of the extrusions of 
its elements,
+     *       which shall be of the above types or collections of them.</li>
+     * </ul>
+     *
+     * <p>If the reference system of the base contains the extrusion reference 
system, the extrusion
+     * replaces the coordinate of the corresponding axis. Otherwise the 
extrusion adds a dimension,
+     * and the result is in the compound of the two reference systems.</p>
+     *
+     * <p>Each position of the result is a copy of a position of the base, 
with the extrusion
+     * coordinate set to one of the limits. The other attributes of that 
position are copied unchanged.</p>
+     *
+     * @return the extrusion of the base.
+     * @throws UnsupportedOperationException if the base or one of its 
elements is of another type,
+     *         if the extrusion reference system has more than one dimension,
+     *         or if the extrusion of a polygon would not be 3-dimensional.
+     */
+    Geometry toExtrusion();
+
 }
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 f035562b4a..fdda173d48 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
@@ -16,14 +16,36 @@
  */
 package org.apache.sis.geometries.internal.shared;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 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.DataPoints;
+import org.apache.sis.geometries.DataPointsType;
+import org.apache.sis.geometries.Geometries;
 import org.apache.sis.geometries.Geometry;
+import org.apache.sis.geometries.GeometryCollection;
+import org.apache.sis.geometries.GeometryFactory;
+import org.apache.sis.geometries.GeometryType;
+import org.apache.sis.geometries.Point;
 import org.apache.sis.geometries.Prism;
+import org.apache.sis.geometries.curve.LineString;
+import org.apache.sis.geometries.curve.LinearRing;
+import org.apache.sis.geometries.solid.Polyhedron;
+import org.apache.sis.geometries.surface.Polygon;
+import org.apache.sis.geometries.surface.PolyhedralSurface;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.maths.Array;
+import org.apache.sis.maths.NDArrays;
+import org.apache.sis.maths.SampleSystem;
+import org.apache.sis.maths.Vector;
+import org.apache.sis.maths.Vectors;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.util.Utilities;
@@ -141,6 +163,305 @@ public non-sealed class DefaultPrism extends 
AbstractGeometry implements Prism {
         return envelope;
     }
 
+    /**
+     * Returns the geometry covered by the extrusion of the base.
+     */
+    @Override
+    public Geometry toExtrusion() {
+        return new Extrusion().extrude(base);
+    }
+
+    /**
+     * Builds the extrusion of the base and of its elements. This class holds 
the reference system of the
+     * extruded geometries and the axis on which the extrusion is done, which 
are the same for all elements.
+     */
+    private final class Extrusion {
+        /**
+         * Factory of the extruded geometries.
+         */
+        private final GeometryFactory factory = GeometryFactory.DEFAULT;
+
+        /**
+         * Reference system of the extruded geometries.
+         */
+        private final CoordinateReferenceSystem crs;
+
+        /**
+         * Index of the extrusion axis in {@link #crs}, which is the dimension 
of the base if the extrusion adds a dimension.
+         */
+        private final int axis;
+
+        /**
+         * Lower and upper limits of the extrusion.
+         */
+        private final double min, max;
+
+        /**
+         * Finds the reference system and the axis of the extrusion.
+         * The extrusion is along an axis of the base if the extrusion 
reference system is a component
+         * of the base one. Otherwise the extrusion adds a dimension after the 
dimensions of the base.
+         */
+        Extrusion() {
+            if (extrusionCrs != null && 
extrusionCrs.getCoordinateSystem().getDimension() != 1) {
+                throw new UnsupportedOperationException("The extrusion of a 
prism is computed along a single axis only.");
+            }
+            final CoordinateReferenceSystem baseCRS = 
base.getCoordinateReferenceSystem();
+            final int baseDimension = 
baseCRS.getCoordinateSystem().getDimension();
+            CoordinateReferenceSystem target = null;
+            int index = baseDimension;
+            if (extrusionCrs != null) {
+                int offset = 0;
+                for (final SingleCRS component : 
CRS.getSingleComponents(baseCRS)) {
+                    if (Utilities.equalsIgnoreMetadata(component, 
extrusionCrs)) {
+                        target = baseCRS;
+                        index  = offset;
+                        break;
+                    }
+                    offset += component.getCoordinateSystem().getDimension();
+                }
+            }
+            if (target == null) {
+                if (extrusionCrs == null) {
+                    target = Geometries.getUndefinedCRS(baseDimension + 1);
+                } else {
+                    try {
+                        target = CRS.compound(baseCRS, extrusionCrs);
+                    } catch (FactoryException e) {
+                        throw new IllegalStateException("Cannot combine the 
base and extrusion reference systems.", e);
+                    }
+                }
+            }
+            crs  = target;
+            axis = index;
+            min  = extrusionRange.getMinDouble();
+            max  = extrusionRange.getMaxDouble();
+        }
+
+        /**
+         * Returns the extrusion of the given geometry, which is the base or 
one of its elements.
+         */
+        Geometry extrude(final Geometry geometry) {
+            if (geometry instanceof Point point) {
+                return extrude(point);
+            } else if (geometry instanceof LineString line) {
+                return extrude(line);
+            } else if (geometry instanceof Polygon polygon) {
+                return extrude(polygon);
+            } else if (geometry instanceof GeometryCollection<?> collection) {
+                return extrude(collection);
+            }
+            throw new UnsupportedOperationException("Cannot extrude a " + 
geometry.getGeometryType() + '.');
+        }
+
+        /**
+         * Returns the extrusion of a point, which is the line string from the 
lower limit to the upper limit.
+         */
+        private LineString extrude(final Point point) {
+            return factory.createLineString(copy(point.getDataPoints(), new 
int[] {0, 0}, new double[] {min, max}));
+        }
+
+        /**
+         * Returns the extrusion of a line string, which is a polyhedral 
surface
+         * made of one quadrilateral for each segment of the line string.
+         */
+        private PolyhedralSurface<Polygon> extrude(final LineString line) {
+            final DataPoints points = line.getDataPoints();
+            final int[] order = new int[points.size()];
+            Arrays.setAll(order, (i) -> i);
+            final var faces = new ArrayList<Polygon>();
+            walls(points, order, faces);
+            return factory.createPolyhedralSurface(crs, 
faces.toArray(Polygon[]::new));
+        }
+
+        /**
+         * Returns the extrusion of a polygon, which is a polyhedron whose 
exterior shell is made of the polygon
+         * at the lower limit, the polygon at the upper limit and the walls of 
the extrusion of its rings.
+         *
+         * <p>The rings of the polygon are first ordered so that, seen from 
the upper side of the extrusion axis,
+         * the exterior one is counterclockwise and the holes are clockwise. 
The upper face then keeps that order,
+         * the lower face reverses it, and the walls are built as for line 
strings, which makes all the faces
+         * oriented outward.</p>
+         */
+        private Polyhedron extrude(final Polygon polygon) {
+            if (crs.getCoordinateSystem().getDimension() != 3) {
+                throw new UnsupportedOperationException("The extrusion of a 
polygon is computed in 3 dimensions only.");
+            }
+            if (polygon.isEmpty()) {
+                return 
factory.createPolyhedron(factory.createMultiPolygon(crs), List.of());
+            }
+            /*
+             * The two other axes, in increasing order, followed by the 
extrusion axis form a right-handed
+             * system if they are an even permutation of (0, 1, 2), which is 
the case unless the extrusion
+             * axis is in the middle. In a left-handed system, the 
counterclockwise order is reversed.
+             */
+            final int h0 = (axis == 0) ? 1 : 0;
+            final int h1 = (axis == 2) ? 1 : 2;
+            final boolean rightHanded = (axis != 1);
+            final var rings = new ArrayList<DataPoints>();
+            rings.add(polygon.getExteriorRing().getDataPoints());
+            for (final LinearRing hole : polygon.getInteriorRings()) {
+                rings.add(hole.getDataPoints());
+            }
+            final var orders = new ArrayList<int[]>(rings.size());
+            for (int k = 0; k < rings.size(); k++) {
+                orders.add(order(rings.get(k), h0, h1, (k == 0) == 
rightHanded));
+            }
+            final var faces = new ArrayList<Polygon>();
+            faces.add(cap(rings, orders, min, true));
+            faces.add(cap(rings, orders, max, false));
+            for (int k = 0; k < rings.size(); k++) {
+                walls(rings.get(k), orders.get(k), faces);
+            }
+            return factory.createPolyhedron(factory.createMultiPolygon(crs, 
faces.toArray(Polygon[]::new)), List.of());
+        }
+
+        /**
+         * Returns the collection of the extrusions of the elements of the 
given collection.
+         */
+        private GeometryCollection<Geometry> extrude(final 
GeometryCollection<?> collection) {
+            final var elements = new Geometry[collection.getNumGeometries()];
+            for (int i = 0; i < elements.length; i++) {
+                elements[i] = extrude(collection.getGeometryN(i));
+            }
+            final GeometryType type = switch (collection.getElementType()) {
+                case POINT                  -> GeometryType.LINESTRING;
+                case LINESTRING, LINEARRING -> GeometryType.POLYHEDRALSURFACE;
+                case POLYGON, TRIANGLE      -> GeometryType.POLYHEDRON;
+                default                     -> GeometryType.GEOMETRY;
+            };
+            return factory.createGeometryCollection(type, crs, elements);
+        }
+
+        /**
+         * Adds to the given list the walls of the extrusion of the given 
positions. The wall of the segment
+         * going from <var>a</var> to <var>b</var> goes from the lower to the 
upper limit along <var>b</var>,
+         * then back to the lower limit along <var>a</var>.
+         *
+         * @param  points  the positions to extrude.
+         * @param  order   indices of the positions in the order of the 
segments.
+         * @param  faces   where to add the walls.
+         */
+        private void walls(final DataPoints points, final int[] order, final 
List<Polygon> faces) {
+            for (int i = 1; i < order.length; i++) {
+                final int a = order[i-1];
+                final int b = order[i];
+                if (!isRepeated(points, a, b)) {
+                    
faces.add(factory.createPolygon(factory.createLinearRing(copy(points,
+                            new int[]    {a,   b,   b,   a,   a},
+                            new double[] {min, min, max, max, min})), null));
+                }
+            }
+        }
+
+        /**
+         * Returns whether the two given positions have the same coordinates, 
the extrusion one excepted.
+         * The extrusion of the segment joining such positions has no area.
+         */
+        private boolean isRepeated(final DataPoints points, final int a, final 
int b) {
+            final double[] pa = points.getPosition(a).toArrayDouble();
+            final double[] pb = points.getPosition(b).toArrayDouble();
+            for (int d = 0; d < pa.length; d++) {
+                if (d != axis && pa[d] != pb[d]) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Returns the polygon at the given extrusion value, as a face of its 
extrusion.
+         *
+         * @param  rings    positions of the exterior ring of the polygon 
followed by those of its holes.
+         * @param  orders   order of the positions of each ring for a face 
oriented toward the upper limit.
+         * @param  value    the extrusion value of all positions of the face.
+         * @param  reverse  whether to reverse the orders, for a face oriented 
toward the lower limit.
+         */
+        private Polygon cap(final List<DataPoints> rings, final List<int[]> 
orders, final double value, final boolean reverse) {
+            final var faceRings = new ArrayList<LinearRing>(rings.size());
+            for (int k = 0; k < rings.size(); k++) {
+                final int[] order = orders.get(k);
+                final int n = order.length;
+                final int[] indices = new int[n];
+                for (int i = 0; i < n; i++) {
+                    indices[i] = order[reverse ? n - 1 - i : i];
+                }
+                final double[] values = new double[n];
+                Arrays.fill(values, value);
+                faceRings.add(factory.createLinearRing(copy(rings.get(k), 
indices, values)));
+            }
+            return factory.createPolygon(faceRings.get(0), 
faceRings.subList(1, faceRings.size()));
+        }
+
+        /**
+         * Returns copies of the given positions of the source with the given 
extrusion values.
+         * The position at index <var>i</var> of the returned {@code 
DataPoints} is a copy of the
+         * position at index {@code indices[i]} of the source, with all its 
attributes, except
+         * that its coordinate on the extrusion axis is {@code values[i]}.
+         *
+         * @param  source   positions to copy.
+         * @param  indices  indices in the source of the positions to copy.
+         * @param  values   extrusion value of each copied position.
+         */
+        private DataPoints copy(final DataPoints source, final int[] indices, 
final double[] values) {
+            final DataPointsType type = source.getType();
+            final SampleSystem system = SampleSystem.of(crs);
+            final Map<String,Array> attributes = new LinkedHashMap<>();
+            for (final String name : type.getAttributeNames()) {
+                final boolean isPosition = 
DataPointsType.ATT_POSITION.equals(name);
+                attributes.put(name, NDArrays.of(isPosition ? system : 
type.getAttributeSystem(name),
+                                                 type.getAttributeType(name), 
indices.length));
+            }
+            final Array positions = 
attributes.get(DataPointsType.ATT_POSITION);
+            final Vector<?> position = Vectors.create(system, 
type.getAttributeType(DataPointsType.ATT_POSITION));
+            final int dimension = source.getDimension();
+            for (int i = 0; i < indices.length; i++) {
+                final Vector<?> p = source.getPosition(indices[i]);
+                for (int d = 0; d < dimension; d++) {
+                    position.set(d, p.get(d));
+                }
+                position.set(axis, values[i]);
+                positions.set(i, position);
+                for (final Map.Entry<String,Array> entry : 
attributes.entrySet()) {
+                    if (entry.getValue() != positions) {
+                        entry.getValue().set(i, 
source.getAttribute(indices[i], entry.getKey()));
+                    }
+                }
+            }
+            return factory.createDataPoints(attributes);
+        }
+    }
+
+    /**
+     * Returns the indices of the positions of the given closed ring, in their 
order or in reverse order,
+     * so that the ring is counterclockwise or clockwise in the plane of the 
given axes.
+     *
+     * @param  ring              positions of the ring, the last one being 
equal to the first one.
+     * @param  h0                index of the first axis of the plane.
+     * @param  h1                index of the second axis of the plane.
+     * @param  counterclockwise  whether the returned order shall be 
counterclockwise.
+     */
+    private static int[] order(final DataPoints ring, final int h0, final int 
h1, final boolean counterclockwise) {
+        final int n = ring.size();
+        double area = 0;
+        Vector<?> p = ring.getPosition(n - 1);
+        double px = p.get(h0);
+        double py = p.get(h1);
+        for (int i = 0; i < n; i++) {
+            p = ring.getPosition(i);
+            final double x = p.get(h0);
+            final double y = p.get(h1);
+            area += px * y - x * py;
+            px = x;
+            py = y;
+        }
+        final boolean reverse = (area > 0) != counterclockwise;
+        final int[] order = new int[n];
+        for (int i = 0; i < n; i++) {
+            order[i] = reverse ? n - 1 - i : i;
+        }
+        return order;
+    }
+
     @Override
     public Geometry getBoundary() {
         throw new UnsupportedOperationException("Not supported yet.");
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GeometryProcessor.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GeometryProcessor.java
index 917acead48..3ba5b37bc4 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GeometryProcessor.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GeometryProcessor.java
@@ -37,6 +37,7 @@ import org.apache.sis.geometries.Geometry;
 import org.apache.sis.geometries.GeometryCollection;
 import org.apache.sis.geometries.GeometryType;
 import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.Prism;
 import org.apache.sis.geometries.curve.LineString;
 import org.apache.sis.geometries.curve.LinearRing;
 import org.apache.sis.geometries.mesh.MeshPrimitive;
@@ -499,6 +500,9 @@ public final class GeometryProcessor {
             return ToPrimitive.toPrimitive(cdt);
         } else if (geom instanceof Polyhedron cdt) {
             return ToPrimitive.toPrimitive(cdt);
+        } else if (geom instanceof Prism cdt) {
+            //a prism is converted to the line string, surface or solid 
covered by its extrusion.
+            return toPrimitive(cdt.toExtrusion(), spacing, offset);
         } else if (geom instanceof GeometryCollection<?> cdt) {
             final GeometryType elementType = cdt.getElementType();
             switch (elementType) {
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/PrismTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/PrismTest.java
index 14fffc6f7d..19afe25154 100644
--- 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/PrismTest.java
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/PrismTest.java
@@ -16,11 +16,30 @@
  */
 package org.apache.sis.geometries;
 
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.VerticalCRS;
+import org.opengis.util.FactoryException;
 import org.apache.sis.geometries.curve.LineString;
+import org.apache.sis.geometries.curve.LinearRing;
+import org.apache.sis.geometries.solid.Polyhedron;
+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.NDArrays;
+import org.apache.sis.maths.Vector;
 import org.apache.sis.measure.NumberRange;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
 
 // Test dependencies
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
@@ -103,6 +122,198 @@ public class PrismTest extends GeometryTest {
         assertEquals(2, surfacePrism.getTopologicDimension(), "Extruding a 
curve gives a surface.");
     }
 
+    /**
+     * Name of the attribute holding {@code x + 10*y} at each position, for 
checking that it is copied.
+     */
+    private static final String ATT_TEST = "TEST";
+
+    /**
+     * Returns the given positions, each one with a {@value #ATT_TEST} 
attribute.
+     */
+    private static DataPoints points(final CoordinateReferenceSystem crs, 
final double... coordinates) {
+        final int dimension = crs.getCoordinateSystem().getDimension();
+        final double[] values = new double[coordinates.length / dimension];
+        for (int i = 0; i < values.length; i++) {
+            values[i] = coordinates[i * dimension] + 10 * coordinates[i * 
dimension + 1];
+        }
+        final Map<String,Array> attributes = new LinkedHashMap<>();
+        attributes.put(DataPointsType.ATT_POSITION, NDArrays.of(crs, 
coordinates));
+        attributes.put(ATT_TEST, NDArrays.of(1, values));
+        return GeometryFactory.DEFAULT.createDataPoints(attributes);
+    }
+
+    /**
+     * Returns a ring of the given positions, each one with a {@value 
#ATT_TEST} attribute.
+     */
+    private static LinearRing ring(final CoordinateReferenceSystem crs, final 
double... coordinates) {
+        return GeometryFactory.DEFAULT.createLinearRing(points(crs, 
coordinates));
+    }
+
+    /**
+     * Returns a square polygon of the given size, whose positions have a 
{@value #ATT_TEST} attribute.
+     */
+    private static Polygon square(final double x, final double y, final double 
size) {
+        return GeometryFactory.DEFAULT.createPolygon(ring(CRS_2D,
+                x, y,  x + size, y,  x + size, y + size,  x, y + size,  x, y), 
null);
+    }
+
+    /**
+     * Returns the prism of the given base, extruded from the given lower 
limit to the given upper limit.
+     */
+    private static Prism prism(final Geometry base, final double min, final 
double max) {
+        return GeometryFactory.DEFAULT.createPrism(base, 
NumberRange.create(min, true, max, true), null);
+    }
+
+    /**
+     * Returns the volume enclosed by the exterior shell of the given 
polyhedron,
+     * positive if its faces are oriented outward.
+     */
+    private static double signedVolume(final Polyhedron polyhedron) {
+        final GeometryCollection<Polygon> shell = 
polyhedron.getExteriorShell();
+        double volume = 0;
+        for (int f = 0; f < shell.getNumGeometries(); f++) {
+            final Polygon face = shell.getGeometryN(f);
+            final var rings = new ArrayList<LinearRing>();
+            rings.add(face.getExteriorRing());
+            rings.addAll(face.getInteriorRings());
+            for (final LinearRing ring : rings) {
+                final DataPoints points = ring.getDataPoints();
+                final Vector<?> a = points.getPosition(0);
+                for (int i = 2; i < points.size(); i++) {
+                    volume += 
a.dot(points.getPosition(i-1).cross(points.getPosition(i))) / 6;
+                }
+            }
+        }
+        return volume;
+    }
+
+    /**
+     * Verifies that each position of the given extrusion has the {@value 
#ATT_TEST} attribute of the base,
+     * and that its coordinate on the extrusion axis is one of the given 
limits.
+     */
+    private static void assertPositions(final Geometry extrusion, final int 
axis, final double min, final double max) {
+        final DataPoints points = extrusion.getDataPoints();
+        assertTrue(points.size() > 0);
+        for (int i = 0; i < points.size(); i++) {
+            final Vector<?> p = points.getPosition(i);
+            final int h0 = (axis == 0) ? 1 : 0;
+            final int h1 = (axis == 2) ? 1 : 2;
+            assertEquals(p.get(h0) + 10 * p.get(h1), points.getAttribute(i, 
ATT_TEST).get(0), TOLERANCE);
+            final double value = p.get(axis);
+            assertTrue(value == min || value == max, "Not an extrusion limit: 
" + value);
+        }
+    }
+
+    /**
+     * The prism of a point becomes the line string joining the two limits.
+     */
+    @Test
+    public void testToExtrusionOfPoint() {
+        final Point point = GeometryFactory.DEFAULT.createPoint(points(CRS_2D, 
1, 2));
+        final LineString line = assertInstanceOf(LineString.class, 
prism(point, 0, 5).toExtrusion());
+        assertEquals(3, line.getDimension());
+        final DataPoints points = line.getDataPoints();
+        assertEquals(2, points.size());
+        assertArrayEquals(new double[] {1, 2, 0}, 
points.getPosition(0).toArrayDouble());
+        assertArrayEquals(new double[] {1, 2, 5}, 
points.getPosition(1).toArrayDouble());
+        assertPositions(line, 2, 0, 5);
+    }
+
+    /**
+     * The prism of a line string becomes a polyhedral surface of one 
quadrilateral by segment.
+     * A repeated position adds no quadrilateral.
+     */
+    @Test
+    public void testToExtrusionOfLineString() {
+        final LineString base = 
GeometryFactory.DEFAULT.createLineString(points(CRS_2D, 0,0, 2,0, 2,0, 2,1));
+        final PolyhedralSurface<?> surface = 
assertInstanceOf(PolyhedralSurface.class, prism(base, 0, 3).toExtrusion());
+        assertEquals(2, surface.getNumPatches());
+        assertEquals(2*3 + 1*3, surface.getArea().getValue().doubleValue(), 
TOLERANCE);
+        for (int i = 0; i < surface.getNumPatches(); i++) {
+            assertPositions(surface.getPatchN(i), 2, 0, 3);
+        }
+    }
+
+    /**
+     * The prism of a collection becomes the collection of the extrusions of 
its elements.
+     */
+    @Test
+    public void testToExtrusionOfCollection() {
+        final GeometryFactory f = GeometryFactory.DEFAULT;
+        final GeometryCollection<?> polyhedra = 
assertInstanceOf(GeometryCollection.class,
+                prism(f.createMultiPolygon(CRS_2D, square(0, 0, 1), square(3, 
0, 2)), 0, 2).toExtrusion());
+        assertEquals(GeometryType.POLYHEDRON, polyhedra.getElementType());
+        assertEquals(2, polyhedra.getNumGeometries());
+        assertEquals(1*1*2, signedVolume(assertInstanceOf(Polyhedron.class, 
polyhedra.getGeometryN(0))), TOLERANCE);
+        assertEquals(2*2*2, signedVolume(assertInstanceOf(Polyhedron.class, 
polyhedra.getGeometryN(1))), TOLERANCE);
+
+        final GeometryCollection<?> mixed = 
assertInstanceOf(GeometryCollection.class, 
prism(f.createGeometryCollection(CRS_2D,
+                f.createPoint(points(CRS_2D, 1, 2)),
+                f.createLineString(points(CRS_2D, 0,0, 2,0)),
+                square(5, 5, 1)), 0, 1).toExtrusion());
+        assertEquals(GeometryType.GEOMETRY, mixed.getElementType());
+        assertEquals(3, 
mixed.getCoordinateReferenceSystem().getCoordinateSystem().getDimension());
+        assertInstanceOf(LineString.class,        mixed.getGeometryN(0));
+        assertInstanceOf(PolyhedralSurface.class, mixed.getGeometryN(1));
+        assertInstanceOf(Polyhedron.class,        mixed.getGeometryN(2));
+        for (int i = 0; i < mixed.getNumGeometries(); i++) {
+            assertPositions(mixed.getGeometryN(i), 2, 0, 1);
+        }
+    }
+
+    /**
+     * The prism of a square becomes a box whose faces are oriented outward,
+     * whatever the orientation of the square.
+     */
+    @Test
+    public void testToExtrusionOfPolygon() {
+        for (final boolean clockwise : new boolean[] {false, true}) {
+            final LinearRing square = clockwise ? ring(CRS_2D, 0,0, 0,2, 2,2, 
2,0, 0,0)
+                                                : ring(CRS_2D, 0,0, 2,0, 2,2, 
0,2, 0,0);
+            final Prism prism = GeometryFactory.DEFAULT.createPrism(
+                    GeometryFactory.DEFAULT.createPolygon(square, null), 
NumberRange.create(1., true, 4., true), null);
+            final Polyhedron polyhedron = (Polyhedron) prism.toExtrusion();
+            assertEquals(3, polyhedron.getDimension());
+            assertEquals(6, polyhedron.getExteriorShell().getNumGeometries(), 
"Expected 2 caps and 4 walls.");
+            assertTrue(polyhedron.getInteriorShells().isEmpty());
+            assertEquals(12, signedVolume(polyhedron), TOLERANCE);
+            assertPositions(polyhedron, 2, 1, 4);
+        }
+    }
+
+    /**
+     * The holes of the base become tunnels through the polyhedron, still 
oriented outward.
+     */
+    @Test
+    public void testToExtrusionOfPolygonWithHole() {
+        final Polygon base = 
GeometryFactory.DEFAULT.createPolygon(ring(CRS_2D, 0,0, 4,0, 4,4, 0,4, 0,0),
+                List.of(ring(CRS_2D, 1,1, 3,1, 3,3, 1,3, 1,1)));
+        final Prism prism = GeometryFactory.DEFAULT.createPrism(base, 
NumberRange.create(0., true, 2., true), null);
+        final Polyhedron polyhedron = (Polyhedron) prism.toExtrusion();
+        final GeometryCollection<Polygon> shell = 
polyhedron.getExteriorShell();
+        assertEquals(10, shell.getNumGeometries(), "Expected 2 caps and 8 
walls.");
+        assertEquals(1, shell.getGeometryN(0).getNumInteriorRing(), "The lower 
cap shall keep the hole.");
+        assertEquals((16 - 4) * 2, signedVolume(polyhedron), TOLERANCE);
+        assertPositions(polyhedron, 2, 0, 2);
+    }
+
+    /**
+     * When the reference system of the base contains the extrusion one,
+     * the extrusion replaces the coordinate on the corresponding axis.
+     */
+    @Test
+    public void testToExtrusionAlongBaseAxis() throws FactoryException {
+        final VerticalCRS vertical = CommonCRS.Vertical.MEAN_SEA_LEVEL.crs();
+        final CoordinateReferenceSystem crs = CRS.compound(CRS_2D, vertical);
+        final Polygon base = GeometryFactory.DEFAULT.createPolygon(ring(crs, 
0,0,5, 2,0,5, 2,3,5, 0,3,5, 0,0,5), null);
+        final Prism prism = GeometryFactory.DEFAULT.createPrism(base, 
NumberRange.create(-1., true, 1., true), vertical);
+        final Polyhedron polyhedron = (Polyhedron) prism.toExtrusion();
+        assertEquals(crs, polyhedron.getCoordinateReferenceSystem());
+        assertEquals(6, polyhedron.getExteriorShell().getNumGeometries());
+        assertEquals(12, signedVolume(polyhedron), TOLERANCE);
+        assertPositions(polyhedron, 2, -1, 1);
+    }
+
     @Test
     @Disabled("Not implemented yet.")
     public void testGetBoundaryType() {
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/ToPrimitiveTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/ToPrimitiveTest.java
index dd13ecd158..f239dcd651 100644
--- 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/ToPrimitiveTest.java
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/ToPrimitiveTest.java
@@ -24,6 +24,7 @@ import org.apache.sis.geometries.Point;
 import org.apache.sis.geometries.DataPoints;
 import org.apache.sis.geometries.DataPointsType;
 import org.apache.sis.geometries.GeometryCollection;
+import org.apache.sis.geometries.Prism;
 import org.apache.sis.geometries.curve.LineString;
 import org.apache.sis.geometries.curve.LinearRing;
 import org.apache.sis.geometries.surface.Polygon;
@@ -35,6 +36,7 @@ import org.apache.sis.geometries.mesh.MeshPrimitive;
 import org.apache.sis.geometries.mesh.MultiMeshPrimitive;
 import org.apache.sis.maths.NDArrays;
 import org.apache.sis.maths.Array;
+import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Quantities;
 import org.apache.sis.measure.Units;
 import org.apache.sis.referencing.CommonCRS;
@@ -334,6 +336,43 @@ public class ToPrimitiveTest {
         }
     }
 
+    /**
+     * The prism of a polygon is a solid, which becomes the triangles of the 
faces of its polyhedron.
+     */
+    @Test
+    public void testPrism() {
+        final GeometryFactory f = GeometryFactory.DEFAULT;
+        final Polygon square = 
f.createPolygon(f.createLinearRing(f.createDataPoints(
+                NDArrays.of(CommonCRS.WGS84.normalizedGeographic(), new 
double[] {0,0, 2,0, 2,2, 0,2, 0,0}))), null);
+        final Prism prism = f.createPrism(square, NumberRange.create(0., true, 
3., true), null);
+        final Geometry result = new GeometryProcessor().toPrimitive(prism, 
null, null);
+        assertTrue(result instanceof MeshPrimitive.Triangles);
+        final MeshPrimitive primitive = (MeshPrimitive) result;
+        assertEquals(3, 
primitive.getCoordinateReferenceSystem().getCoordinateSystem().getDimension());
+        assertEquals(36, primitive.getPositions().getLength(), "Expected 2 
triangles per face.");
+        assertEquals(2*4 + 4*6, trianglesArea3D(primitive), 1E-9);
+    }
+
+    /**
+     * The prism of a point becomes a line strip, and the prism of a line 
string the triangles of its walls.
+     */
+    @Test
+    public void testPrismOfPointAndLineString() {
+        final GeometryFactory f = GeometryFactory.DEFAULT;
+        final CoordinateReferenceSystem crs = 
CommonCRS.WGS84.normalizedGeographic();
+        final NumberRange<Double> range = NumberRange.create(0., true, 3., 
true);
+
+        final Geometry strip = new 
GeometryProcessor().toPrimitive(f.createPrism(f.createPoint(crs, 1, 2), range, 
null), null, null);
+        assertTrue(strip instanceof MeshPrimitive.LineStrip);
+        assertEquals(2, ((MeshPrimitive) strip).getPositions().getLength());
+
+        final LineString line = 
f.createLineString(f.createDataPoints(NDArrays.of(crs, new double[] {0,0, 2,0, 
2,1})));
+        final Geometry walls = new 
GeometryProcessor().toPrimitive(f.createPrism(line, range, null), null, null);
+        assertTrue(walls instanceof MeshPrimitive.Triangles);
+        assertEquals(12, ((MeshPrimitive) walls).getPositions().getLength(), 
"Expected 2 triangles per segment.");
+        assertEquals(2*3 + 1*3, trianglesArea3D((MeshPrimitive) walls), 1E-9);
+    }
+
     private static LinearRing ring3D(final CoordinateReferenceSystem crs, 
final double... coordinates) {
         return GeometryFactory.DEFAULT.createLinearRing(
                 GeometryFactory.DEFAULT.createDataPoints(NDArrays.of(crs, 
coordinates)));

Reply via email to