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 839025133ce11270d43ca7a17d0def2cae7dcc5d
Author: jsorel <[email protected]>
AuthorDate: Tue Oct 13 16:26:53 2026 +0200

    feat(Geometry): refactor Triangle and Mesh interpolation to use default 
DataPoints interpolation capabilities
---
 .../sis/geometries/AttributeInterpolation.java     |   6 +
 .../main/org/apache/sis/geometries/DataPoints.java | 129 ++++++++++++++++++++-
 .../org/apache/sis/geometries/DataPointsType.java  |  17 ++-
 .../main/org/apache/sis/geometries/Point.java      |   3 +-
 .../internal/shared/BSplineEvaluator.java          |   8 +-
 .../sis/geometries/internal/shared/Curves.java     |   4 +-
 .../apache/sis/geometries/surface/Triangle.java    | 128 ++------------------
 .../org/apache/sis/geometries/DataPointsTest.java  | 111 +++++++++++++++---
 .../sis/geometries/curve/LineStringTest.java       |   3 +-
 .../apache/sis/geometries/curve/NurbCurveTest.java |   3 +-
 10 files changed, 263 insertions(+), 149 deletions(-)

diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/AttributeInterpolation.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/AttributeInterpolation.java
index 79d829192f..82bc9b6ccc 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/AttributeInterpolation.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/AttributeInterpolation.java
@@ -41,6 +41,12 @@ public enum AttributeInterpolation {
      * It must follow the curve (distance along the curve) or surface.
      */
     LINEAR,
+    /**
+     * Computed value should be linearly interpolated between geometric 
points, as for {@link #LINEAR},
+     * then normalized to a unit vector. This is the interpolation of 
directions such as normals and
+     * tangents, since a weighted sum of unit vectors is shorter than one.
+     */
+    LINEAR_NORMALIZED,
     /**
      * An attribute value applies to the whole segment until the next point.
      * For a curve of two points [A..B] this means the value should be used on 
the [A..B[ interval, excluding B.
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 2e6972cbf2..f5c64ea245 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
@@ -132,7 +132,7 @@ public interface DataPoints {
 
     /**
      * Returns a new point between the two given points of this {@code 
DataPoints}, for example on a segment.
-     * This is a shortcut for {@link #interpolatePoint(int[], double[])} with 
the weights
+     * This is a shortcut for {@link #getInterpolatedPoint(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.
@@ -140,21 +140,24 @@ public interface DataPoints {
      * @param  ratio  position between the two points, from 0 to 1.
      * @return a new point holding the computed attributes, independent of 
this {@code DataPoints}.
      */
-    default Point interpolatePoint(final int i0, final int i1, final double 
ratio) {
-        return interpolatePoint(new int[] {i0, i1}, new double[] {1 - ratio, 
ratio});
+    default Point getInterpolatedPoint(final int i0, final int i1, final 
double ratio) {
+        return getInterpolatedPoint(new int[] {i0, i1}, new double[] {1 - 
ratio, ratio});
     }
 
     /**
      * Returns a new point interpolated between any number of points of this 
{@code DataPoints}, 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[])}.
+     * computed once by {@link #interpolateAttribute(String, int[], 
double[])}. The returned point is
+     * independent of this {@code DataPoints}: changing either of them does 
not change the other one.
      *
      * @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 {@code DataPoints}.
      * @throws IllegalArgumentException if there is no point, or not one 
weight per point.
+     *
+     * @see #viewInterpolatedPoint(int[], double[])
      */
-    default Point interpolatePoint(final int[] indices, final double[] 
weights) {
+    default Point getInterpolatedPoint(final int[] indices, final double[] 
weights) {
         final DataPointsType type = getType();
         final Point point = GeometryFactory.DEFAULT.createPoint(type);
         for (final String name : type.getAttributeNames()) {
@@ -163,6 +166,24 @@ public interface DataPoints {
         return point;
     }
 
+    /**
+     * Returns a read-only view of the point interpolated between any number 
of points of this
+     * {@code DataPoints}. The attributes of the returned point are not 
stored: each of them is
+     * computed by {@link #interpolateAttribute(String, int[], double[])} 
every time it is requested,
+     * so the view follows the changes of the points of this {@code 
DataPoints}.
+     * Attempts to modify the returned point throw {@link 
UnsupportedOperationException}.
+     *
+     * @param  indices  indices of the points to interpolate between, at least 
one.
+     * @param  weights  weight of each point, in the same order.
+     * @return a read-only view of the interpolated point.
+     * @throws IllegalArgumentException if there is no point, or not one 
weight per point.
+     *
+     * @see #getInterpolatedPoint(int[], double[])
+     */
+    default Point viewInterpolatedPoint(final int[] indices, final double[] 
weights) {
+        return new InterpolatedPoint(this, indices, weights);
+    }
+
     /**
      * Returns the value of an attribute between the two given points of this 
{@code DataPoints}, for example on
      * a segment. This is a shortcut for {@link #interpolateAttribute(String, 
int[], double[])} with the
@@ -188,6 +209,8 @@ public interface DataPoints {
      * <ul>
      *   <li>{@link AttributeInterpolation#LINEAR LINEAR} and {@link 
AttributeInterpolation#TRANSLATE TRANSLATE}:
      *       the weighted sum of the values.</li>
+     *   <li>{@link AttributeInterpolation#LINEAR_NORMALIZED 
LINEAR_NORMALIZED}: the weighted sum of the values,
+     *       normalized to a unit vector.</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
@@ -206,13 +229,21 @@ public interface DataPoints {
             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)) {
+        final AttributeInterpolation method = 
getType().getAttributeInterpolation(name);
+        switch (method) {
             case LINEAR:
+            case LINEAR_NORMALIZED:
             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]));
                 }
+                if (method == AttributeInterpolation.LINEAR_NORMALIZED) {
+                    // TODO: a glTF tangent has 4 components: the direction 
(x,y,z) and the handedness
+                    // (w = ±1) of the bitangent. Only the direction should be 
normalized, while the
+                    // handedness should be kept, for example taken from the 
nearest point.
+                    value.normalize();
+                }
                 return value;
             }
             case LEADING: {
@@ -337,4 +368,90 @@ public interface DataPoints {
         }
         return true;
     }
+
+    /**
+     * A read-only view of a point interpolated between points of a {@code 
DataPoints}.
+     * The attributes are not stored but computed every time they are 
requested.
+     *
+     * @see DataPoints#viewInterpolatedPoint(int[], double[])
+     */
+    final class InterpolatedPoint implements Point {
+        /**
+         * The points between which this point is interpolated.
+         */
+        private final DataPoints points;
+
+        /**
+         * Indices of the points to interpolate between.
+         */
+        private final int[] indices;
+
+        /**
+         * Weight of each point, in the same order as the indices.
+         */
+        private final double[] weights;
+
+        /**
+         * Creates a view of the point interpolated between the given points.
+         *
+         * @throws IllegalArgumentException if there is no point, or not one 
weight per point.
+         */
+        private InterpolatedPoint(final DataPoints points, 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.");
+            }
+            this.points  = points;
+            this.indices = indices.clone();
+            this.weights = weights.clone();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+
+        @Override
+        public Vector<?> getPosition() {
+            return points.interpolateAttribute(DataPointsType.ATT_POSITION, 
indices, weights);
+        }
+
+        /**
+         * Returns the attribute of the given name, interpolated between the 
points of the viewed
+         * {@code DataPoints}, or {@code null} if that {@code DataPoints} has 
no such attribute.
+         */
+        @Override
+        public Vector<?> getAttribute(final String name) {
+            if (!points.getType().getAttributeNames().contains(name)) {
+                return null;
+            }
+            return points.interpolateAttribute(name, indices, weights);
+        }
+
+        /**
+         * Throws {@link UnsupportedOperationException}: this point is a 
read-only view.
+         */
+        @Override
+        public void setAttribute(final String name, final ReadOnly.Vector<?> 
tuple) {
+            throw new UnsupportedOperationException("An interpolated point 
view is read-only.");
+        }
+
+        @Override
+        public DataPointsType getDataPointsType() {
+            return points.getType();
+        }
+
+        @Override
+        public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+            return points.getCoordinateReferenceSystem();
+        }
+
+        /**
+         * Throws {@link UnsupportedOperationException}: this point is a 
read-only view.
+         */
+        @Override
+        public void setCoordinateReferenceSystem(final 
CoordinateReferenceSystem crs) {
+            throw new UnsupportedOperationException("An interpolated point 
view is read-only.");
+        }
+    }
 }
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 4731375cf4..bc401ea75f 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,9 +84,14 @@ 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.
+     * The default implementation returns:
+     *
+     * <ul>
+     *   <li>{@link AttributeInterpolation#LINEAR_NORMALIZED} for the 
{@linkplain #ATT_NORMAL normal}
+     *       and the {@linkplain #ATT_TANGENT tangent}, which are directions 
of unit length;</li>
+     *   <li>{@link AttributeInterpolation#LINEAR} for all other attributes, 
including the
+     *       {@linkplain #ATT_POSITION position}, which is what locates an 
interpolated point.</li>
+     * </ul>
      *
      * TODO : experimentale but needed
      *
@@ -94,7 +99,11 @@ public interface DataPointsType {
      * @return interpolation of the attribute, never null.
      */
     default AttributeInterpolation getAttributeInterpolation(String name) {
-        return ATT_POSITION.equals(name) ? AttributeInterpolation.LINEAR : 
AttributeInterpolation.NEAREST;
+        switch (name) {
+            case ATT_NORMAL:
+            case ATT_TANGENT: return AttributeInterpolation.LINEAR_NORMALIZED;
+            default:          return AttributeInterpolation.LINEAR;
+        }
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java
index d61c4ba788..59100d9cb9 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Point.java
@@ -27,7 +27,6 @@ import 
org.apache.sis.geometries.internal.shared.ArrayDataPoints;
 import org.apache.sis.geometries.internal.shared.DefaultPoint;
 import org.apache.sis.geometries.internal.shared.IndexedPoint;
 import org.apache.sis.geometries.mesh.MeshPrimitive;
-import org.apache.sis.geometries.surface.Triangle;
 import org.apache.sis.maths.ReadOnly;
 import org.apache.sis.maths.Vector;
 
@@ -63,7 +62,7 @@ public sealed interface Point extends Primitive
                 IndexedPoint,
                 ArrayDataPoints.Indexed,
                 MeshPrimitive.Vertex,
-                Triangle.InterpolatedPoint
+                DataPoints.InterpolatedPoint
 {
 
     /**
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/BSplineEvaluator.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/BSplineEvaluator.java
index e0f04b619e..5750e57e7b 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/BSplineEvaluator.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/BSplineEvaluator.java
@@ -22,6 +22,7 @@ import java.util.List;
 import javax.measure.Quantity;
 import javax.measure.Unit;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.AttributeInterpolation;
 import org.apache.sis.geometries.DataPoints;
 import org.apache.sis.geometries.DataPointsType;
 import org.apache.sis.geometries.GeometryFactory;
@@ -360,13 +361,18 @@ final class BSplineEvaluator {
         final Point point = GeometryFactory.DEFAULT.createPoint(type);
         for (final String name : type.getAttributeNames()) {
             final Vector<?> value;
-            switch (type.getAttributeInterpolation(name)) {
+            final AttributeInterpolation method = 
type.getAttributeInterpolation(name);
+            switch (method) {
                 case LINEAR:
+                case LINEAR_NORMALIZED:
                 case TRANSLATE: {
                     value = controls.getAttribute(first, name).scale(R[0]);
                     for (int r = 1; r <= degree; r++) {
                         value.add(controls.getAttribute(first + r, 
name).scale(R[r]));
                     }
+                    if (method == AttributeInterpolation.LINEAR_NORMALIZED) {
+                        value.normalize();
+                    }
                     break;
                 }
                 case LEADING: value = controls.getAttribute(leading, name);  
break;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Curves.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Curves.java
index aad6862ed1..cda7562b28 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Curves.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Curves.java
@@ -112,10 +112,10 @@ public final class Curves {
             final double length = segments[i];
             if (s <= length || i == segments.length - 1) {
                 final double ratio = (length > 0) ? Math.min(s / length, 1) : 
0;
-                return points.interpolatePoint(i, i+1, ratio);
+                return points.getInterpolatedPoint(i, i+1, ratio);
             }
             s -= length;
         }
-        return points.interpolatePoint(0, 0, 0);    // A single position, at 
distance zero.
+        return points.getInterpolatedPoint(0, 0, 0);    // A single position, 
at distance zero.
     }
 }
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 9b3a8bdc07..8fb960cc00 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
@@ -33,11 +33,9 @@ import 
org.apache.sis.geometries.internal.shared.DefaultTriangle;
 import org.apache.sis.geometries.operation.simplify.greedyinsert.WTriangle;
 import 
org.apache.sis.geometries.operation.triangulate.delaunay.OrientedTriangle;
 import org.apache.sis.maths.Maths;
-import org.apache.sis.maths.ReadOnly;
 import org.apache.sis.maths.Vector;
 import org.apache.sis.maths.Vector2D;
 import org.apache.sis.maths.Vector3D;
-import org.apache.sis.maths.Vectors;
 import org.apache.sis.measure.Quantities;
 import org.apache.sis.measure.Units;
 
@@ -156,13 +154,18 @@ public sealed interface Triangle extends Polygon
     }
 
     /**
-     * Interpolate a point in the triangle.
+     * Returns a read-only view of the point of this triangle at the given 
barycentric weights.
+     * Each attribute is interpolated between the three corners as declared by 
its
+     * {@linkplain DataPointsType#getAttributeInterpolation(String) attribute 
interpolation},
+     * every time it is requested.
      *
-     * @param weights each corner barycentric weights
-     * @return interpolated record
+     * @param  weights  barycentric weight of each corner.
+     * @return a view of the interpolated point.
+     *
+     * @see DataPoints#viewInterpolatedPoint(int[], double[])
      */
     default Point interpolate(double[] weights) {
-        return new InterpolatedPoint(this, weights);
+        return getExteriorRing().getDataPoints().viewInterpolatedPoint(new 
int[] {0, 1, 2}, weights);
     }
 
     /**
@@ -257,117 +260,4 @@ public sealed interface Triangle extends Polygon
         return null;
     }
 
-    public static final class InterpolatedPoint implements Point {
-
-        private static final double[][] CORNERS = new double[][]{
-            {1,0,0},
-            {0,1,0},
-            {0,0,1}};
-
-        private final Triangle triangle;
-        private final double[] weights;
-        private final int cornerIdx;
-
-        InterpolatedPoint(Triangle triangle, int cornerIdx) {
-            this.triangle = triangle;
-            this.cornerIdx = cornerIdx;
-            this.weights = CORNERS[cornerIdx];
-        }
-
-        public InterpolatedPoint(Triangle triangle, double[] weights) {
-            this.triangle = triangle;
-            this.weights = weights;
-            this.cornerIdx = -1;
-        }
-
-        public Triangle getTriangle() {
-            return triangle;
-        }
-
-        @Override
-        public boolean isEmpty() {
-            return false;
-        }
-
-        @Override
-        public Vector getPosition() {
-            final DataPoints points = 
triangle.getExteriorRing().getDataPoints();
-            switch (cornerIdx) {
-                case -1 :
-                    return interpolate(
-                        points.getPosition(0),
-                        points.getPosition(1),
-                        points.getPosition(2),
-                        false);
-                default :
-                    return points.getPosition(cornerIdx);
-            }
-        }
-
-        /**
-         * NORMAL and TANGENT attributes interpolation will be normalized.
-         *
-         * @param name attribute name, must be one of the sample dimension 
names
-         * @return triangle corner attribute.
-         */
-        @Override
-        public Vector getAttribute(String name) {
-            final DataPoints points = 
triangle.getExteriorRing().getDataPoints();
-            switch (cornerIdx) {
-                case -1 :
-                    return interpolate(points.getAttribute(0,name),
-                        points.getAttribute(1,name),
-                        points.getAttribute(2,name),
-                        DataPointsType.ATT_NORMAL.equals(name) || 
DataPointsType.ATT_TANGENT.equals(name));
-                default :
-                    return points.getAttribute(cornerIdx, name);
-            }
-        }
-
-        @Override
-        public void setAttribute(String name, ReadOnly.Vector tuple) {
-            throw new UnsupportedOperationException("Not supported on 
interpolated points.");
-        }
-
-        private Vector interpolate(Vector a, Vector b, Vector c, boolean 
normalize) {
-            if (a == null) {
-                //may happen if TIN has 0 sample dimensions.
-                return null;
-            }
-            Vector buffer = a.copy();
-            final int dimension = a.getDimension();
-            switch (dimension) {
-                default :
-                    for (int i = 3; i < dimension; i++) {
-                        buffer.set(i, a.get(i) * weights[0] + b.get(i) * 
weights[1] + c.get(i) * weights[2]);
-                    }
-                case 3 :
-                    buffer.set(2, a.get(2) * weights[0] + b.get(2) * 
weights[1] + c.get(2) * weights[2]);
-                case 2 :
-                    buffer.set(1, a.get(1) * weights[0] + b.get(1) * 
weights[1] + c.get(1) * weights[2]);
-                case 1 :
-                    buffer.set(0, a.get(0) * weights[0] + b.get(0) * 
weights[1] + c.get(0) * weights[2]);
-                case 0 : //do nothing
-            }
-
-            if (normalize) buffer.normalize();
-            return buffer;
-        }
-
-        @Override
-        public DataPointsType getDataPointsType() {
-            return triangle.getDataPointsType();
-        }
-
-        @Override
-        public CoordinateReferenceSystem getCoordinateReferenceSystem() {
-            return triangle.getCoordinateReferenceSystem();
-        }
-
-        @Override
-        public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) 
throws IllegalArgumentException {
-            throw new UnsupportedOperationException("Not supported.");
-        }
-    }
-
 }
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 f1f2a0f1a9..99511dbcae 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
@@ -117,7 +117,8 @@ public class DataPointsTest {
         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,
+                Map.of("nearest", AttributeInterpolation.NEAREST,
+                       "linear",  AttributeInterpolation.LINEAR,
                        "leading", AttributeInterpolation.LEADING));
     }
 
@@ -142,11 +143,11 @@ public class DataPointsTest {
     @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);
+        assertPoint(points.getInterpolatedPoint(0, 1, 0),     0, 0, 10, 10, 
10);
+        assertPoint(points.getInterpolatedPoint(0, 1, 0.25), 2.5, 0, 10, 15, 
10);
+        assertPoint(points.getInterpolatedPoint(0, 1, 0.5),    5, 0, 10, 20, 
10);
+        assertPoint(points.getInterpolatedPoint(0, 1, 0.75), 7.5, 0, 30, 25, 
10);
+        assertPoint(points.getInterpolatedPoint(0, 1, 1),     10, 0, 30, 30, 
30);
     }
 
     /**
@@ -156,9 +157,9 @@ public class DataPointsTest {
     @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);
+        assertPoint(points.getInterpolatedPoint(new int[] {0, 1, 2}, new 
double[] {0.2, 0.3, 0.5}), 3, 5, 30, 23, 10);
+        assertPoint(points.getInterpolatedPoint(new int[] {0, 1, 2}, new 
double[] {0, 1, 0}),      10, 0, 20, 20, 10);
+        assertPoint(points.getInterpolatedPoint(new int[] {2, 0, 1}, new 
double[] {0.5, 0.2, 0.3}), 3, 5, 30, 23, 30);
     }
 
     /**
@@ -191,7 +192,7 @@ public class DataPointsTest {
         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);
+        assertPoint(points.getInterpolatedPoint(new int[] {0, 1, 2, 3}, 
weights), 2.5, 7.5, 40, 31.25, 10);
     }
 
     /**
@@ -200,7 +201,7 @@ public class DataPointsTest {
     @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);
+        assertPoint(points.getInterpolatedPoint(new int[] {1}, new double[] 
{1}), 10, 0, 20, 20, 20);
     }
 
     /**
@@ -209,9 +210,93 @@ public class DataPointsTest {
     @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.getInterpolatedPoint(new int[0], new double[0]));
+        assertThrows(IllegalArgumentException.class, () -> 
points.getInterpolatedPoint(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}));
+        assertThrows(IllegalArgumentException.class, () -> 
points.viewInterpolatedPoint(new int[0], new double[0]));
+        assertThrows(IllegalArgumentException.class, () -> 
points.viewInterpolatedPoint(new int[] {0, 1}, new double[] {1}));
+    }
+
+    /**
+     * The normalized linear interpolation of two orthogonal unit vectors 
gives the unit vector of the
+     * bisector, while their linear interpolation gives a vector of length √½ 
only.
+     */
+    @Test
+    public void testInterpolateLinearNormalized() {
+        final Map<String,Array> attributes = new LinkedHashMap<>();
+        attributes.put(DataPointsType.ATT_POSITION, 
NDArrays.of(GeometryTest.CRS_2D, new double[] {0,0, 10,0}));
+        attributes.put("linear",     NDArrays.of(2, new double[] {1,0, 0,1}));
+        attributes.put("normalized", NDArrays.of(2, new double[] {1,0, 0,1}));
+        final DataPoints points = 
GeometryTest.withInterpolations(GeometryFactory.DEFAULT.createDataPoints(attributes),
+                Map.of("linear",     AttributeInterpolation.LINEAR,
+                       "normalized", 
AttributeInterpolation.LINEAR_NORMALIZED));
+
+        final Vector<?> linear = points.interpolateAttribute("linear", 0, 1, 
0.5);
+        assertEquals(0.5, linear.get(0), GeometryTest.TOLERANCE);
+        assertEquals(0.5, linear.get(1), GeometryTest.TOLERANCE);
+
+        final double h = Math.sqrt(0.5);
+        final Vector<?> normalized = points.interpolateAttribute("normalized", 
0, 1, 0.5);
+        assertEquals(h, normalized.get(0), GeometryTest.TOLERANCE);
+        assertEquals(h, normalized.get(1), GeometryTest.TOLERANCE);
+
+        final Vector<?> fromPoint = points.getInterpolatedPoint(0, 1, 
0.25).getAttribute("normalized");
+        assertEquals(1, fromPoint.length(), GeometryTest.TOLERANCE, "The 
interpolated vector shall have a unit length.");
+        assertEquals(3 / Math.sqrt(10), fromPoint.get(0), 
GeometryTest.TOLERANCE);
+        assertEquals(1 / Math.sqrt(10), fromPoint.get(1), 
GeometryTest.TOLERANCE);
+    }
+
+    /**
+     * Normals and tangents are interpolated as unit vectors without declaring 
it, while other
+     * attributes are interpolated linearly by default.
+     */
+    @Test
+    public void testDefaultInterpolation() {
+        final Map<String,Array> attributes = new LinkedHashMap<>();
+        attributes.put(DataPointsType.ATT_POSITION, 
NDArrays.of(GeometryTest.CRS_2D, new double[] {0,0, 10,0}));
+        attributes.put(DataPointsType.ATT_NORMAL,   NDArrays.of(2, new 
double[] {1,0, 0,1}));
+        attributes.put("other",                     NDArrays.of(2, new 
double[] {1,0, 0,1}));
+        final DataPoints points = 
GeometryFactory.DEFAULT.createDataPoints(attributes);
+        final DataPointsType type = points.getType();
+        assertEquals(AttributeInterpolation.LINEAR,            
type.getAttributeInterpolation(DataPointsType.ATT_POSITION));
+        assertEquals(AttributeInterpolation.LINEAR_NORMALIZED, 
type.getAttributeInterpolation(DataPointsType.ATT_NORMAL));
+        assertEquals(AttributeInterpolation.LINEAR_NORMALIZED, 
type.getAttributeInterpolation(DataPointsType.ATT_TANGENT));
+        assertEquals(AttributeInterpolation.LINEAR,            
type.getAttributeInterpolation("other"));
+
+        final double h = Math.sqrt(0.5);
+        final Vector<?> normal = 
points.interpolateAttribute(DataPointsType.ATT_NORMAL, 0, 1, 0.5);
+        assertEquals(h, normal.get(0), GeometryTest.TOLERANCE);
+        assertEquals(h, normal.get(1), GeometryTest.TOLERANCE);
+
+        final Vector<?> other = points.interpolateAttribute("other", 0, 1, 
0.5);
+        assertEquals(0.5, other.get(0), GeometryTest.TOLERANCE);
+        assertEquals(0.5, other.get(1), GeometryTest.TOLERANCE);
+    }
+
+    /**
+     * A view gives the same values as an interpolated point, but follows the 
changes of the viewed
+     * points and cannot be modified.
+     */
+    @Test
+    public void testViewInterpolatedPoint() {
+        final DataPoints points = 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};
+        final Point copy = points.getInterpolatedPoint(indices, weights);
+        final Point view = points.viewInterpolatedPoint(indices, weights);
+        assertPoint(copy, 3, 5, 30, 23, 10);
+        assertPoint(view, 3, 5, 30, 23, 10);
+        assertEquals(null, view.getAttribute("unknown"), "An attribute absent 
from the viewed points has no value.");
+
+        final Vector<?> moved = points.getPosition(2);
+        moved.set(new double[] {0, 20});
+        points.setPosition(2, moved);
+        weights[0] = 1;                                 // Shall not change 
the view, which copied the weights.
+        GeometryTest.assertPositionEquals(3, 10, view);
+        GeometryTest.assertPositionEquals(3,  5, copy);
+
+        assertThrows(UnsupportedOperationException.class, () -> 
view.setAttribute("linear", copy.getAttribute("linear")));
+        assertThrows(UnsupportedOperationException.class, () -> 
view.setCoordinateReferenceSystem(GeometryTest.CRS_2D));
     }
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/LineStringTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/LineStringTest.java
index 1e8cd52b46..533239332c 100644
--- 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/LineStringTest.java
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/LineStringTest.java
@@ -305,7 +305,8 @@ public abstract class LineStringTest extends GeometryTest {
         attributes.put("leading", NDArrays.of(1, new double[] {0, 10, 30}));
         final LineString curve = 
GeometryFactory.DEFAULT.createLineString(withInterpolations(
                 GeometryFactory.DEFAULT.createDataPoints(attributes),
-                Map.of("linear",  AttributeInterpolation.LINEAR,
+                Map.of("nearest", AttributeInterpolation.NEAREST,
+                       "linear",  AttributeInterpolation.LINEAR,
                        "leading", AttributeInterpolation.LEADING)));
 
         Point point = curve.subCurve(Quantities.create(80, Units.PERCENT));
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/NurbCurveTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/NurbCurveTest.java
index 1ecd09cc7b..1cca871fb5 100644
--- 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/NurbCurveTest.java
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/NurbCurveTest.java
@@ -138,7 +138,8 @@ public class NurbCurveTest extends GeometryTest {
         attributes.put("nearest", NDArrays.of(1, new double[] {0, 0, 30}));
         attributes.put("linear",  NDArrays.of(1, new double[] {0, 0, 30}));
         final NurbCurve arc = 
createQuarterCircle(withInterpolations(GeometryFactory.DEFAULT.createDataPoints(attributes),
-                Map.of("linear", AttributeInterpolation.LINEAR)));
+                Map.of("nearest", AttributeInterpolation.NEAREST,
+                       "linear",  AttributeInterpolation.LINEAR)));
         final Point point = arc.subCurve(Quantities.create(50, Units.PERCENT));
         assertEquals(0, point.getAttribute("nearest").get(0), TOLERANCE, 
"Value of the control point of largest weight.");
         assertEquals(30 * (1 - Math.sqrt(0.5)), 
point.getAttribute("linear").get(0), TOLERANCE, "Blended by the basis 
functions.");

Reply via email to