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.");
