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