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 48ec4dd86222c7a0c03b0ef2eab9507904310b86 Author: jsorel <[email protected]> AuthorDate: Wed Oct 14 11:00:22 2026 +0200 feat(Geometry): implement closed surfaces boundary method --- .../main/org/apache/sis/geometries/Geometry.java | 6 +- .../main/org/apache/sis/geometries/HyperPlane.java | 5 +- .../main/org/apache/sis/geometries/Plane.java | 5 +- .../main/org/apache/sis/geometries/Primitive.java | 6 +- .../geometries/internal/shared/DefaultCapsule.java | 6 -- .../internal/shared/DefaultCylinder.java | 19 +++- .../internal/shared/DefaultEllipsoid.java | 6 -- .../geometries/internal/shared/DefaultSphere.java | 6 -- .../org/apache/sis/geometries/surface/Capsule.java | 10 ++ .../apache/sis/geometries/surface/Cylinder.java | 112 ++++++++++++++++++++- .../apache/sis/geometries/surface/Ellipsoid.java | 10 ++ .../org/apache/sis/geometries/surface/Sphere.java | 10 ++ 12 files changed, 171 insertions(+), 30 deletions(-) diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java index 150c5239b5..5d5717c340 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java @@ -276,14 +276,16 @@ public sealed interface Geometry * boundary, and in the interior those lying on more than one.</li> * </ul> * + * <p>The default implementation returns {@link BoundaryType#MOD_2}, the rule of OGC Simple Feature + * Access. The three rules agree on geometries which are not aggregates.</p> + * * @return rule used to compute the boundary of this geometry. * * @see ISO 19107:2019 - 6.4.3, 10.8.3 */ @UML(identifier="boundaryType", specification=ISO_19107) default BoundaryType getBoundaryType() { - //TODO remove this method default when all classes implement it. - throw new UnsupportedOperationException(); + return BoundaryType.MOD_2; } /** diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/HyperPlane.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/HyperPlane.java index 74627f4640..ecff5d2b03 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/HyperPlane.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/HyperPlane.java @@ -149,9 +149,12 @@ public final class HyperPlane extends AbstractGeometry{ return points.getCoordinateReferenceSystem(); } + /** + * Returns the empty geometry: a hyperplane extends to infinity in all its directions, so it has no edge. + */ @Override public Geometry getBoundary() { - throw new UnsupportedOperationException("Not supported yet."); + return GeometryFactory.DEFAULT.createEmpty(getCoordinateReferenceSystem()); } @Override diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Plane.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Plane.java index 62cdc24f30..3bd323154a 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Plane.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Plane.java @@ -210,9 +210,12 @@ public final class Plane extends AbstractGeometry{ return points.getCoordinateReferenceSystem(); } + /** + * Returns the empty geometry: a plane extends to infinity in all its directions, so it has no edge. + */ @Override public Geometry getBoundary() { - throw new UnsupportedOperationException("Not supported yet."); + return GeometryFactory.DEFAULT.createEmpty(getCoordinateReferenceSystem()); } @Override diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Primitive.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Primitive.java index b4bac83149..8b1d49a277 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Primitive.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Primitive.java @@ -95,12 +95,14 @@ public sealed interface Primitive extends Geometry * <li>For a surface or a solid, the order may be ignored.</li> * </ul> * + * <p>The default implementation returns an empty list, for a primitive which is not + * decomposed in smaller ones.</p> + * * @return segments of this primitive, possibly empty. * * @see ISO 19107:2019 - 6.4.11.2 */ default List<Primitive> getSegments() { - //TODO - throw new UnsupportedOperationException(); + return List.of(); } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCapsule.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCapsule.java index 1ffdcb304e..d33f5c06e2 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCapsule.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCapsule.java @@ -19,7 +19,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.Geometry; import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.surface.Capsule; import org.apache.sis.maths.NDArrays; @@ -121,11 +120,6 @@ public non-sealed class DefaultCapsule extends AbstractGeometry implements Capsu throw new UnsupportedOperationException("Not supported yet."); } - @Override - public Geometry getBoundary() { - throw new UnsupportedOperationException("Not supported yet."); - } - @Override public boolean equals(Object obj) { if (obj == null) { 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 b7405ea923..cffd3f385f 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 @@ -19,7 +19,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.Geometry; import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.surface.Cylinder; import org.apache.sis.maths.NDArrays; @@ -43,6 +42,11 @@ public non-sealed class DefaultCylinder extends AbstractGeometry implements Cyli private double radiusTop = 1.0; private double radiusBottom = 1.0; + /** + * Whether the ends are closed by disks. + */ + private boolean closed = true; + /** * Creates a cylinder along the axis joining the two given positions. * The reference system of the cylinder is the one of those positions. @@ -122,8 +126,13 @@ public non-sealed class DefaultCylinder extends AbstractGeometry implements Cyli } @Override - public Geometry getBoundary() { - throw new UnsupportedOperationException("Not supported yet."); + public boolean isClosed() { + return closed; + } + + @Override + public void setClosed(boolean closed) { + this.closed = closed; } @Override @@ -139,7 +148,8 @@ public non-sealed class DefaultCylinder extends AbstractGeometry implements Cyli return false; } return Double.doubleToLongBits(this.radiusTop) == Double.doubleToLongBits(other.radiusTop) - && Double.doubleToLongBits(this.radiusBottom) == Double.doubleToLongBits(other.radiusBottom); + && Double.doubleToLongBits(this.radiusBottom) == Double.doubleToLongBits(other.radiusBottom) + && this.closed == other.closed; } @Override @@ -148,6 +158,7 @@ public non-sealed class DefaultCylinder extends AbstractGeometry implements Cyli hash = 89 * hash + Objects.hashCode(this.points); hash = 89 * hash + Double.hashCode(this.radiusTop); hash = 89 * hash + Double.hashCode(this.radiusBottom); + hash = 89 * hash + Boolean.hashCode(this.closed); return hash; } } 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 9c15c172b5..40b491e35b 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 @@ -24,7 +24,6 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.geometries.Curve; import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.Geometries; -import org.apache.sis.geometries.Geometry; import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.surface.Ellipsoid; import org.apache.sis.maths.DataType; @@ -193,11 +192,6 @@ public non-sealed class DefaultEllipsoid extends AbstractGeometry implements Ell points.setPosition(0, position); } - @Override - public Geometry getBoundary() { - throw new UnsupportedOperationException("Not supported yet."); - } - // methods from ParametricCurveSurface @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 70223e83d6..3d298d699d 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 @@ -23,7 +23,6 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.geometries.Curve; import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.Geometries; -import org.apache.sis.geometries.Geometry; import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.surface.Sphere; import org.apache.sis.maths.DataType; @@ -131,11 +130,6 @@ public non-sealed class DefaultSphere extends AbstractGeometry implements Sphere points.setPosition(0, position); } - @Override - public Geometry getBoundary() { - throw new UnsupportedOperationException("Not supported yet."); - } - // methods from ParametricCurveSurface @Override diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Capsule.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Capsule.java index d7ddb0f81b..edc2769956 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Capsule.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Capsule.java @@ -22,6 +22,8 @@ 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.Geometry; +import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.Surface; import org.apache.sis.geometries.SurfaceInterpolation; @@ -155,6 +157,14 @@ public sealed interface Capsule extends Surface return bbox; } + /** + * Returns the empty geometry: a capsule is a closed surface, so it has no edge. + */ + @Override + default Geometry getBoundary() { + return GeometryFactory.DEFAULT.createEmpty(getCoordinateReferenceSystem()); + } + @Override default CoordinateReferenceSystem getCoordinateReferenceSystem() { return getDataPoints().getCoordinateReferenceSystem(); 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 c36befb43b..73b4b4d232 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 @@ -16,17 +16,22 @@ */ package org.apache.sis.geometries.surface; +import java.util.ArrayList; import java.util.List; 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.Geometry; +import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.Surface; import org.apache.sis.geometries.SurfaceInterpolation; +import org.apache.sis.geometries.curve.Circle; import org.apache.sis.geometries.internal.shared.DefaultCylinder; import org.apache.sis.maths.DataType; +import org.apache.sis.maths.NDArrays; import org.apache.sis.maths.ReadOnly; import org.apache.sis.maths.Vectors; import org.apache.sis.maths.Vector; @@ -41,6 +46,10 @@ import org.apache.sis.maths.Vector; * * ISO 19107 : classified as a conic surface * + * <p>A cylinder is either {@linkplain #isClosed() closed} by two disks at its ends, which makes it + * the boundary of a volume as in the KHR_implicit_shapes extension, or open, in which case it is + * only the lateral surface that ISO 19107 defines.</p> + * * <p>Constraints:</p> * <ul> * <li>The {@linkplain #getDataPoints() data points} hold exactly two positions: @@ -66,15 +75,33 @@ public sealed interface Cylinder extends Surface } /** - * Returns {@link SurfaceInterpolation#CONIC}: ISO 19107 classifies cylinders and cones as conic surfaces. + * Returns {@link SurfaceInterpolation#CONIC}, since ISO 19107 classifies cylinders and cones + * as conic surfaces, followed by {@link SurfaceInterpolation#PLANAR} for the end disks if this + * cylinder is {@linkplain #isClosed() closed}. * * @see ISO 19107:2019 - 8.5.3, 8.5.4 */ @Override default List<SurfaceInterpolation> getInterpolation() { - return List.of(SurfaceInterpolation.CONIC); + return isClosed() ? List.of(SurfaceInterpolation.CONIC, SurfaceInterpolation.PLANAR) + : List.of(SurfaceInterpolation.CONIC); } + /** + * Returns whether this cylinder is closed by a disk at each of its ends. + * An end whose radius is zero, the apex of a cone, needs no disk. + * + * @return {@code true} if the ends are closed by disks, or {@code false} for the lateral surface only. + */ + boolean isClosed(); + + /** + * Sets whether this cylinder is closed by a disk at each of its ends. + * + * @param closed {@code true} to close the ends by disks, or {@code false} for the lateral surface only. + */ + void setClosed(boolean closed); + /** * Returns {@code false}: a cylinder always has its two circle centers. */ @@ -191,6 +218,87 @@ public sealed interface Cylinder extends Surface return bbox; } + /** + * Returns the empty geometry if this cylinder is {@linkplain #isClosed() closed}, + * or the circles at its ends otherwise. + * + * <p>The circles of an open cylinder are oriented so that the lateral surface is on their left + * when seen from its outward normal: the bottom circle turns counterclockwise around the axis + * going from the bottom to the top, and the top circle clockwise. An end whose radius is zero, + * the apex of a cone, is a single point which bounds nothing, so it has no circle.</p> + * + * @return the empty geometry, or a collection of the end circles. + * @throws UnsupportedOperationException if this cylinder is open and not 3-dimensional. + * @throws IllegalStateException if this cylinder is open and its two positions are the same. + */ + @Override + default Geometry getBoundary() { + final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(); + if (isClosed()) { + return GeometryFactory.DEFAULT.createEmpty(crs); + } + if (getDataPoints().getDimension() != 3) { + throw new UnsupportedOperationException("The boundary of an open cylinder is computed in 3 dimensions only."); + } + final Vector<?> axis = getAxis(); + final double height = axis.length(); + if (!(height > 0)) { + throw new IllegalStateException("The cylinder has no axis, its two positions are the same."); + } + axis.scale(1 / height); + /* + * Builds (u, w, axis) as a right-handed orthonormal basis. The vector `u` is derived from the + * coordinate axis which is the most perpendicular to the cylinder axis, for numerical stability. + */ + int k = 0; + for (int i = 1; i < 3; i++) { + if (Math.abs(axis.get(i)) < Math.abs(axis.get(k))) k = i; + } + final Vector<?> u = Vectors.create(axis.getSampleSystem(), DataType.DOUBLE); + u.set(k, 1); + u.subtract(axis.copy().scale(axis.get(k))); + u.normalize(); + final Vector<?> w = axis.cross(u); + final var circles = new ArrayList<Circle>(2); + final double radiusBottom = getRadiusBottom(); + final double radiusTop = getRadiusTop(); + if (radiusBottom > 0) { + circles.add(circle(getBottom(), radiusBottom, u, w)); + } + if (radiusTop > 0) { + circles.add(circle(getTop(), radiusTop, u, w.copy().scale(-1))); + } + return GeometryFactory.DEFAULT.createMultiCurve(crs, circles.toArray(Circle[]::new)); + } + + /** + * Returns the circle of the given center and radius in the plane of the two given unit vectors, + * turning from {@code u} toward {@code w}. The circle is made of four quarter arcs, since a + * single arc of a circle shall stay below a half turn. + */ + private static Circle circle(final ReadOnly.Vector<?> center, final double radius, + final ReadOnly.Vector<?> u, final ReadOnly.Vector<?> w) + { + final var points = new ArrayList<Vector<?>>(5); + final var centers = new ArrayList<Vector<?>>(4); + final ReadOnly.Vector<?>[] directions = {u, w, u.copy().scale(-1), w.copy().scale(-1), u}; + for (final ReadOnly.Vector<?> direction : directions) { + final Vector<?> p = Vectors.create(center.getSampleSystem(), DataType.DOUBLE); + p.set(center); + p.add(direction.copy().scale(radius)); + points.add(p); + } + for (int i = 0; i < 4; i++) { + final Vector<?> c = Vectors.create(center.getSampleSystem(), DataType.DOUBLE); + c.set(center); + centers.add(c); + } + final GeometryFactory factory = GeometryFactory.DEFAULT; + return factory.createCircle( + factory.createDataPoints(NDArrays.of(points, center.getSampleSystem(), DataType.DOUBLE)), + NDArrays.of(centers, center.getSampleSystem(), DataType.DOUBLE), null); + } + @Override default CoordinateReferenceSystem getCoordinateReferenceSystem() { return getDataPoints().getCoordinateReferenceSystem(); diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Ellipsoid.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Ellipsoid.java index 254686b012..1c8639c6f7 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Ellipsoid.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Ellipsoid.java @@ -23,6 +23,8 @@ import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.geometries.BBox; import org.apache.sis.geometries.DataPointsType; +import org.apache.sis.geometries.Geometry; +import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.SurfaceInterpolation; import org.apache.sis.geometries.internal.shared.DefaultEllipsoid; @@ -139,6 +141,14 @@ public sealed interface Ellipsoid extends ParametricCurveSurface */ void setCenter(ReadOnly.Vector<?> position); + /** + * Returns the empty geometry: an ellipsoid is a closed surface, so it has no edge. + */ + @Override + default Geometry getBoundary() { + return GeometryFactory.DEFAULT.createEmpty(getCoordinateReferenceSystem()); + } + @Override default CoordinateReferenceSystem getCoordinateReferenceSystem() { return getDataPoints().getCoordinateReferenceSystem(); diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Sphere.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Sphere.java index 8e8218958d..f8c5ac2d05 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Sphere.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Sphere.java @@ -23,6 +23,8 @@ import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.geometries.BBox; import org.apache.sis.geometries.DataPointsType; +import org.apache.sis.geometries.Geometry; +import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.SurfaceInterpolation; import org.apache.sis.geometries.internal.shared.DefaultSphere; @@ -130,6 +132,14 @@ public sealed interface Sphere extends ParametricCurveSurface */ void setCenter(ReadOnly.Vector<?> position); + /** + * Returns the empty geometry: a sphere is a closed surface, so it has no edge. + */ + @Override + default Geometry getBoundary() { + return GeometryFactory.DEFAULT.createEmpty(getCoordinateReferenceSystem()); + } + @Override default CoordinateReferenceSystem getCoordinateReferenceSystem() { return getDataPoints().getCoordinateReferenceSystem();
