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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 0b2d6fba0c feat(Geometry): add Vector.slerp, support Tuple arrays in 
ArrayFactory
0b2d6fba0c is described below

commit 0b2d6fba0cd2912d7c4f137da76ce8b44dd1d653
Author: jsorel <[email protected]>
AuthorDate: Tue Sep 8 17:08:31 2026 +0200

    feat(Geometry): add Vector.slerp, support Tuple arrays in ArrayFactory
---
 .../org.apache.sis.geometry/main/module-info.java  |   1 +
 .../solid/polyhedron/AbstractPolyhedron.java       |   5 +-
 .../geometries/solid/polyhedron/Dodecahedron.java  |   2 +
 .../geometries/solid/polyhedron/Hexahedron.java    |   2 +
 .../geometries/solid/polyhedron/Icosahedron.java   |   2 +
 .../geometries/solid/polyhedron/Octahedron.java    |   2 +
 .../solid/polyhedron/RhombicTriacontahedron.java   |   2 +
 .../geometries/solid/polyhedron/Tetrahedron.java   |   2 +
 .../solid/polyhedron/TruncatedIcosahedron.java     |   2 +
 .../sis/geometries/spherical/GreatCircleArc.java   |  40 +++++++
 .../geometries/spherical/SphericalTriangle.java    |  72 ++++++++++---
 .../main/org/apache/sis/maths/ArrayFactory.java    |  56 ++++++++--
 .../main/org/apache/sis/maths/Vector.java          |  22 ++++
 .../main/org/apache/sis/maths/Vectors.java         | 101 ++++++++++++++++++
 .../geometries/spherical/GreatCircleArcTest.java   |  50 +++++++++
 .../spherical/SphericalTriangleTest.java           |  68 ++++++++++++
 .../test/org/apache/sis/maths/VectorsTest.java     | 115 +++++++++++++++++++++
 17 files changed, 518 insertions(+), 26 deletions(-)

