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

Reply via email to