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 1837f53a81fb07b13915eccc2ea07ad76d27f905 Author: jsorel <[email protected]> AuthorDate: Mon Nov 2 17:00:54 2026 +0100 feat(Geometry): implemente centroid on several classes --- .../main/org/apache/sis/geometries/Curve.java | 20 +- .../apache/sis/geometries/GeometryCollection.java | 27 +- .../apache/sis/geometries/curve/LineString.java | 12 + .../apache/sis/geometries/curve/ProductCurve.java | 18 ++ .../sis/geometries/internal/shared/Centroids.java | 352 +++++++++++++++++++++ .../geometries/internal/shared/DefaultBezier.java | 10 + .../internal/shared/DefaultCurvePolygon.java | 3 +- .../internal/shared/DefaultGeometryCollection.java | 21 -- .../internal/shared/DefaultLineString.java | 11 - .../internal/shared/DefaultMultiPoint.java | 20 -- .../geometries/internal/shared/DefaultPolygon.java | 12 +- .../internal/shared/DefaultTriangle.java | 26 ++ .../sis/geometries/surface/CurvePolygon.java | 31 ++ .../sis/geometries/surface/PolyhedralSurface.java | 31 ++ .../apache/sis/geometries/surface/Triangle.java | 36 +++ .../org/apache/sis/geometries/CentroidTest.java | 345 ++++++++++++++++++++ 16 files changed, 915 insertions(+), 60 deletions(-) diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Curve.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Curve.java index d85ee70f5a..ac47d96313 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Curve.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Curve.java @@ -35,6 +35,7 @@ import org.apache.sis.geometries.curve.ProductCurve; import org.apache.sis.geometries.curve.Ray; import org.apache.sis.geometries.curve.Rhumb; import org.apache.sis.geometries.curve.Spiral; +import org.apache.sis.geometries.internal.shared.Centroids; import org.apache.sis.geometries.internal.shared.DefaultReversedCurve; import org.apache.sis.maths.Array; import org.apache.sis.maths.Vector; @@ -177,6 +178,21 @@ public sealed interface Curve extends Orientable return subCurve(Quantities.create(50, Units.PERCENT)); } + /** + * Returns the centroid of this curve, computed on a fine linear approximation of this curve. + * + * @return the centroid of this curve, or {@code null} if this curve is empty. + * + * @see ISO 19107:2019 - 6.4.4.8 + */ + @Override + default Point getCentroid() { + if (isEmpty()) { + return null; + } + return Centroids.polyline(Centroids.linearize(this).getDataPoints()); + } + /** * Returns whether this curve is closed, i.e. whether its start point equals its end point. * For a simple curve, this is equivalent to {@link Geometry#isCycle()}. @@ -711,8 +727,8 @@ public sealed interface Curve extends Orientable * @see derived from ISO 19107:2019 - 6.4.18.22 */ default Point subCurve(Quantity<?> dist) { - //TODO - throw new UnsupportedOperationException(); + //measured on a fine linear approximation, curves may override with an exact computation + return Centroids.linearize(this).subCurve(dist); } /** diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryCollection.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryCollection.java index 5ba33a22d1..4e779dbbc6 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryCollection.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryCollection.java @@ -22,12 +22,12 @@ import org.opengis.annotation.UML; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; import org.apache.sis.geometries.curve.ProductCurve; +import org.apache.sis.geometries.internal.shared.Centroids; import org.apache.sis.geometries.internal.shared.ConcatenatedDataPoints; import org.apache.sis.geometries.internal.shared.DefaultGeometryCollection; import org.apache.sis.geometries.internal.shared.DefaultMultiPoint; import org.apache.sis.geometries.internal.shared.EmptyDataPoints; import org.apache.sis.geometries.mesh.MeshPrimitive; -import org.apache.sis.geometries.mesh.MultiMeshPrimitive; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.measure.Quantities; import org.apache.sis.measure.Units; @@ -197,6 +197,31 @@ public sealed interface GeometryCollection<T extends Geometry> extends Geometry return dimension; } + /** + * Returns the mean of the centroids of the elements having the largest topological dimension, + * weighted by their number of points, length or area. Elements of lower dimensions are ignored. + * + * @return the centroid, or {@code null} if all elements are empty. + * + * @see ISO 19107:2019 - 6.4.4.8 + */ + @Override + default Point getCentroid() { + return Centroids.collection(this); + } + + /** + * Returns the representative point of the first element having the largest topological dimension. + * + * @return a point of this collection, or {@code null} if all elements are empty. + * + * @see ISO 19107:2019 - 6.4.4.19 + */ + @Override + default Point getRepresentativePoint() { + return Centroids.representative(this); + } + /** * Returns the number of geometries in this GeometryCollection. * It can only change by adding or removing elements. diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java index cc3f52d9c9..fe889f42d8 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java @@ -29,6 +29,7 @@ import org.apache.sis.geometries.Point; import org.apache.sis.geometries.DataPointsType; import org.apache.sis.geometries.Geometries; import org.apache.sis.geometries.GeometryFactory; +import org.apache.sis.geometries.internal.shared.Centroids; import org.apache.sis.geometries.internal.shared.CurveLinearizer; import org.apache.sis.geometries.internal.shared.Curves; import org.apache.sis.geometries.internal.shared.DefaultLineString; @@ -121,6 +122,17 @@ public sealed interface LineString extends Curve return Curves.alongPolyline(getDataPoints(), dist, Geometries.getLinearUnit(this)); } + /** + * Returns the mean of the segment middles weighted by the segment lengths, + * or the mean of the points if this line string has no length. + * + * @return the centroid, or {@code null} if this line string is empty. + */ + @Override + default Point getCentroid() { + return Centroids.polyline(getDataPoints()); + } + /** * The number of Points in this LineString. * diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ProductCurve.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ProductCurve.java index f4d2f43272..d3603f3be6 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ProductCurve.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ProductCurve.java @@ -26,6 +26,7 @@ import org.apache.sis.geometries.CurveInterpolation; import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.GeometryCollection; import org.apache.sis.geometries.GeometryType; +import org.apache.sis.geometries.Point; import org.apache.sis.geometries.cs.Projection; import org.apache.sis.geometries.internal.shared.DefaultProductCurve; import org.apache.sis.measure.Range; @@ -157,6 +158,23 @@ public sealed interface ProductCurve extends Curve, GeometryCollection<Curve> return Curve.super.isClosed(); } + /** + * Returns the centroid of this curve in the complete reference system, + * not the mean of its projections. + */ + @Override + default Point getCentroid() { + return Curve.super.getCentroid(); + } + + /** + * Returns the point at the middle of this curve in the complete reference system. + */ + @Override + default Point getRepresentativePoint() { + return Curve.super.getRepresentativePoint(); + } + /** * Returns 1 whatever the position: a product curve is a curve, which has the same * dimension at every interior position, even if it is also a collection of curves. diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Centroids.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Centroids.java new file mode 100644 index 0000000000..95fc7a615f --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Centroids.java @@ -0,0 +1,352 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.geometries.internal.shared; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.measure.Quantity; +import org.opengis.geometry.Envelope; +import org.apache.sis.geometries.Curve; +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.Point; +import org.apache.sis.geometries.Surface; +import org.apache.sis.geometries.curve.LineString; +import org.apache.sis.geometries.curve.LinearRing; +import org.apache.sis.geometries.operation.ToPrimitive; +import org.apache.sis.geometries.surface.CurvePolygon; +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.Quantities; + + +/** + * Centroids and representative points of geometries which do not compute them directly, + * from their components or from a linear approximation. + * + * @author Johann Sorel (Geomatys) + */ +public final class Centroids { + + /** + * Maximal distance between a curve and its linear approximation, + * as a fraction of the diagonal of the curve envelope. + */ + private static final double LINEARIZATION_OFFSET = 1E-6; + + private Centroids() { + } + + /** + * Returns a fine linear approximation of the given curve. + * + * @param curve the curve to approximate. + * @return the linear approximation. + * @throws UnsupportedOperationException if the curve has no linear approximation. + */ + public static LineString linearize(final Curve curve) { + final Envelope envelope = curve.getEnvelope(); + double diagonal = 0; + if (envelope != null) { + for (int i = 0, n = envelope.getDimension(); i < n; i++) { + final double span = envelope.getSpan(i); + if (Double.isFinite(span)) { + diagonal += span * span; + } + } + } + diagonal = Math.sqrt(diagonal); + final Quantity<?> offset = (diagonal > 0) + ? Quantities.create(LINEARIZATION_OFFSET * diagonal, Geometries.getLinearUnit(curve)) : null; + return curve.asLine(null, offset); + } + + /** + * Returns a polygon with the linear approximations of the rings of the given curve polygon. + * + * @param surface the curve polygon to approximate. + * @return the polygon approximating the curve polygon. + */ + public static Polygon linearize(final CurvePolygon surface) { + final LinearRing exterior = ring(surface.getExteriorRing()); + final List<LinearRing> interiors = new ArrayList<>(); + for (final Curve interior : surface.getInteriorRings()) { + interiors.add(ring(interior)); + } + return GeometryFactory.DEFAULT.createPolygon(exterior, interiors); + } + + private static LinearRing ring(final Curve curve) { + if (curve instanceof LinearRing ring) { + return ring; + } + return GeometryFactory.DEFAULT.createLinearRing(linearize(curve).getDataPoints()); + } + + /** + * Returns the centroid of a polyline: the mean of the segment middles weighted by the segment lengths, + * or the mean of the positions if the polyline has no length. All attributes are interpolated + * with the same weights, each segment giving half of its length to each of its two points. + * + * @param points points of the polyline. + * @return the centroid, or {@code null} if there is no point. + */ + public static Point polyline(final DataPoints points) { + final int n = points.size(); + if (n == 0) { + return null; + } + final double[] weights = new double[n]; + double total = 0; + for (int i = 1; i < n; i++) { + final double half = distance(points.getPosition(i - 1), points.getPosition(i)) / 2; + weights[i - 1] += half; + weights[i] += half; + total += 2 * half; + } + for (int i = 0; i < n; i++) { + weights[i] = (total > 0) ? weights[i] / total : 1.0 / n; + } + return points.getInterpolatedPoint(indices(n), weights); + } + + /** + * Returns the centroid of a polygon: the centroid of its triangulation, attributes being interpolated + * inside each triangle. Falls back on the centroid of the exterior ring if the polygon has no area. + * + * @param polygon the polygon. + * @return the centroid of the polygon. + */ + public static Point polygon(final Polygon polygon) { + final Point centroid = ToPrimitive.toPrimitive(polygon).getCentroid(); + return (centroid != null) ? centroid : polyline(polygon.getExteriorRing().getDataPoints()); + } + + /** + * Returns a point inside a polygon: the centroid of the largest triangle of its triangulation. + * Falls back on the centroid of the exterior ring if the polygon has no area. + * + * @param polygon the polygon. + * @return a point inside the polygon. + */ + public static Point pointOnPolygon(final Polygon polygon) { + final Point point = ToPrimitive.toPrimitive(polygon).getRepresentativePoint(); + return (point != null) ? point : polyline(polygon.getExteriorRing().getDataPoints()); + } + + /** + * Returns the centroid of a collection: the mean of the centroids of the elements having the largest + * topological dimension, weighted by their number of points, length or area. + * Elements of lower dimensions are ignored. + * + * @param collection the collection. + * @return the centroid, or {@code null} if all elements are empty. + */ + public static Point collection(final GeometryCollection<?> collection) { + final int dimension = largestDimension(collection); + if (dimension < 0) { + return null; + } + final List<Point> centroids = new ArrayList<>(); + final List<Double> weights = new ArrayList<>(); + for (int i = 0, n = collection.getNumGeometries(); i < n; i++) { + final Geometry element = collection.getGeometryN(i); + if (!element.isEmpty() && element.getTopologicDimension() == dimension) { + final Point centroid = element.getCentroid(); + if (centroid != null) { + centroids.add(centroid); + weights.add(measure(element, dimension)); + } + } + } + return mean(centroids, weights); + } + + /** + * Returns a representative point of a collection: the one of the first element + * having the largest topological dimension. + * + * @param collection the collection. + * @return the representative point, or {@code null} if all elements are empty. + */ + public static Point representative(final GeometryCollection<?> collection) { + final int dimension = largestDimension(collection); + for (int i = 0, n = collection.getNumGeometries(); i < n; i++) { + final Geometry element = collection.getGeometryN(i); + if (!element.isEmpty() && element.getTopologicDimension() == dimension) { + return element.getRepresentativePoint(); + } + } + return null; + } + + /** + * Returns the centroid of a polyhedral surface: the mean of the centroids of the patches weighted by their area. + * + * @param surface the polyhedral surface. + * @return the centroid, or {@code null} if the surface has no patch. + */ + public static Point patches(final PolyhedralSurface<?> surface) { + final int n = surface.getNumPatches(); + final List<Point> centroids = new ArrayList<>(n); + final List<Double> weights = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + final Polygon patch = surface.getPatchN(i); + if (!patch.isEmpty()) { + centroids.add(patch.getCentroid()); + weights.add(patch.getArea().getValue().doubleValue()); + } + } + return mean(centroids, weights); + } + + /** + * Returns a representative point of a polyhedral surface: the one of its largest patch. + * + * @param surface the polyhedral surface. + * @return a point on the surface, or {@code null} if the surface has no patch. + */ + public static Point representative(final PolyhedralSurface<?> surface) { + Polygon largest = null; + double area = -1; + for (int i = 0, n = surface.getNumPatches(); i < n; i++) { + final Polygon patch = surface.getPatchN(i); + if (!patch.isEmpty()) { + final double a = patch.getArea().getValue().doubleValue(); + if (a > area) { + area = a; + largest = patch; + } + } + } + return (largest != null) ? largest.getRepresentativePoint() : null; + } + + /** + * Returns the largest topological dimension among the non-empty elements, or -1 if none. + */ + private static int largestDimension(final GeometryCollection<?> collection) { + int dimension = -1; + for (int i = 0, n = collection.getNumGeometries(); i < n; i++) { + final Geometry element = collection.getGeometryN(i); + if (!element.isEmpty()) { + dimension = Math.max(dimension, element.getTopologicDimension()); + } + } + return dimension; + } + + /** + * Returns the number of points, the length or the area of the given element, + * for the given topological dimension. + */ + private static double measure(final Geometry element, final int dimension) { + if (element instanceof GeometryCollection<?> collection) { + double sum = 0; + for (int i = 0, n = collection.getNumGeometries(); i < n; i++) { + final Geometry child = collection.getGeometryN(i); + if (!child.isEmpty() && child.getTopologicDimension() == dimension) { + sum += measure(child, dimension); + } + } + return sum; + } + switch (dimension) { + case 0: return 1; + case 1: { + if (element instanceof Curve curve) { + final DataPoints points = (curve instanceof LineString line ? line : linearize(curve)).getDataPoints(); + double length = 0; + for (int i = 1, n = points.size(); i < n; i++) { + length += distance(points.getPosition(i - 1), points.getPosition(i)); + } + return length; + } + break; + } + case 2: { + if (element instanceof Surface surface) { + return surface.getArea().getValue().doubleValue(); + } + break; + } + } + return 1; + } + + /** + * Returns the mean of the given points weighted by the given weights, or the plain mean if the weights + * sum to zero. The attributes present in all points are interpolated with the same weights, as declared + * by the {@linkplain DataPointsType#getAttributeInterpolation(String) attribute interpolation}. + */ + private static Point mean(final List<Point> points, final List<Double> weights) { + final int n = points.size(); + if (n == 0) { + return null; + } + double total = 0; + for (final double w : weights) { + total += w; + } + final double[] w = new double[n]; + for (int i = 0; i < n; i++) { + w[i] = (total > 0) ? weights.get(i) / total : 1.0 / n; + } + final DataPointsType type = points.get(0).getDataPointsType(); + final Map<String,Array> attributes = new LinkedHashMap<>(); +search: for (final String name : type.getAttributeNames()) { + final List<Vector<?>> values = new ArrayList<>(n); + for (final Point point : points) { + if (!point.getDataPointsType().getAttributeNames().contains(name)) { + continue search; + } + values.add(point.getAttribute(name)); + } + attributes.put(name, NDArrays.of(values, type.getAttributeSystem(name), type.getAttributeType(name))); + } + return new ArrayDataPoints(attributes).getInterpolatedPoint(indices(n), w); + } + + /** + * Returns the indices from 0 inclusive to {@code n} exclusive. + */ + private static int[] indices(final int n) { + final int[] indices = new int[n]; + for (int i = 0; i < n; i++) { + indices[i] = i; + } + return indices; + } + + private static double distance(final Vector<?> a, final Vector<?> b) { + double sum = 0; + for (int d = 0, n = a.getDimension(); d < n; d++) { + final double delta = b.get(d) - a.get(d); + sum += delta * delta; + } + return Math.sqrt(sum); + } +} diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBezier.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBezier.java index 62752c90cf..b629fb47d4 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBezier.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultBezier.java @@ -17,6 +17,7 @@ package org.apache.sis.geometries.internal.shared; import javax.measure.Quantity; +import org.opengis.geometry.Envelope; import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.Geometries; import org.apache.sis.geometries.GeometryFactory; @@ -103,4 +104,13 @@ public non-sealed class DefaultBezier extends DefaultPolynomialSpline implements } return evaluator().asLine(this, spacing, offset); } + + /** + * Returns the envelope of the data points and of the control points. + * A Bézier curve stays inside the convex hull of its control points. + */ + @Override + public Envelope getEnvelope() { + return controlHullEnvelope(points, controlPoints, false); + } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java index 80c9b74ae3..b1f3971b3d 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultCurvePolygon.java @@ -99,7 +99,8 @@ public non-sealed class DefaultCurvePolygon extends AbstractGeometry implements @Override public Quantity<?> getArea() { - throw new UnsupportedOperationException("Not supported yet."); + //measured on a polygon approximating the rings + return Centroids.linearize(this).getArea(); } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultGeometryCollection.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultGeometryCollection.java index 77448dff30..e1a17f09c3 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultGeometryCollection.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultGeometryCollection.java @@ -25,7 +25,6 @@ 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.Geometries; import org.apache.sis.referencing.CRS; import org.apache.sis.util.ArgumentChecks; @@ -90,26 +89,6 @@ public non-sealed class DefaultGeometryCollection<T extends Geometry> extends Ab return geometries.get(n); } - @Override - public Point getCentroid() { - if (isEmpty()) { - // The centroid of the empty set is undefined. - return null; - } - //TODO : fallback on JTS until implemented - return (Point) fromJTS(asJTS().getCentroid()); - } - - @Override - public Point getRepresentativePoint() { - if (isEmpty()) { - // The empty set has no interior position. - return null; - } - //TODO : fallback on JTS until implemented - return (Point) fromJTS(asJTS().getInteriorPoint()); - } - @Override public Geometry getBoundary() { if (isEmpty()) { diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLineString.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLineString.java index 53a183b0d0..0b0ee133ec 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLineString.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLineString.java @@ -19,7 +19,6 @@ package org.apache.sis.geometries.internal.shared; import javax.measure.Quantity; import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.Geometries; -import org.apache.sis.geometries.Point; import org.apache.sis.geometries.curve.LineString; import org.apache.sis.measure.Quantities; @@ -52,16 +51,6 @@ public non-sealed class DefaultLineString extends AbstractGeometry implements Li return Quantities.create(asJTS().getLength(), Geometries.getLinearUnit(this)); } - @Override - public Point getCentroid() { - if (isEmpty()) { - // The centroid of the empty set is undefined. - return null; - } - //TODO : fallback on JTS until implemented - return (Point) fromJTS(asJTS().getCentroid()); - } - @Override public boolean isSimple() { //TODO : fallback on JTS until implemented diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPoint.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPoint.java index 003adbd02b..7804bdd513 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPoint.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultMultiPoint.java @@ -75,26 +75,6 @@ public non-sealed class DefaultMultiPoint extends AbstractGeometry implements Ge return points; } - @Override - public Point getCentroid() { - if (isEmpty()) { - // The centroid of the empty set is undefined. - return null; - } - //TODO : fallback on JTS until implemented - return (Point) fromJTS(asJTS().getCentroid()); - } - - @Override - public Point getRepresentativePoint() { - if (isEmpty()) { - // The empty set has no interior position. - return null; - } - //TODO : fallback on JTS until implemented - return (Point) fromJTS(asJTS().getInteriorPoint()); - } - @Override public Geometry getBoundary() { if (isEmpty()) { diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolygon.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolygon.java index 5df2377c5c..bac3c84544 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolygon.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolygon.java @@ -110,24 +110,28 @@ public non-sealed class DefaultPolygon extends AbstractGeometry implements Polyg return Quantities.create(asJTS().getLength(), Geometries.getLinearUnit(this)); } + /** + * Returns the centroid of the triangulation of this polygon, with interpolated attributes. + */ @Override public Point getCentroid() { if (isEmpty()) { // The centroid of the empty set is undefined. return null; } - //TODO : fallback on JTS until implemented - return (Point) fromJTS(asJTS().getCentroid()); + return Centroids.polygon(this); } + /** + * Returns the centroid of the largest triangle of the triangulation of this polygon, with interpolated attributes. + */ @Override public Point getPointOnSurface() { if (isEmpty()) { // The empty set has no interior position. return null; } - //TODO : fallback on JTS until implemented - return (Point) fromJTS(asJTS().getInteriorPoint()); + return Centroids.pointOnPolygon(this); } /** diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangle.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangle.java index acb9c10426..ebd42a7969 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangle.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultTriangle.java @@ -17,7 +17,9 @@ 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.Point; import org.apache.sis.geometries.curve.LinearRing; import org.apache.sis.geometries.surface.Triangle; @@ -53,6 +55,30 @@ public non-sealed class DefaultTriangle extends DefaultPolygon implements Triang return (LinearRing) exterior; } + /* + * The following methods use the triangle computations instead of the polygon ones inherited from DefaultPolygon. + */ + + @Override + public Quantity<?> getArea() { + return Triangle.super.getArea(); + } + + @Override + public Point getCentroid() { + return Triangle.super.getCentroid(); + } + + @Override + public Point getPointOnSurface() { + return Triangle.super.getPointOnSurface(); + } + + @Override + public Point getRepresentativePoint() { + return Triangle.super.getRepresentativePoint(); + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/CurvePolygon.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/CurvePolygon.java index 8f6dda385d..cc2f26f727 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/CurvePolygon.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/CurvePolygon.java @@ -25,6 +25,8 @@ import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.Surface; import org.apache.sis.geometries.DataPointsType; +import org.apache.sis.geometries.Point; +import org.apache.sis.geometries.internal.shared.Centroids; import org.apache.sis.geometries.internal.shared.ConcatenatedDataPoints; import org.apache.sis.geometries.internal.shared.DefaultCurvePolygon; @@ -157,6 +159,35 @@ public sealed interface CurvePolygon extends Surface * * @see ISO 19107:2019 - 8.1.2.3 */ + /** + * Returns the centroid of a polygon approximating this surface with linear rings. + * + * @return the centroid, or {@code null} if this surface is empty. + * + * @see ISO 19107:2019 - 6.4.4.8 + */ + @Override + default Point getCentroid() { + return isEmpty() ? null : Centroids.linearize(this).getCentroid(); + } + + /** + * Returns a point inside a polygon approximating this surface with linear rings. + * + * @return a point on this surface, or {@code null} if this surface is empty. + * + * @see ISO 19107:2019 - 6.4.4.19 + */ + @Override + default Point getRepresentativePoint() { + return isEmpty() ? null : Centroids.linearize(this).getRepresentativePoint(); + } + + @Override + default Point getPointOnSurface() { + return getRepresentativePoint(); + } + @UML(identifier="spanningSurface", specification=ISO_19107) default GriddedSurface getSpanningSurface() { //TODO diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/PolyhedralSurface.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/PolyhedralSurface.java index eea04569d5..8ea1733780 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/PolyhedralSurface.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/PolyhedralSurface.java @@ -27,6 +27,8 @@ import org.apache.sis.geometries.GeometryCollection; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.Surface; import org.apache.sis.geometries.SurfaceInterpolation; +import org.apache.sis.geometries.Point; +import org.apache.sis.geometries.internal.shared.Centroids; import org.apache.sis.geometries.internal.shared.ConcatenatedDataPoints; import org.apache.sis.geometries.internal.shared.DefaultPolyhedralSurface; import org.apache.sis.measure.Quantities; @@ -148,6 +150,35 @@ public sealed interface PolyhedralSurface<T extends Polygon> extends /*GeometryC return area; } + /** + * Returns the mean of the centroids of the patches weighted by their area. + * + * @return the centroid, or {@code null} if this surface has no patch. + * + * @see ISO 19107:2019 - 6.4.4.8 + */ + @Override + default Point getCentroid() { + return Centroids.patches(this); + } + + /** + * Returns a point on the largest patch. + * + * @return a point on this surface, or {@code null} if this surface has no patch. + * + * @see ISO 19107:2019 - 6.4.4.19 + */ + @Override + default Point getRepresentativePoint() { + return Centroids.representative(this); + } + + @Override + default Point getPointOnSurface() { + return getRepresentativePoint(); + } + /** * Returns the collection of polygons in this surface that bounds the given polygon “p” for any polygon “p” in the surface. * diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Triangle.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Triangle.java index e1fc1eef30..21b9d1cc63 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Triangle.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/surface/Triangle.java @@ -144,6 +144,42 @@ public sealed interface Triangle extends Polygon return Quantities.create(Curves.ringArea(getExteriorRing().getDataPoints()), Geometries.getAreaUnit(this)); } + /** + * Returns the point at equal weights of the three corners. Each attribute, the position included, + * is interpolated between the corners as declared by its + * {@linkplain DataPointsType#getAttributeInterpolation(String) attribute interpolation}. + * + * @return the centroid of this triangle, or {@code null} if this triangle is empty. + * + * @see ISO 19107:2019 - 6.4.4.8 + */ + @Override + default Point getCentroid() { + if (isEmpty()) { + return null; + } + final double third = 1.0 / 3; + return getExteriorRing().getDataPoints().getInterpolatedPoint(new int[] {0, 1, 2}, new double[] {third, third, third}); + } + + /** + * Returns the {@linkplain #getCentroid() centroid}, which is always inside a triangle. + * + * @see ISO 19107:2019 - 6.4.4.19 + */ + @Override + default Point getRepresentativePoint() { + return getCentroid(); + } + + /** + * Returns the {@linkplain #getCentroid() centroid}, which is always inside a triangle. + */ + @Override + default Point getPointOnSurface() { + return getCentroid(); + } + /** * Compute point distance to triangle. * @param pt point to evaluate diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/CentroidTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/CentroidTest.java new file mode 100644 index 0000000000..ae18bec659 --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/CentroidTest.java @@ -0,0 +1,345 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.geometries; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.geometries.curve.Arc; +import org.apache.sis.geometries.curve.Circle; +import org.apache.sis.geometries.curve.LineString; +import org.apache.sis.geometries.curve.LinearRing; +import org.apache.sis.geometries.curve.OffsetCurve; +import org.apache.sis.geometries.mesh.MeshPrimitive; +import org.apache.sis.geometries.surface.CurvePolygon; +import org.apache.sis.geometries.surface.Polygon; +import org.apache.sis.geometries.surface.Triangle; +import org.apache.sis.maths.Array; +import org.apache.sis.maths.NDArrays; +import org.apache.sis.maths.Vector; +import org.apache.sis.measure.Quantities; +import org.apache.sis.measure.Units; + +// Test dependencies +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + + +/** + * Tests {@link Geometry#getCentroid()} and {@link Geometry#getRepresentativePoint()} on all two-dimensional + * geometry types, including those computed on a linear approximation and the mesh primitives. + * + * @author Johann Sorel (Geomatys) + */ +public class CentroidTest { + + private static final double TOLERANCE = 1E-9; + + /** + * Tolerance for the results computed on a linear approximation of a curve. + */ + private static final double CURVE_TOLERANCE = 1E-5; + + private static final GeometryFactory F = GeometryFactory.DEFAULT; + + private static final CoordinateReferenceSystem CRS2 = Geometries.getUndefinedCRS(2); + + private static DataPoints points(double... c) { + return F.createDataPoints(CRS2, c); + } + + private static Array array(double... c) { + return NDArrays.of(CRS2, c); + } + + private static LineString line(double... c) { + return F.createLineString(points(c)); + } + + private static LinearRing ring(double... c) { + return F.createLinearRing(points(c)); + } + + private static Polygon polygon(LinearRing exterior, LinearRing... holes) { + return F.createPolygon(exterior, List.of(holes)); + } + + private static Circle circle(double cx, double cy, double r) { + return F.createCircle(points(cx+r,cy, cx,cy+r, cx-r,cy, cx,cy-r, cx+r,cy), + array(cx,cy, cx,cy, cx,cy, cx,cy), List.of()); + } + + private static <M extends MeshPrimitive> M mesh(M mesh, double... c) { + mesh.setPositions(array(c)); + return mesh; + } + + private static void assertPoint(double x, double y, Point actual, double tolerance) { + assertNotNull(actual); + final Vector<?> p = actual.getPosition(); + assertEquals(x, p.get(0), tolerance, "x"); + assertEquals(y, p.get(1), tolerance, "y"); + } + + /** + * Verifies that the given point is inside the rectangle. + */ + private static void assertInside(double xmin, double ymin, double xmax, double ymax, Point actual) { + assertNotNull(actual); + final Vector<?> p = actual.getPosition(); + assertTrue(p.get(0) > xmin && p.get(0) < xmax && p.get(1) > ymin && p.get(1) < ymax, "Point outside: " + p); + } + + /** + * Verifies that the given point is one of the given positions. + */ + private static void assertOneOf(Point actual, double... c) { + assertNotNull(actual); + final Vector<?> p = actual.getPosition(); + for (int i = 0; i < c.length; i += 2) { + if (p.get(0) == c[i] && p.get(1) == c[i+1]) return; + } + fail("Point is not one of the positions: " + p); + } + + /** + * Tests points, line strings, polygons and their collections. + */ + @Test + public void testSimpleFeatures() { + Geometry g = F.createPoint(CRS2, 1, 2); + assertPoint(1, 2, g.getCentroid(), TOLERANCE); + assertPoint(1, 2, g.getRepresentativePoint(), TOLERANCE); + + g = F.createMultiPoint(points(0,0, 2,1, 1,3)); + assertPoint(1, 4.0/3, g.getCentroid(), TOLERANCE); + assertOneOf(g.getRepresentativePoint(), 0,0, 2,1, 1,3); + + g = line(0,0, 2,1, 3,3); + assertPoint(1.75, 1.25, g.getCentroid(), TOLERANCE); + assertPoint(2, 1, g.getRepresentativePoint(), TOLERANCE); + + // Segments of lengths 2, 2 and 2√2 with middles (1,0), (2,1) and (1,1). + g = ring(0,0, 2,0, 2,2, 0,0); + final double s2 = Math.sqrt(2); + assertPoint((6 + 2*s2) / (4 + 2*s2), (2 + 2*s2) / (4 + 2*s2), g.getCentroid(), TOLERANCE); + + // Lines of lengths 2 and √5 with middles (1,0) and (1,1.5). + g = F.createMultiLineString(CRS2, line(0,0, 2,0), line(0,1, 2,2)); + final double s5 = Math.sqrt(5); + assertPoint(1, 1.5*s5 / (2 + s5), g.getCentroid(), TOLERANCE); + assertPoint(1, 0, g.getRepresentativePoint(), TOLERANCE); + + // A square of area 100 with a hole of area 4 centered on (2,2). + g = polygon(ring(0,0, 10,0, 10,10, 0,10, 0,0), ring(1,1, 1,3, 3,3, 3,1, 1,1)); + assertPoint(5.125, 5.125, g.getCentroid(), TOLERANCE); + final Vector<?> p = g.getRepresentativePoint().getPosition(); + assertInside(0, 0, 10, 10, g.getRepresentativePoint()); + assertFalse(p.get(0) > 1 && p.get(0) < 3 && p.get(1) > 1 && p.get(1) < 3, "Point in the hole"); + + g = F.createMultiPolygon(CRS2, polygon(ring(0,0, 1,0, 1,1, 0,1, 0,0)), polygon(ring(2,0, 3,0, 3,1, 2,1, 2,0))); + assertPoint(1.5, 0.5, g.getCentroid(), TOLERANCE); + assertInside(0, 0, 1, 1, g.getRepresentativePoint()); + + g = F.createTriangle(ring(0,0, 2,0, 0,2, 0,0)); + assertPoint(2.0/3, 2.0/3, g.getCentroid(), TOLERANCE); + + // Elements of lower dimensions are ignored. + g = F.createGeometryCollection(CRS2, F.createPoint(CRS2, 0, 0), line(1,0, 3,2)); + assertPoint(2, 1, g.getCentroid(), TOLERANCE); + assertPoint(2, 1, g.getRepresentativePoint(), TOLERANCE); + } + + /** + * Tests curves which are not line strings, computed on a linear approximation. + */ + @Test + public void testCurves() { + // Quarter of the unit circle, centroid at 2/π from the center on both axes. + final Arc arc = F.createArc(points(1,0, 0,1), array(0,0), List.of(), false); + assertPoint(2/Math.PI, 2/Math.PI, arc.getCentroid(), CURVE_TOLERANCE); + assertPoint(Math.sqrt(0.5), Math.sqrt(0.5), arc.getRepresentativePoint(), CURVE_TOLERANCE); + + final Circle circle = circle(0, 0, 1); + assertPoint(0, 0, circle.getCentroid(), CURVE_TOLERANCE); + assertPoint(-1, 0, circle.getRepresentativePoint(), CURVE_TOLERANCE); + + // Upper half of the circle of radius 1 centered on (1,0). + final Curve half = F.createCircularString(points(0,0, 1,1, 2,0)); + assertPoint(1, 2/Math.PI, half.getCentroid(), CURVE_TOLERANCE); + assertPoint(1, 1, half.getRepresentativePoint(), CURVE_TOLERANCE); + + // Lines of lengths 2 and 1 with middles (0,0) and (1,0.5). + final Curve compound = F.createCompoundCurve(line(-1,0, 1,0), line(1,0, 1,1)); + assertPoint(1.0/3, 1.0/6, compound.getCentroid(), CURVE_TOLERANCE); + assertPoint(0.5, 0, compound.getRepresentativePoint(), CURVE_TOLERANCE); + + // Symmetric quadratic Bézier curve culminating at (1,1). + final Curve bezier = F.createBezier(points(0,0, 2,0), array(0,0, 1,2, 2,0), new double[] {0, 1}, 2, + null, null, null, null); + final Point centroid = bezier.getCentroid(); + assertEquals(1, centroid.getPosition().get(0), CURVE_TOLERANCE); + assertInside(0, 0, 2, 1, centroid); + assertPoint(1, 1, bezier.getRepresentativePoint(), CURVE_TOLERANCE); + + final OffsetCurve offset = F.createOffsetCurve(line(0,0, 2,1, 3,3), Quantities.create(0.5, Units.UNITY), null); + final LineString linear = offset.asLine(null, null); + assertEquals(linear.getCentroid().getPosition(), offset.getCentroid().getPosition()); + assertEquals(linear.getRepresentativePoint().getPosition(), offset.getRepresentativePoint().getPosition()); + } + + /** + * Tests surfaces which are not polygons. + */ + @Test + public void testSurfaces() { + final CurvePolygon disk = F.createCurvePolygon(circle(0, 0, 2), List.of()); + assertPoint(0, 0, disk.getCentroid(), CURVE_TOLERANCE); + assertTrue(disk.getRepresentativePoint().getPosition().length() < 2); + assertEquals(4 * Math.PI, disk.getArea().getValue().doubleValue(), 1E-4); + + final CurvePolygon square = F.createCurvePolygon(ring(0,0, 2,0, 2,2, 0,2, 0,0), List.of()); + assertPoint(1, 1, square.getCentroid(), TOLERANCE); + assertInside(0, 0, 2, 2, square.getRepresentativePoint()); + + Geometry g = F.createPolyhedralSurface(CRS2, new Polygon[] { + polygon(ring(0,0, 1,0, 1,1, 0,1, 0,0)), polygon(ring(1,0, 2,0, 2,1, 1,1, 1,0))}); + assertPoint(1, 0.5, g.getCentroid(), TOLERANCE); + assertInside(0, 0, 2, 1, g.getRepresentativePoint()); + + g = F.createTIN(CRS2, new Triangle[] { + F.createTriangle(ring(0,0, 2,0, 0,2, 0,0)), F.createTriangle(ring(2,0, 2,2, 0,2, 2,0))}); + assertPoint(1, 1, g.getCentroid(), TOLERANCE); + assertInside(0, 0, 2, 2, g.getRepresentativePoint()); + + // A curve polygon of area 4 centered on (1,1) and a polygon of area 1 centered on (4.5,0.5). + g = F.createGeometryCollection(CRS2, square, polygon(ring(4,0, 5,0, 5,1, 4,1, 4,0))); + assertPoint(1.7, 0.9, g.getCentroid(), TOLERANCE); + } + + /** + * Tests the mesh primitives, which are viewed as collections, line strings or triangulated surfaces. + */ + @Test + public void testMeshes() { + Geometry g = mesh(new MeshPrimitive.Points(), 0,0, 2,0, 1,3); + assertPoint(1, 1, g.getCentroid(), TOLERANCE); + assertOneOf(g.getRepresentativePoint(), 0,0, 2,0, 1,3); + + g = mesh(new MeshPrimitive.Lines(), 0,0, 2,0, 0,1, 2,2); + final double s5 = Math.sqrt(5); + assertPoint(1, 1.5*s5 / (2 + s5), g.getCentroid(), TOLERANCE); + + g = mesh(new MeshPrimitive.LineStrip(), 0,0, 2,1, 3,3); + assertPoint(1.75, 1.25, g.getCentroid(), TOLERANCE); + assertPoint(2, 1, g.getRepresentativePoint(), TOLERANCE); + + g = mesh(new MeshPrimitive.LineLoop(), 0,0, 2,0, 2,2); + assertEquals(ring(0,0, 2,0, 2,2, 0,0).getCentroid().getPosition(), g.getCentroid().getPosition()); + + for (final MeshPrimitive m : new MeshPrimitive[] { + mesh(new MeshPrimitive.Triangles(), 0,0, 2,0, 0,2, 2,0, 2,2, 0,2), + mesh(new MeshPrimitive.TriangleStrip(), 0,0, 0,2, 2,0, 2,2), + mesh(new MeshPrimitive.TriangleFan(), 0,0, 2,0, 2,2, 0,2)}) + { + assertPoint(1, 1, m.getCentroid(), TOLERANCE); + assertInside(0, 0, 2, 2, m.getRepresentativePoint()); + } + } + + /** + * Name of an attribute holding {@code x + 10*y} at each position. Any interpolation of a linear + * function of the position gives the value of that function at the interpolated position. + */ + private static final String ATT_TEST = "TEST"; + + /** + * Creates data points with a {@value #ATT_TEST} attribute. + */ + private static DataPoints attributed(double... c) { + final double[] values = new double[c.length / 2]; + for (int i = 0; i < values.length; i++) { + values[i] = c[2*i] + 10 * c[2*i + 1]; + } + final Map<String,Array> attributes = new LinkedHashMap<>(); + attributes.put(DataPointsType.ATT_POSITION, array(c)); + attributes.put(ATT_TEST, NDArrays.of(1, values)); + return F.createDataPoints(attributes); + } + + private static LinearRing attributedRing(double... c) { + return F.createLinearRing(attributed(c)); + } + + /** + * Verifies that the {@value #ATT_TEST} attribute of the given point is {@code x + 10*y}. + */ + private static void assertAttribute(Point point) { + assertNotNull(point); + final Vector<?> p = point.getPosition(); + final Vector<?> value = point.getAttribute(ATT_TEST); + assertNotNull(value, "Missing attribute"); + assertEquals(p.get(0) + 10 * p.get(1), value.get(0), TOLERANCE); + } + + /** + * Tests that all attributes are interpolated at the centroid and at the representative point. + */ + @Test + public void testAttributes() { + Geometry g = F.createLineString(attributed(0,0, 2,1, 3,3)); + assertAttribute(g.getCentroid()); + assertAttribute(g.getRepresentativePoint()); + + final Triangle triangle = F.createTriangle(attributedRing(0,0, 2,0, 0,2, 0,0)); + assertPoint(2.0/3, 2.0/3, triangle.getCentroid(), TOLERANCE); + assertAttribute(triangle.getCentroid()); + + final Polygon holed = polygon(attributedRing(0,0, 10,0, 10,10, 0,10, 0,0), attributedRing(1,1, 1,3, 3,3, 3,1, 1,1)); + assertPoint(5.125, 5.125, holed.getCentroid(), TOLERANCE); + assertAttribute(holed.getCentroid()); + assertAttribute(holed.getRepresentativePoint()); + + g = F.createMultiPolygon(CRS2, holed, polygon(attributedRing(20,0, 21,0, 21,1, 20,1, 20,0))); + assertAttribute(g.getCentroid()); + + g = F.createTIN(CRS2, new Triangle[] { + F.createTriangle(attributedRing(0,0, 2,0, 0,2, 0,0)), F.createTriangle(attributedRing(2,0, 2,2, 0,2, 2,0))}); + assertPoint(1, 1, g.getCentroid(), TOLERANCE); + assertAttribute(g.getCentroid()); + assertAttribute(g.getRepresentativePoint()); + + final MeshPrimitive.Triangles mesh = mesh(new MeshPrimitive.Triangles(), 0,0, 2,0, 0,2, 2,0, 2,2, 0,2); + mesh.setAttribute(ATT_TEST, NDArrays.of(1, new double[] {0, 2, 20, 2, 22, 20})); + assertAttribute(mesh.getCentroid()); + assertAttribute(mesh.getRepresentativePoint()); + } + + /** + * Tests that normals are normalized after interpolation. + */ + @Test + public void testNormalizedAttribute() { + final Map<String,Array> attributes = new LinkedHashMap<>(); + attributes.put(DataPointsType.ATT_POSITION, array(0,0, 2,0, 0,2, 0,0)); + attributes.put(DataPointsType.ATT_NORMAL, NDArrays.of(3, new double[] {0,0,1, 0,1,0, 1,0,0, 0,0,1})); + final Triangle triangle = F.createTriangle(F.createLinearRing(F.createDataPoints(attributes))); + final double c = 1 / Math.sqrt(3); + assertArrayEquals(new double[] {c, c, c}, triangle.getCentroid().getAttribute(DataPointsType.ATT_NORMAL).toArrayDouble(), TOLERANCE); + } +}
