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 829e767d5ae3903283630b7a57e0125e368029c3
Author: jsorel <[email protected]>
AuthorDate: Tue Oct 13 14:44:29 2026 +0200

    feat(Geometry): implement DataPoints interpolation
---
 .../main/org/apache/sis/geometries/DataPoints.java |  98 +++++++++++++++++
 .../org/apache/sis/geometries/DataPointsType.java  |   8 +-
 .../org/apache/sis/geometries/GeometryFactory.java |  27 +++++
 .../org/apache/sis/geometries/DataPointsTest.java  | 118 +++++++++++++++++++++
 4 files changed, 250 insertions(+), 1 deletion(-)

diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DataPoints.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DataPoints.java
index 2b699574e5..c5bbfbebe2 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DataPoints.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DataPoints.java
@@ -130,6 +130,104 @@ public interface DataPoints {
      */
     void setAttribute(int index, String name, ReadOnly.Vector<?> value);
 
+    /**
+     * Returns a new point between the two given points of this sequence, for 
example on a segment.
+     * This is a shortcut for {@link #interpolatePoint(int[], double[])} with 
the weights
+     * 1 − {@code ratio} and {@code ratio}.
+     *
+     * @param  i0     index of the point at ratio 0, the start of the segment.
+     * @param  i1     index of the point at ratio 1, the end of the segment.
+     * @param  ratio  position between the two points, from 0 to 1.
+     * @return a new point holding the computed attributes, independent of 
this sequence.
+     */
+    default Point interpolatePoint(final int i0, final int i1, final double 
ratio) {
+        return interpolatePoint(new int[] {i0, i1}, new double[] {1 - ratio, 
ratio});
+    }
+
+    /**
+     * Returns a new point interpolated between any number of points of this 
sequence, for example on
+     * a segment or inside a triangle. Each attribute of the new point, the 
position included, is
+     * computed by {@link #interpolateAttribute(String, int[], double[])}.
+     *
+     * @param  indices  indices of the points to interpolate between, at least 
one.
+     * @param  weights  weight of each point, in the same order.
+     * @return a new point holding the computed attributes, independent of 
this sequence.
+     * @throws IllegalArgumentException if there is no point, or not one 
weight per point.
+     */
+    default Point interpolatePoint(final int[] indices, final double[] 
weights) {
+        final DataPointsType type = getType();
+        final Point point = GeometryFactory.DEFAULT.createPoint(type);
+        for (final String name : type.getAttributeNames()) {
+            point.setAttribute(name, interpolateAttribute(name, indices, 
weights));
+        }
+        return point;
+    }
+
+    /**
+     * Returns the value of an attribute between the two given points of this 
sequence, for example on
+     * a segment. This is a shortcut for {@link #interpolateAttribute(String, 
int[], double[])} with the
+     * weights 1 − {@code ratio} and {@code ratio}.
+     *
+     * @param  name   name of the attribute.
+     * @param  i0     index of the point at ratio 0, the start of the segment.
+     * @param  i1     index of the point at ratio 1, the end of the segment.
+     * @param  ratio  position between the two points, from 0 to 1.
+     * @return the attribute value, independent of this sequence.
+     */
+    default Vector<?> interpolateAttribute(final String name, final int i0, 
final int i1, final double ratio) {
+        return interpolateAttribute(name, new int[] {i0, i1}, new double[] {1 
- ratio, ratio});
+    }
+
+    /**
+     * Returns the value of an attribute interpolated between any number of 
points of this sequence,
+     * for example on a segment, inside a triangle or inside a quadrilateral. 
The weights are the
+     * generalized barycentric coordinates of the position where the value is 
computed: they are
+     * positive and sum to 1. The value is computed as declared by the
+     * {@linkplain DataPointsType#getAttributeInterpolation(String) attribute 
interpolation}:
+     *
+     * <ul>
+     *   <li>{@link AttributeInterpolation#LINEAR LINEAR} and {@link 
AttributeInterpolation#TRANSLATE TRANSLATE}:
+     *       the weighted sum of the values.</li>
+     *   <li>{@link AttributeInterpolation#NEAREST NEAREST}: the value of the 
point of largest weight,
+     *       the first one on ties.</li>
+     *   <li>{@link AttributeInterpolation#LEADING LEADING}: the value of the 
first point, which starts
+     *       the segment or the surface. Between two points, which is a 
segment, the end point keeps
+     *       its own value.</li>
+     * </ul>
+     *
+     * @param  name     name of the attribute.
+     * @param  indices  indices of the points to interpolate between, at least 
one.
+     * @param  weights  weight of each point, in the same order.
+     * @return the attribute value, independent of this sequence.
+     * @throws IllegalArgumentException if there is no point, or not one 
weight per point.
+     */
+    default Vector<?> interpolateAttribute(final String name, final int[] 
indices, final double[] weights) {
+        if (indices.length == 0 || weights.length != indices.length) {
+            throw new IllegalArgumentException("Expected at least one point 
with one weight each, but got "
+                    + indices.length + " points and " + weights.length + " 
weights.");
+        }
+        switch (getType().getAttributeInterpolation(name)) {
+            case LINEAR:
+            case TRANSLATE: {
+                final Vector<?> value = getAttribute(indices[0], 
name).scale(weights[0]);
+                for (int i = 1; i < indices.length; i++) {
+                    value.add(getAttribute(indices[i], 
name).scale(weights[i]));
+                }
+                return value;
+            }
+            case LEADING: {
+                return getAttribute(indices[(indices.length == 2 && weights[1] 
>= 1) ? 1 : 0], name);
+            }
+            default: {
+                int nearest = 0;
+                for (int i = 1; i < weights.length; i++) {
+                    if (weights[i] > weights[nearest]) nearest = i;
+                }
+                return getAttribute(indices[nearest], name);
+            }
+        }
+    }
+
     /**
      * Get all attribute values as an Array.
      *
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DataPointsType.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DataPointsType.java
index 457862053a..4731375cf4 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DataPointsType.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DataPointsType.java
@@ -84,11 +84,17 @@ public interface DataPointsType {
 
     /**
      * Get how values should be interpolation in geometric operations.
+     * The default implementation returns {@link 
AttributeInterpolation#LINEAR} for the
+     * {@linkplain #ATT_POSITION position}, since it is what locates an 
interpolated point,
+     * and {@link AttributeInterpolation#NEAREST} for all other attributes.
      *
      * TODO : experimentale but needed
+     *
+     * @param  name  name of the attribute.
+     * @return interpolation of the attribute, never null.
      */
     default AttributeInterpolation getAttributeInterpolation(String name) {
-        return AttributeInterpolation.NEAREST;
+        return ATT_POSITION.equals(name) ? AttributeInterpolation.LINEAR : 
AttributeInterpolation.NEAREST;
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
index 9a4b9ccfc1..bbbcc8c08e 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
@@ -175,6 +175,22 @@ public final class GeometryFactory {
         return new ArrayDataPoints(attributes);
     }
 
+    /**
+     * Creates a sequence of the given size holding the attributes declared by 
the given type.
+     * All the values are initialized to zero.
+     *
+     * @param  type  the attributes of the sequence, which shall include the 
position.
+     * @param  size  number of points in the sequence.
+     * @return a new sequence of the given size.
+     */
+    public DataPoints createDataPoints(DataPointsType type, int size) {
+        final Map<String,Array> attributes = new LinkedHashMap<>();
+        for (final String name : type.getAttributeNames()) {
+            attributes.put(name, NDArrays.of(type.getAttributeSystem(name), 
type.getAttributeType(name), size));
+        }
+        return createDataPoints(attributes);
+    }
+
     /**
      * Returns a copy of the given DataPoints.
      */
@@ -220,6 +236,17 @@ public final class GeometryFactory {
         return new DefaultPoint(sequence);
     }
 
+    /**
+     * Creates a point holding the attributes declared by the given type.
+     * All the values are initialized to zero.
+     *
+     * @param  type  the attributes of the point, which shall include the 
position.
+     * @return a new point.
+     */
+    public Point createPoint(DataPointsType type) {
+        return createPoint(createDataPoints(type, 1));
+    }
+
     // ////////////////////////////////////////////////////////////////////////
     // Curves /////////////////////////////////////////////////////////////////
     // ////////////////////////////////////////////////////////////////////////
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/DataPointsTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/DataPointsTest.java
index 7df18ff71d..47e1754cee 100644
--- 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/DataPointsTest.java
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/DataPointsTest.java
@@ -16,7 +16,16 @@
  */
 package org.apache.sis.geometries;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import org.apache.sis.maths.Array;
+import org.apache.sis.maths.NDArrays;
+import org.apache.sis.maths.Vector;
+
 // Test dependencies
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
@@ -96,4 +105,113 @@ public class DataPointsTest {
     @Disabled("Not implemented yet.")
     public void testGetAttributeRange() {
     }
+
+    /**
+     * Creates a sequence of the given positions, holding the given values in 
three attributes
+     * interpolated in the three possible ways.
+     */
+    private static DataPoints createPoints(final double[] positions, final 
double[] values) {
+        final Map<String,Array> attributes = new LinkedHashMap<>();
+        attributes.put(DataPointsType.ATT_POSITION, 
NDArrays.of(GeometryTest.CRS_2D, positions));
+        attributes.put("nearest", NDArrays.of(1, values.clone()));
+        attributes.put("linear",  NDArrays.of(1, values.clone()));
+        attributes.put("leading", NDArrays.of(1, values.clone()));
+        return 
GeometryTest.withInterpolations(GeometryFactory.DEFAULT.createDataPoints(attributes),
+                Map.of("linear",  AttributeInterpolation.LINEAR,
+                       "leading", AttributeInterpolation.LEADING));
+    }
+
+    /**
+     * Asserts that the given point has the expected position and attribute 
values.
+     */
+    private static void assertPoint(final Point point, final double x, final 
double y,
+            final double nearest, final double linear, final double leading)
+    {
+        GeometryTest.assertPositionEquals(x, y, point);
+        assertEquals(Set.of(DataPointsType.ATT_POSITION, "nearest", "linear", 
"leading"),
+                     
Set.copyOf(point.getDataPointsType().getAttributeNames()));
+        assertEquals(nearest, point.getAttribute("nearest").get(0), 
GeometryTest.TOLERANCE, "nearest");
+        assertEquals(linear,  point.getAttribute("linear" ).get(0), 
GeometryTest.TOLERANCE, "linear");
+        assertEquals(leading, point.getAttribute("leading").get(0), 
GeometryTest.TOLERANCE, "leading");
+    }
+
+    /**
+     * Interpolation on the segment from (0 0) to (10 0), holding the values 
10 and 30.
+     * The leading value applies to the whole segment except its end point.
+     */
+    @Test
+    public void testInterpolateSegment() {
+        final DataPoints points = createPoints(new double[] {0,0, 10,0}, new 
double[] {10, 30});
+        assertPoint(points.interpolatePoint(0, 1, 0),     0, 0, 10, 10, 10);
+        assertPoint(points.interpolatePoint(0, 1, 0.25), 2.5, 0, 10, 15, 10);
+        assertPoint(points.interpolatePoint(0, 1, 0.5),    5, 0, 10, 20, 10);
+        assertPoint(points.interpolatePoint(0, 1, 0.75), 7.5, 0, 30, 25, 10);
+        assertPoint(points.interpolatePoint(0, 1, 1),     10, 0, 30, 30, 30);
+    }
+
+    /**
+     * Interpolation in the triangle (0 0) (10 0) (0 10), holding the values 
10, 20 and 30, at the
+     * barycentric coordinates (0.2, 0.3, 0.5). The leading value applies to 
the whole triangle.
+     */
+    @Test
+    public void testInterpolateTriangle() {
+        final DataPoints points = createPoints(new double[] {0,0, 10,0, 0,10}, 
new double[] {10, 20, 30});
+        assertPoint(points.interpolatePoint(new int[] {0, 1, 2}, new double[] 
{0.2, 0.3, 0.5}), 3, 5, 30, 23, 10);
+        assertPoint(points.interpolatePoint(new int[] {0, 1, 2}, new double[] 
{0, 1, 0}),      10, 0, 20, 20, 10);
+        assertPoint(points.interpolatePoint(new int[] {2, 0, 1}, new double[] 
{0.5, 0.2, 0.3}), 3, 5, 30, 23, 30);
+    }
+
+    /**
+     * A single attribute is interpolated without creating a point, the same 
way as in a point.
+     */
+    @Test
+    public void testInterpolateAttribute() {
+        final DataPoints segment = createPoints(new double[] {0,0, 10,0}, new 
double[] {10, 30});
+        assertEquals(10, segment.interpolateAttribute("nearest", 0, 1, 
0.25).get(0), GeometryTest.TOLERANCE);
+        assertEquals(15, segment.interpolateAttribute("linear",  0, 1, 
0.25).get(0), GeometryTest.TOLERANCE);
+        assertEquals(30, segment.interpolateAttribute("leading", 0, 1, 
1).get(0),    GeometryTest.TOLERANCE);
+        final Vector<?> position = 
segment.interpolateAttribute(DataPointsType.ATT_POSITION, 0, 1, 0.25);
+        assertEquals(2.5, position.get(0), GeometryTest.TOLERANCE);
+        assertEquals(0,   position.get(1), GeometryTest.TOLERANCE);
+
+        final DataPoints triangle = createPoints(new double[] {0,0, 10,0, 
0,10}, new double[] {10, 20, 30});
+        final int[]    indices = {0, 1, 2};
+        final double[] weights = {0.2, 0.3, 0.5};
+        assertEquals(30, triangle.interpolateAttribute("nearest", indices, 
weights).get(0), GeometryTest.TOLERANCE);
+        assertEquals(23, triangle.interpolateAttribute("linear",  indices, 
weights).get(0), GeometryTest.TOLERANCE);
+        assertEquals(10, triangle.interpolateAttribute("leading", indices, 
weights).get(0), GeometryTest.TOLERANCE);
+    }
+
+    /**
+     * Interpolation in the square (0 0) (10 0) (10 10) (0 10), holding the 
values 10, 20, 30 and 40,
+     * with the bilinear weights of the position (2.5 7.5). The leading value 
applies to the whole square.
+     */
+    @Test
+    public void testInterpolateQuadrilateral() {
+        final DataPoints points = createPoints(new double[] {0,0, 10,0, 10,10, 
0,10}, new double[] {10, 20, 30, 40});
+        final double s = 0.25, t = 0.75;
+        final double[] weights = {(1-s)*(1-t), s*(1-t), s*t, (1-s)*t};
+        assertPoint(points.interpolatePoint(new int[] {0, 1, 2, 3}, weights), 
2.5, 7.5, 40, 31.25, 10);
+    }
+
+    /**
+     * Interpolation with a single point gives the values of that point.
+     */
+    @Test
+    public void testInterpolateSinglePoint() {
+        final DataPoints points = createPoints(new double[] {0,0, 10,0}, new 
double[] {10, 20});
+        assertPoint(points.interpolatePoint(new int[] {1}, new double[] {1}), 
10, 0, 20, 20, 20);
+    }
+
+    /**
+     * At least one point, with one weight each, is required.
+     */
+    @Test
+    public void testInterpolateInvalid() {
+        final DataPoints points = createPoints(new double[] {0,0, 10,0, 0,10, 
10,10}, new double[] {10, 20, 30, 40});
+        assertThrows(IllegalArgumentException.class, () -> 
points.interpolatePoint(new int[0], new double[0]));
+        assertThrows(IllegalArgumentException.class, () -> 
points.interpolatePoint(new int[] {0, 1, 2}, new double[] {0.5, 0.5}));
+        assertThrows(IllegalArgumentException.class, () -> 
points.interpolateAttribute("linear", new int[0], new double[0]));
+        assertThrows(IllegalArgumentException.class, () -> 
points.interpolateAttribute("linear", new int[] {0, 1}, new double[] {1}));
+    }
 }

Reply via email to