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