diff --git a/incubator/src/org.apache.sis.geometry/main/module-info.java 
b/incubator/src/org.apache.sis.geometry/main/module-info.java
index 53e2b9255f..db680f0f4c 100644
--- a/incubator/src/org.apache.sis.geometry/main/module-info.java
+++ b/incubator/src/org.apache.sis.geometry/main/module-info.java
@@ -37,6 +37,7 @@ module org.apache.sis.geometry {
     exports org.apache.sis.geometries.surface;
     exports org.apache.sis.geometries.solid;
     exports org.apache.sis.geometries.solid.polyhedron;
+    exports org.apache.sis.geometries.spherical;
     exports org.apache.sis.maths;
     exports org.apache.sis.scene;
     exports org.apache.sis.scene.light;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/AbstractPolyhedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/AbstractPolyhedron.java
index 0ac8a35f27..d9d399ad88 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/AbstractPolyhedron.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/AbstractPolyhedron.java
@@ -28,6 +28,7 @@ import org.apache.sis.geometries.surface.Polygon;
 import org.apache.sis.maths.Array;
 import org.apache.sis.maths.NDArrays;
 import org.apache.sis.maths.ReadOnly;
+import org.apache.sis.maths.SampleSystem;
 import org.apache.sis.maths.Vector3D;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -137,7 +138,7 @@ public abstract sealed class AbstractPolyhedron extends 
AbstractGeometry impleme
      * @return unit direction vector
      */
     protected static ReadOnly.Vector<?> fromLatLon(double latRad, double 
lonRad) {
-        return new Vector3D.Double().setFromLatLon(latRad, lonRad);
+        return new 
Vector3D.Double(SampleSystem.cartesian(3)).setFromLatLon(latRad, lonRad);
     }
 
     /**
@@ -150,7 +151,7 @@ public abstract sealed class AbstractPolyhedron extends 
AbstractGeometry impleme
      * @return direction vector
      */
     protected static ReadOnly.Vector<?> fromLatLon(double latRad, double 
lonRad, double radius) {
-        return new Vector3D.Double().setFromLatLon(latRad, 
lonRad).scale(radius);
+        return new 
Vector3D.Double(SampleSystem.cartesian(3)).setFromLatLon(latRad, 
lonRad).scale(radius);
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Dodecahedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Dodecahedron.java
index a79ffe04bd..508602b5d4 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Dodecahedron.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Dodecahedron.java
@@ -73,6 +73,8 @@ public final class Dodecahedron extends AbstractPolyhedron{
         {15, 12, 11, 19, 14, 15}
     };
 
+    public Dodecahedron(){}
+
     @Override
     public int getFaceCount() {
         return FACES.length;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Hexahedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Hexahedron.java
index c89f800214..050614cf16 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Hexahedron.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Hexahedron.java
@@ -51,6 +51,8 @@ public final class Hexahedron extends AbstractPolyhedron{
         {6, 4, 5, 7, 6}
     };
 
+    public Hexahedron() {}
+
     @Override
     public int getFaceCount() {
         return FACES.length;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Icosahedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Icosahedron.java
index 83df0b730c..5c82036213 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Icosahedron.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Icosahedron.java
@@ -73,6 +73,8 @@ public final class Icosahedron extends AbstractPolyhedron{
         {11,  5,  7, 11}
     };
 
+    public Icosahedron() {}
+
     @Override
     public int getFaceCount() {
         return FACES.length;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Octahedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Octahedron.java
index af98108e87..dfcb67227a 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Octahedron.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Octahedron.java
@@ -51,6 +51,8 @@ public final class Octahedron extends AbstractPolyhedron{
         {3, 1, 5, 3}
     };
 
+    public Octahedron() {}
+
     @Override
     public int getFaceCount() {
         return FACES.length;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/RhombicTriacontahedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/RhombicTriacontahedron.java
index ea05f96ff3..62c7b7698c 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/RhombicTriacontahedron.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/RhombicTriacontahedron.java
@@ -110,6 +110,8 @@ public final class RhombicTriacontahedron extends 
AbstractPolyhedron{
         { 9, 24, 11, 27,  9}
     };
 
+    public RhombicTriacontahedron() {}
+
     @Override
     public int getFaceCount() {
         return FACES.length;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Tetrahedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Tetrahedron.java
index d6b88ee4c6..57d6d8f10f 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Tetrahedron.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/Tetrahedron.java
@@ -45,6 +45,8 @@ public final class Tetrahedron extends AbstractPolyhedron{
         {2, 1, 3, 2}
     };
 
+    public Tetrahedron(){}
+
     @Override
     public int getFaceCount() {
         return FACES.length;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/TruncatedIcosahedron.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/TruncatedIcosahedron.java
index b05ec4b539..31283a05bd 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/TruncatedIcosahedron.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/solid/polyhedron/TruncatedIcosahedron.java
@@ -159,6 +159,8 @@ public final class TruncatedIcosahedron extends 
AbstractPolyhedron{
         {47, 46, 42, 43, 54, 55, 47}
     };
 
+    public TruncatedIcosahedron() {}
+
     @Override
     public int getFaceCount() {
         return FACES.length;
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/spherical/GreatCircleArc.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/spherical/GreatCircleArc.java
index 0d553ed186..67075662a5 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/spherical/GreatCircleArc.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/spherical/GreatCircleArc.java
@@ -77,4 +77,44 @@ public final class GreatCircleArc {
         return sphere.getRadius() * Math.acos(cosAngle);
     }
 
+    /**
+     * Get the point at the given fraction of the arc, interpolated along the 
great circle.
+     * <p>
+     * A fraction of 0 returns {@linkplain #getA() A}, a fraction of 1 returns
+     * {@linkplain #getB() B} and 0.5 returns the arc middle. Fractions outside
+     * the [0 .. 1] range are clamped.
+     *
+     * @param fraction position on the arc, in range [0 .. 1]
+     * @return point at given fraction, as a unit direction vector from the 
sphere center
+     * @throws IllegalArgumentException if the two arc ends are antipodal, 
since they do
+     *         not define a unique great circle
+     */
+    public ReadOnly.Vector<?> pointAt(double fraction) {
+        return interpolate(vecA, vecB, fraction);
+    }
+
+    /**
+     * Spherical linear interpolation between two unit direction vectors.
+     * <p>
+     * The result is a unit vector on the great circle passing through both 
vectors, at the
+     * given fraction of the angle between them. Coincident vectors 
interpolate to
+     * themselves. Antipodal vectors are rejected : every great circle passes 
through both
+     * of them, so there is no arc to interpolate along.
+     *
+     * @param vecA first point, as a unit direction vector from the sphere 
center
+     * @param vecB second point, as a unit direction vector from the sphere 
center
+     * @param fraction position on the arc, in range [0 .. 1]
+     * @return point at given fraction, as a unit direction vector from the 
sphere center
+     * @throws IllegalArgumentException if the two vectors are antipodal
+     * @see org.apache.sis.maths.Vector#slerp(ReadOnly.Tuple, double)
+     */
+    public static ReadOnly.Vector<?> interpolate(ReadOnly.Vector<?> vecA, 
ReadOnly.Vector<?> vecB, double fraction) {
+        /*
+         * Slerp already keeps the length of unit vectors, but normalizing 
removes the
+         * rounding it leaves behind. Worth the cost because subdividing a 
cell feeds the
+         * result back in, so the drift would otherwise pile up over the 
levels.
+         */
+        return vecA.copy().slerp(vecB, fraction).normalize();
+    }
+
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/spherical/SphericalTriangle.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/spherical/SphericalTriangle.java
index 19d696db85..981c965bdf 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/spherical/SphericalTriangle.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/spherical/SphericalTriangle.java
@@ -19,6 +19,7 @@ package org.apache.sis.geometries.spherical;
 import org.apache.sis.geometries.solid.Sphere;
 import org.apache.sis.maths.Maths;
 import org.apache.sis.maths.ReadOnly;
+import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -163,22 +164,67 @@ public final class SphericalTriangle {
      * @return regular 4 triangle subdivision
      */
     public SphericalTriangle[] quadSubdivide() {
-        final ReadOnly.Vector<?> vecAB = middle(vecA, vecB);
-        final ReadOnly.Vector<?> vecBC = middle(vecB, vecC);
-        final ReadOnly.Vector<?> vecCA = middle(vecC, vecA);
-        return new SphericalTriangle[]{
-            new SphericalTriangle(sphere, vecA, vecAB, vecCA),
-            new SphericalTriangle(sphere, vecAB, vecB, vecBC),
-            new SphericalTriangle(sphere, vecCA, vecBC, vecC),
-            new SphericalTriangle(sphere, vecAB, vecBC, vecCA)
-        };
+        return subdivide(2);
     }
 
     /**
-     * Get middle unit vector between two vectors.
-     */
-    private static ReadOnly.Vector<?> middle(ReadOnly.Vector<?> p, 
ReadOnly.Vector<?> q) {
-        return p.copy().add(q).normalize();
+     * Subdivide triangle in {@code n*n} triangles perfectly overlapping this 
triangle.
+     * <p>
+     * Each edge is divided in {@code n} equal arcs, and the triangle is cut 
along the
+     * lines joining those divisions, following the same construction as
+     * {@link #quadSubdivide() } generalized to any ratio. All returned 
triangles
+     * keep the CCW order of this triangle.
+     * <p>
+     * The returned array lists the {@code n*(n+1)/2} triangles pointing the 
same way
+     * as this triangle first, ordered by rows starting at corner A and, 
within a row,
+     * from the A→B edge toward the A→C edge. The {@code n*(n-1)/2} triangles 
pointing
+     * the opposite way follow, in the same row order. For {@code n = 2} this 
yields
+     * corner A, corner B, corner C then the center triangle.
+     *
+     * @param n number of divisions of each edge, must be 1 or more
+     * @return regular {@code n*n} triangle subdivision
+     * @throws IllegalArgumentException if {@code n} is less than 1
+     */
+    public SphericalTriangle[] subdivide(final int n) {
+        ArgumentChecks.ensureStrictlyPositive("n", n);
+        if (n == 1) {
+            return new SphericalTriangle[]{this};
+        }
+        /*
+         * Build the triangular lattice of vertices. Row r, for r in [0 .. n], 
holds r+1
+         * vertices spread on the arc going from the A→B edge to the A→C edge 
at fraction
+         * r/n. Row 0 degenerates to corner A, row n is the B→C edge.
+         */
+        final ReadOnly.Vector<?>[][] lattice = new ReadOnly.Vector<?>[n+1][];
+        lattice[0] = new ReadOnly.Vector<?>[]{vecA};
+        for (int r = 1; r <= n; r++) {
+            //the last row ends on the original corners, keep them as they are
+            final ReadOnly.Vector<?> left  = (r == n) ? vecB : 
GreatCircleArc.interpolate(vecA, vecB, r / (double) n);
+            final ReadOnly.Vector<?> right = (r == n) ? vecC : 
GreatCircleArc.interpolate(vecA, vecC, r / (double) n);
+            final ReadOnly.Vector<?>[] row = new ReadOnly.Vector<?>[r+1];
+            row[0] = left;
+            row[r] = right;
+            for (int k = 1; k < r; k++) {
+                row[k] = GreatCircleArc.interpolate(left, right, k / (double) 
r);
+            }
+            lattice[r] = row;
+        }
+
+        final SphericalTriangle[] result = new SphericalTriangle[n*n];
+        int i = 0;
+        //triangles pointing the same way as this triangle
+        for (int r = 0; r < n; r++) {
+            for (int k = 0; k <= r; k++) {
+                result[i++] = new SphericalTriangle(sphere, lattice[r][k], 
lattice[r+1][k], lattice[r+1][k+1]);
+            }
+        }
+        //triangles pointing the opposite way
+        for (int r = 1; r < n; r++) {
+            for (int k = 0; k < r; k++) {
+                result[i++] = new SphericalTriangle(sphere, lattice[r][k], 
lattice[r+1][k+1], lattice[r][k+1]);
+            }
+        }
+        return result;
     }
 
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/ArrayFactory.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/ArrayFactory.java
index cee1b99a82..5d18fce398 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/ArrayFactory.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/ArrayFactory.java
@@ -17,6 +17,7 @@
 package org.apache.sis.maths;
 
 import java.lang.foreign.SegmentAllocator;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -173,6 +174,15 @@ public interface ArrayFactory {
                         throw new IllegalArgumentException("Values iterable is 
not made of Tuple or primitive array");
                     }
                 }
+            } else if (values != null && values.getClass().isArray()) {
+                final Class<?> componentType = 
values.getClass().getComponentType();
+                if (ReadOnly.Tuple.class.isAssignableFrom(componentType)) {
+                    final int size = java.lang.reflect.Array.getLength(values);
+                    if (size != 0) {
+                        final ReadOnly.Tuple tuple = (ReadOnly.Tuple) 
java.lang.reflect.Array.get(values, 0);
+                        return tuple.getSampleSystem();
+                    }
+                }
             }
             return SampleSystem.ofSize(1);
         }
@@ -187,8 +197,13 @@ public interface ArrayFactory {
                 return nd.getShape();
             } else if (values != null && values.getClass().isArray()) {
                 final int size = java.lang.reflect.Array.getLength(values);
-                if ((size % nbSample) != 0) throw new 
IllegalArgumentException("Values size : " + size + "is not a multiple of sample 
system size : " + nbSample);
-                return new long[]{size / nbSample};
+                final Class<?> componentType = 
values.getClass().getComponentType();
+                if (ReadOnly.Tuple.class.isAssignableFrom(componentType)) {
+                    return new long[]{size};
+                } else {
+                    if ((size % nbSample) != 0) throw new 
IllegalArgumentException("Values size : " + size + " is not a multiple of 
sample system size : " + nbSample);
+                    return new long[]{size / nbSample};
+                }
             } else if (values instanceof Collection<?> col) {
                 return new long[]{col.size()};
             }
@@ -202,7 +217,15 @@ public interface ArrayFactory {
                 return nd.getDataType();
             } else if (values != null && values.getClass().isArray()) {
                 final Class<?> componentType = 
values.getClass().getComponentType();
-                return DataType.forPrimitiveType(componentType, false);
+                if (ReadOnly.Tuple.class.isAssignableFrom(componentType)) {
+                    final int size = java.lang.reflect.Array.getLength(values);
+                    if (size != 0) {
+                        final ReadOnly.Tuple tuple = (ReadOnly.Tuple) 
java.lang.reflect.Array.get(values, 0);
+                        return tuple.getDataType();
+                    }
+                } else {
+                    return DataType.forPrimitiveType(componentType, false);
+                }
             } else if (values instanceof Collection<?> col) {
                 final Iterator<?> ite = col.iterator();
                 if (ite.hasNext()) {
@@ -224,16 +247,27 @@ public interface ArrayFactory {
             final int nbDim = target.getSampleSystem().getSize();
 
             if (values != null) {
+                final Class<? extends Object> valueClass = values.getClass();
                 if (values instanceof Array array) {
                     target.set(0, array, 0, array.getLength());
-                } else if (values != null && values.getClass().isArray()) {
-                    int idx = 0;
-                    final Cursor cursor = target.cursor();
-                    while (cursor.next()) {
-                        final Tuple tuple = cursor.samples();
-                        for (int i = 0; i < nbDim; i++) {
-                            tuple.set(i, 
java.lang.reflect.Array.getDouble(values, idx));
-                            idx++;
+                } else if (values != null && valueClass.isArray()) {
+                    Class<?> componentType = valueClass.getComponentType();
+                    if (ReadOnly.Tuple.class.isAssignableFrom(componentType)) {
+                        int idx = 0;
+                        final Cursor cursor = target.cursor();
+                        while (cursor.next()) {
+                            final Tuple tuple = cursor.samples();
+                            tuple.set((ReadOnly.Tuple) 
java.lang.reflect.Array.get(values, idx));
+                        }
+                    } else {
+                        int idx = 0;
+                        final Cursor cursor = target.cursor();
+                        while (cursor.next()) {
+                            final Tuple tuple = cursor.samples();
+                            for (int i = 0; i < nbDim; i++) {
+                                tuple.set(i, 
java.lang.reflect.Array.getDouble(values, idx));
+                                idx++;
+                            }
                         }
                     }
                 } else if (values instanceof Collection<?> col) {
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vector.java 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vector.java
index ce4ded62ea..8f467e7acd 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vector.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vector.java
@@ -102,6 +102,28 @@ public interface Vector<T extends Vector<T>> extends 
Tuple<T>, ReadOnly.Vector<T
         return (T) this;
     }
 
+    /**
+     * Spherical linear interpolation from this vector to the other vector.
+     * <p>
+     * Where {@link #lerp(ReadOnly.Tuple, double) } moves along the straight 
line joining
+     * the two vectors, this moves along the arc joining them, sweeping the 
angle between
+     * them proportionally to the ratio. Both vectors having the same length, 
the result
+     * keeps it too, which makes this the interpolation to use for directions. 
When their
+     * lengths differ, the angle is still swept evenly and the length is 
interpolated
+     * linearly.
+     *
+     * @param other vector to interpolate toward
+     * @param ratio interpolation factor, zero for this vector and one for the 
other vector
+     * @return this vector
+     * @throws IllegalArgumentException if both vectors point in opposite 
directions, in
+     *         which case they define no unique arc to interpolate along
+     * @see Vectors#slerp(double[], double[], double, double[])
+     */
+    default T slerp(ReadOnly.Tuple<?> other, double ratio) {
+        set( Vectors.slerp(toArrayDouble(), other.toArrayDouble(), ratio));
+        return (T) this;
+    }
+
     @Override
     public T copy();
 
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vectors.java 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vectors.java
index 7edfdb8dfd..02f73f1fec 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vectors.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Vectors.java
@@ -762,6 +762,19 @@ public final class Vectors {
         return lerp(start, end, ratio, null);
     }
 
+    /**
+     * Spherically interpolates between given vectors.
+     *
+     * @param start start vector (return value for ratio == 0.)
+     * @param end end vector (return value for ratio == 1.)
+     * @param ratio : 0 is close to start vector, 1 is on end vector
+     * @return the interpolated vector.
+     * @throws IllegalArgumentException if both vectors point in opposite 
directions
+     */
+    public static double[] slerp(final double[] start, final double[] end, 
final double ratio) {
+        return slerp(start, end, ratio, null);
+    }
+
     // 
/////////////////////////////////////////////////////////////////////////
     // OPERATIONS WITH BUFFER 
//////////////////////////////////////////////////
     // 
/////////////////////////////////////////////////////////////////////////
@@ -1192,6 +1205,94 @@ public final class Vectors {
         return buffer;
     }
 
+    /**
+     * Spherically interpolates between given vectors, result is stored in 
buffer.
+     * If buffer is null, a new vector is created.
+     * Vectors must have the same size.
+     * <p>
+     * Where {@link #lerp(double[], double[], double, double[]) } moves along 
the straight
+     * line joining the two vectors, this moves along the arc joining them, so 
the angle
+     * swept is proportional to the ratio. Both vectors keeping the same 
length, the result
+     * keeps it too, which makes this the interpolation to use for directions.
+     * <p>
+     * The direction follows the usual
+     * <var>sin((1-ratio)·Ω)/sin(Ω)·start + sin(ratio·Ω)/sin(Ω)·end</var>, 
where Ω is the
+     * angle between the two vectors, and the length is interpolated linearly. 
For vectors
+     * of equal length that is exactly the formula above ; for vectors of 
different lengths
+     * the angle is still swept evenly, which the plain formula would not do 
because it
+     * would let the longer vector pull the direction toward itself.
+     * <p>
+     * When the two vectors are nearly parallel the arc is indistinguishable 
from the
+     * straight line, and dividing by <var>sin(Ω)</var> would lose precision, 
so a linear
+     * interpolation is used instead. A vector of length zero has no 
direction, so that
+     * case is linear too.
+     *
+     * @param start start vector (return value for ratio == 0.)
+     * @param end end vector (return value for ratio == 1.)
+     * @param ratio : 0 is close to start vector, 1 is on end vector
+     * @param buffer must have same size as start and end vectors or be null.
+     * @return the interpolated vector, buffer if not null
+     * @throws IllegalArgumentException if both vectors point in opposite 
directions, in
+     *         which case they define no unique arc to interpolate along
+     */
+    public static double[] slerp(final double[] start, final double[] end, 
final double ratio, double[] buffer) {
+        if( start.length != end.length ) {
+            throw new IllegalArgumentException("Both vectors must have same 
length.");
+        }
+        if( buffer == null ){
+            buffer = new double[start.length];
+        } else if( start.length != buffer.length ) {
+                throw new IllegalArgumentException("Buffer must have same 
length as start and end vectors.");
+        }
+
+        if (ratio <= 0) {
+            System.arraycopy(start, 0, buffer, 0, start.length);
+            return buffer;
+        } else if (ratio >= 1) {
+            System.arraycopy(end, 0, buffer, 0, end.length);
+            return buffer;
+        }
+
+        /*
+         * Angle between the two directions. Dividing by the lengths keeps the 
angle right
+         * for vectors which are not unit vectors. A vector of length zero has 
no direction,
+         * which the parallel case below handles as a linear interpolation.
+         */
+        final double lengthStart = length(start);
+        final double lengthEnd = length(end);
+        final double lengths = lengthStart * lengthEnd;
+        final double cosAngle = (lengths > 0) ? Maths.clamp(dot(start, end) / 
lengths, -1, 1) : 1;
+        final double angle = Math.acos(cosAngle);
+        final double sinAngle = Math.sin(angle);
+
+        if (sinAngle < 1E-9) {
+            if (cosAngle < 0) {
+                throw new IllegalArgumentException("Cannot interpolate between 
vectors pointing in "
+                        + "opposite directions, they define no unique arc.");
+            }
+            //nearly parallel vectors, the arc is the straight line joining 
them
+            for(int i=0;i<start.length;i++){
+                buffer[i] = (1-ratio)*start[i] + ratio * end[i];
+            }
+            return buffer;
+        }
+
+        /*
+         * The ratios below sweep the angle evenly for vectors of length one, 
so they are
+         * applied to the directions rather than to the vectors, and the 
result is brought
+         * back to the interpolated length. Doing it on the vectors directly 
would let the
+         * longer one pull the direction toward itself.
+         */
+        final double ratioStart = Math.sin(angle * (1-ratio)) / sinAngle / 
lengthStart;
+        final double ratioEnd   = Math.sin(angle *    ratio ) / sinAngle / 
lengthEnd;
+        final double interpolatedLength = (1-ratio)*lengthStart + ratio * 
lengthEnd;
+
+        for(int i=0;i<start.length;i++){
+            buffer[i] = interpolatedLength * (ratioStart*start[i] + ratioEnd * 
end[i]);
+        }
+        return buffer;
+    }
+
     // 
/////////////////////////////////////////////////////////////////////////
     // OPERATIONS WITH MULTIPLE ELEMENTS AT THE SAME TIME 
//////////////////////
     // 
/////////////////////////////////////////////////////////////////////////
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/spherical/GreatCircleArcTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/spherical/GreatCircleArcTest.java
index 74457759a5..d742aa3356 100644
--- 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/spherical/GreatCircleArcTest.java
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/spherical/GreatCircleArcTest.java
@@ -59,4 +59,54 @@ public class GreatCircleArcTest {
         assertEquals(0, emptyArc.getLength(), TOLERANCE);
     }
 
+    /**
+     * Interpolation must land on the arc ends, on the arc middle, and space 
the points
+     * evenly in angle along the way.
+     */
+    @Test
+    public void pointAtTest() {
+        final GreatCircleArc arc = new GreatCircleArc(new Sphere(3), a, b);
+
+        assertArrayEquals(a.toArrayDouble(), arc.pointAt(0).toArrayDouble(), 
TOLERANCE);
+        assertArrayEquals(b.toArrayDouble(), arc.pointAt(1).toArrayDouble(), 
TOLERANCE);
+        //fractions outside the range are clamped
+        assertArrayEquals(a.toArrayDouble(), 
arc.pointAt(-0.5).toArrayDouble(), TOLERANCE);
+        assertArrayEquals(b.toArrayDouble(), arc.pointAt(1.5).toArrayDouble(), 
TOLERANCE);
+
+        //the middle of a quarter circle is at 45 degrees
+        final double h = Math.sqrt(0.5);
+        assertArrayEquals(new double[] {h, h, 0}, 
arc.pointAt(0.5).toArrayDouble(), TOLERANCE);
+
+        //a third of the way is at 30 degrees, which a straight line 
interpolation would miss
+        final double third = Math.PI / 6;
+        assertArrayEquals(new double[] {Math.cos(third), Math.sin(third), 0},
+                          arc.pointAt(1.0 / 3).toArrayDouble(), TOLERANCE);
+
+        //every interpolated point must be a unit vector at the expected angle 
from the start
+        for (int i = 0; i <= 10; i++) {
+            final double fraction = i / 10.0;
+            final ReadOnly.Vector<?> point = arc.pointAt(fraction);
+            assertEquals(1, point.length(), TOLERANCE, "Interpolated points 
must be unit vectors");
+            assertEquals(fraction * Math.PI / 2, Math.acos(point.dot(a)), 
TOLERANCE,
+                         "Points must be evenly spaced in angle");
+        }
+    }
+
+    /**
+     * Coincident points interpolate to themselves, and antipodal points must 
be rejected
+     * rather than silently produce NaN : they do not define a unique great 
circle.
+     */
+    @Test
+    public void pointAtDegenerateTest() {
+        final GreatCircleArc coincident = new GreatCircleArc(new Sphere(3), a, 
a);
+        assertArrayEquals(a.toArrayDouble(), 
coincident.pointAt(0.0).toArrayDouble(), TOLERANCE);
+        assertArrayEquals(a.toArrayDouble(), 
coincident.pointAt(0.5).toArrayDouble(), TOLERANCE);
+        assertArrayEquals(a.toArrayDouble(), 
coincident.pointAt(1.0).toArrayDouble(), TOLERANCE);
+
+        final GreatCircleArc antipodal = new GreatCircleArc(new Sphere(3), a, 
new Vector3D.Double(-1, 0, 0));
+        //the ends themselves are still well defined
+        assertArrayEquals(a.toArrayDouble(), 
antipodal.pointAt(0.0).toArrayDouble(), TOLERANCE);
+        assertThrows(IllegalArgumentException.class, () -> 
antipodal.pointAt(0.5));
+        assertThrows(IllegalArgumentException.class, () -> 
antipodal.pointAt(0.25));
+    }
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/spherical/SphericalTriangleTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/spherical/SphericalTriangleTest.java
index ac001962a8..e80fc887b1 100644
--- 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/spherical/SphericalTriangleTest.java
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/spherical/SphericalTriangleTest.java
@@ -142,4 +142,72 @@ public class SphericalTriangleTest {
         return p.copy().add(q).normalize().toArrayDouble();
     }
 
+    /**
+     * A subdivision in one is the triangle itself.
+     */
+    @Test
+    public void subdivideByOneTest() {
+        final SphericalTriangle triangle = new SphericalTriangle(new 
Sphere(3), a, b, c);
+        final SphericalTriangle[] children = triangle.subdivide(1);
+        assertEquals(1, children.length);
+        assertSame(triangle, children[0]);
+    }
+
+    /**
+     * Children of a subdivision must exactly tile their parent, so their 
areas must add up
+     * to the parent area, they must all keep the CCW order and their centers 
must all fall
+     * inside the parent.
+     */
+    @Test
+    public void subdivideTilesTheParentTest() {
+        final SphericalTriangle triangle = new SphericalTriangle(new 
Sphere(3), a, b, c);
+        for (final int n : new int[] {2, 3, 4, 5}) {
+            final SphericalTriangle[] children = triangle.subdivide(n);
+            assertEquals(n*n, children.length, "Subdivision by " + n);
+
+            double sum = 0;
+            for (SphericalTriangle child : children) {
+                sum += child.getArea();
+                /*
+                 * The spherical excess is computed from the distances between 
the corners,
+                 * so it stays positive whatever the order. The triple product 
is what tells
+                 * the order apart, and it must stay positive for a CCW 
triangle.
+                 */
+                assertTrue(child.getA().dot(child.getB().cross(child.getC())) 
> 0,
+                        "Child of a subdivision by " + n + " must keep the CCW 
order");
+                assertTrue(triangle.contains(child.getCentroidVector()),
+                        "Child center must fall inside the parent");
+            }
+            assertEquals(triangle.getArea(), sum, 1e-9, "Children of a 
subdivision by " + n + " must tile it");
+        }
+    }
+
+    /**
+     * A subdivision must reuse the parent corners as they are, and place the 
new vertices
+     * on the parent edges.
+     */
+    @Test
+    public void subdivideKeepsParentCornersTest() {
+        final SphericalTriangle triangle = new SphericalTriangle(new 
Sphere(3), a, b, c);
+        final SphericalTriangle[] children = triangle.subdivide(3);
+        assertEquals(9, children.length);
+        /*
+         * The 6 triangles pointing the same way as the parent come first, by 
rows starting
+         * at corner A, so corner A is in the first one, corner B in the first 
of the last
+         * row and corner C in the last of that row.
+         */
+        assertSame(a, children[0].getA());
+        assertSame(b, children[3].getB());
+        assertSame(c, children[5].getC());
+    }
+
+    /**
+     * Subdividing must reject a ratio below one.
+     */
+    @Test
+    public void subdivideInvalidTest() {
+        final SphericalTriangle triangle = new SphericalTriangle(new 
Sphere(3), a, b, c);
+        assertThrows(IllegalArgumentException.class, () -> 
triangle.subdivide(0));
+        assertThrows(IllegalArgumentException.class, () -> 
triangle.subdivide(-1));
+    }
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/maths/VectorsTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/maths/VectorsTest.java
index e94d32d8b6..5c09bbc92d 100644
--- 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/maths/VectorsTest.java
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/maths/VectorsTest.java
@@ -124,4 +124,119 @@ public class VectorsTest {
         assertArrayEquals(new double[] {32767, 32767, 32767}, 
coord2.toArrayDouble(), DELTA);
 
     }
+
+    private static final double SLERP_TOLERANCE = 1e-12;
+
+    /**
+     * Spherical interpolation must sweep the angle evenly, and must keep the 
length of
+     * the vectors, which is what sets it apart from a linear interpolation.
+     */
+    @Test
+    public void slerpTest() {
+        final double[] start = {1, 0, 0};
+        final double[] end   = {0, 1, 0};
+
+        //the ends are returned as they are, and ratios outside the range are 
clamped
+        assertArrayEquals(start, Vectors.slerp(start, end, 0), 
SLERP_TOLERANCE);
+        assertArrayEquals(end,   Vectors.slerp(start, end, 1), 
SLERP_TOLERANCE);
+        assertArrayEquals(start, Vectors.slerp(start, end, -1), 
SLERP_TOLERANCE);
+        assertArrayEquals(end,   Vectors.slerp(start, end, 2), 
SLERP_TOLERANCE);
+
+        //the middle of a quarter circle is at 45 degrees, not at the middle 
of the chord
+        final double h = Math.sqrt(0.5);
+        assertArrayEquals(new double[] {h, h, 0}, Vectors.slerp(start, end, 
0.5), SLERP_TOLERANCE);
+        //where a linear interpolation would fall short, inside the circle
+        assertEquals(Math.sqrt(0.5), Vectors.length(Vectors.lerp(start, end, 
0.5)), SLERP_TOLERANCE);
+
+        //a third of the way is a third of the angle, so 30 degrees
+        assertArrayEquals(new double[] {Math.cos(Math.PI/6), 
Math.sin(Math.PI/6), 0},
+                          Vectors.slerp(start, end, 1.0/3), SLERP_TOLERANCE);
+
+        //the length is kept, and the angle grows proportionally to the ratio
+        for (int i = 0; i <= 10; i++) {
+            final double ratio = i / 10.0;
+            final double[] point = Vectors.slerp(start, end, ratio);
+            assertEquals(1, Vectors.length(point), SLERP_TOLERANCE, "Length 
must be kept");
+            assertEquals(ratio * Math.PI/2, Math.acos(Vectors.dot(point, 
start)), SLERP_TOLERANCE,
+                         "Angle must grow proportionally to the ratio");
+        }
+    }
+
+    /**
+     * Spherical interpolation must work in any dimension, and on vectors 
which are not
+     * unit vectors.
+     */
+    @Test
+    public void slerpOtherLengthsAndDimensionsTest() {
+        //2D vectors of length 3
+        final double[] start2D = {3, 0};
+        final double[] end2D   = {0, 3};
+        final double[] mid2D = Vectors.slerp(start2D, end2D, 0.5);
+        assertEquals(3, Vectors.length(mid2D), SLERP_TOLERANCE);
+        assertArrayEquals(new double[] {3*Math.sqrt(0.5), 3*Math.sqrt(0.5)}, 
mid2D, SLERP_TOLERANCE);
+
+        //4D vectors
+        final double[] start4D = {1, 0, 0, 0};
+        final double[] end4D   = {0, 0, 0, 1};
+        final double[] mid4D = Vectors.slerp(start4D, end4D, 0.5);
+        assertEquals(1, Vectors.length(mid4D), SLERP_TOLERANCE);
+        assertArrayEquals(new double[] {Math.sqrt(0.5), 0, 0, Math.sqrt(0.5)}, 
mid4D, SLERP_TOLERANCE);
+
+        //vectors of different lengths still sweep the angle evenly
+        final double[] shortV = {1, 0, 0};
+        final double[] longV  = {0, 4, 0};
+        final double[] mid = Vectors.slerp(shortV, longV, 0.5);
+        assertEquals(Math.PI/4, Math.acos(Vectors.dot(mid, shortV) / 
Vectors.length(mid)), SLERP_TOLERANCE);
+    }
+
+    /**
+     * Interpolating between aligned vectors : parallel vectors have no arc to 
sweep so a
+     * linear interpolation is used, and opposite vectors define no unique arc 
at all so
+     * they must be rejected rather than silently produce NaN.
+     */
+    @Test
+    public void slerpAlignedTest() {
+        final double[] a = {1, 0, 0};
+
+        //parallel vectors of the same length
+        assertArrayEquals(a, Vectors.slerp(a, a.clone(), 0.5), 
SLERP_TOLERANCE);
+        //parallel vectors of different lengths interpolate linearly
+        assertArrayEquals(new double[] {2, 0, 0}, Vectors.slerp(a, new 
double[] {3, 0, 0}, 0.5), SLERP_TOLERANCE);
+        //a vector of length zero has no direction, the interpolation stays 
linear
+        assertArrayEquals(new double[] {0.5, 0, 0}, Vectors.slerp(a, new 
double[] {0, 0, 0}, 0.5), SLERP_TOLERANCE);
+
+        //opposite vectors
+        final double[] opposite = {-1, 0, 0};
+        assertThrows(IllegalArgumentException.class, () -> Vectors.slerp(a, 
opposite, 0.5));
+        //but the ends themselves are still well defined
+        assertArrayEquals(a, Vectors.slerp(a, opposite, 0), SLERP_TOLERANCE);
+        assertArrayEquals(opposite, Vectors.slerp(a, opposite, 1), 
SLERP_TOLERANCE);
+    }
+
+    /**
+     * Vectors of different sizes cannot be interpolated, and a buffer must 
match.
+     */
+    @Test
+    public void slerpInvalidTest() {
+        final double[] v3 = {1, 0, 0};
+        final double[] v2 = {0, 1};
+        assertThrows(IllegalArgumentException.class, () -> Vectors.slerp(v3, 
v2, 0.5));
+        assertThrows(IllegalArgumentException.class, () -> Vectors.slerp(v3, 
v3.clone(), 0.5, new double[2]));
+    }
+
+    /**
+     * The interpolation must also be reachable from the vector itself, 
mutating it.
+     */
+    @Test
+    public void slerpOnVectorTest() {
+        final Vector3D.Double start = new Vector3D.Double(1, 0, 0);
+        final Vector3D.Double end   = new Vector3D.Double(0, 1, 0);
+        final Vector3D.Double result = start.slerp(end, 0.5);
+        //the vector is mutated in place and returned
+        assertSame(start, result);
+        final double h = Math.sqrt(0.5);
+        assertArrayEquals(new double[] {h, h, 0}, result.toArrayDouble(), 
SLERP_TOLERANCE);
+        //the other vector is left alone
+        assertArrayEquals(new double[] {0, 1, 0}, end.toArrayDouble(), 
SLERP_TOLERANCE);
+    }
 }

Reply via email to