This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit ef9e4355d6837e0a20f29a8734fe81db2db48fe5
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Mar 21 21:35:15 2021 +0100

    Add `LinearTransformBuilder.approximate(MathTransform, int...)` for 
building linear approximation of a non-linear transform in a given domain.
---
 .../apache/sis/internal/referencing/Resources.java |  5 ++
 .../sis/internal/referencing/Resources.properties  |  1 +
 .../internal/referencing/Resources_fr.properties   |  1 +
 .../sis/referencing/datum/DatumShiftGrid.java      |  3 +
 .../operation/builder/LinearTransformBuilder.java  | 91 ++++++++++++++++++++++
 .../operation/builder/ResidualGrid.java            | 71 +++++++++++++++--
 .../operation/transform/DatumShiftTransform.java   |  2 +
 .../operation/transform/InterpolatedTransform.java | 13 +++-
 .../builder/LinearTransformBuilderTest.java        | 37 +++++++++
 .../builder/LocalizationGridBuilderTest.java       | 38 ++++++++-
 .../main/java/org/apache/sis/util/ArraysExt.java   | 18 +++++
 11 files changed, 272 insertions(+), 8 deletions(-)

diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
index 1d1710e..5992138 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
@@ -496,6 +496,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short ParameterNotFound_2 = 61;
 
         /**
+         * Points are not on a regular grid.
+         */
+        public static final short PointsAreNotOnRegularGrid = 99;
+
+        /**
          * Recursive call while creating an object of type ‘{0}’ for code 
“{1}”.
          */
         public static final short RecursiveCreateCallForCode_2 = 62;
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
index 7e356ce..0f2a5dd 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
@@ -111,6 +111,7 @@ NoSuchAuthorityCode_3             = No \u2018{1}\u2019 
object found for code \u2
 NoSuchAuthorityCodeInSubset_4     = No \u2018{1}\u2019 object found for code 
\u201c{2}\u201d. However only a subset of the {0} geodetic dataset has been 
queried. See {3} for instruction about how to install the full {0} database.
 NoSuchOperationMethod_1           = No operation method found for name or 
identifier \u201c{0}\u201d.
 ParameterNotFound_2               = No parameter named \u201c{1}\u201d has 
been found in \u201c{0}\u201d.
+PointsAreNotOnRegularGrid         = Points are not on a regular grid.
 RecursiveCreateCallForCode_2      = Recursive call while creating an object of 
type \u2018{0}\u2019 for code \u201c{1}\u201d.
 SingularMatrix                    = Matrix is singular.
 StartOrEndPointNotSet_1           = The {0,choice,0#start|1#end} point has not 
been specified.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
index e789f0e..17d1c77 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
@@ -116,6 +116,7 @@ NoSuchAuthorityCode_3             = Aucun objet de type 
\u2018{1}\u2019 n\u2019a
 NoSuchAuthorityCodeInSubset_4     = Aucun objet de type \u2018{1}\u2019 
n\u2019a \u00e9t\u00e9 trouv\u00e9 pour le code \u00ab\u202f{2}\u202f\u00bb. 
Toutefois seul un sous-ensemble de la base de donn\u00e9es 
g\u00e9od\u00e9siques {0} a \u00e9t\u00e9 interrog\u00e9. Voir {3} pour des 
instructions d\u2019installation de la base de donn\u00e9es {0} compl\u00e8te.
 NoSuchOperationMethod_1           = Aucune m\u00e9thode n\u2019a \u00e9t\u00e9 
trouv\u00e9e pour le nom ou l\u2019identifiant \u00ab\u202f{0}\u202f\u00bb.
 ParameterNotFound_2               = Aucun param\u00e8tre nomm\u00e9 
\u00ab\u202f{1}\u202f\u00bb n\u2019a \u00e9t\u00e9 trouv\u00e9 dans 
\u00ab\u202f{0}\u202f\u00bb.
+PointsAreNotOnRegularGrid         = Les points ne sont pas sur une grille 
r\u00e9guli\u00e8re.
 RecursiveCreateCallForCode_2      = Appels r\u00e9cursifs lors de la 
cr\u00e9ation d\u2019un objet de type \u2018{0}\u2019 pour le code 
\u00ab\u202f{1}\u202f\u00bb.
 SingularMatrix                    = La matrice est singuli\u00e8re.
 StartOrEndPointNotSet_1           = Le point {0,choice,0#de 
d\u00e9part|1#d\u2019arriv\u00e9} n\u2019a pas \u00e9t\u00e9 d\u00e9fini.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
index ab08860..08cc73b 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
@@ -213,6 +213,9 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, 
T extends Quantity<T
     /**
      * Number of grid cells along each dimension. This is usually an array of 
length {@value #INTERPOLATED_DIMENSIONS}
      * containing the number of grid cells along the <var>x</var> and 
<var>y</var> axes.
+     *
+     * @see #getGridSize()
+     * @see #getGridSize(int)
      */
     private final int[] gridSize;
 
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
index 46377c4..c70b38a 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
@@ -36,6 +36,7 @@ import org.opengis.geometry.coordinate.Position;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.math.Line;
@@ -248,6 +249,43 @@ public class LinearTransformBuilder extends 
TransformBuilder {
     }
 
     /**
+     * Returns a linear approximation of the given transform for the specified 
domain.
+     * The source positions are all integer coordinates in a rectangle from 
(0, 0, …) inclusive
+     * to {@code gridSize} exclusive. The target positions are the results of 
transforming all
+     * source coordinates with the given {@code gridToCRS} transform.
+     * This method is equivalent to the following code, but potentially more 
efficient:
+     *
+     * {@preformat java
+     *     val builder = new LinearTransformBuilder(gridSize);
+     *     builder.setControlPoints(gridToCRS);
+     *     return builder.create(null);
+     * }
+     *
+     * @param  gridToCRS  the transform from source coordinates (grid indices) 
to target coordinates.
+     * @param  gridSize   the upper values (exclusive) of source coordinates.
+     * @return a linear approximation of given transform for the specified 
domain.
+     * @throws FactoryException if the transform approximation can not be 
computed.
+     *
+     * @see #setControlPoints(MathTransform)
+     *
+     * @since 1.1
+     */
+    public static LinearTransform approximate(final MathTransform gridToCRS, 
final int... gridSize) throws FactoryException {
+        ArgumentChecks.ensureNonNull("gridToCRS", gridToCRS);
+        try {
+            final LinearTransform mt = ResidualGrid.approximate(gridToCRS, 
gridSize);
+            if (mt != null) {
+                return mt;
+            }
+            final LinearTransformBuilder builder = new 
LinearTransformBuilder(gridSize);
+            builder.setControlPoints(gridToCRS);
+            return builder.create(null);
+        } catch (TransformException e) {
+            throw new FactoryException(e);
+        }
+    }
+
+    /**
      * Returns the grid size for the given dimension. It is caller's 
responsibility to ensure that
      * this method is invoked only on instances created by {@link 
#LinearTransformBuilder(int...)}.
      *
@@ -533,6 +571,59 @@ search: for (int j=numPoints; --j >= 0;) {
 
     /**
      * Sets all control point (source, target) pairs, overwriting any previous 
setting.
+     * The source positions are all integer coordinates in a rectangle from 
(0, 0, …) inclusive
+     * to {@code gridSize} exclusive where {@code gridSize} is an {@code 
int[]} array specified
+     * {@linkplain #LinearTransformBuilder(int...) at construction time}. The 
target positions are
+     * the results of transforming all source coordinates with the given 
{@code gridToCRS} transform.
+     *
+     * @param  gridToCRS  the transform from source coordinates (grid indices) 
to target coordinates.
+     * @throws TransformException if a coordinate value can not be transformed.
+     *
+     * @see #approximate(MathTransform, int...)
+     *
+     * @since 1.1
+     */
+    public void setControlPoints(final MathTransform gridToCRS) throws 
TransformException {
+        ArgumentChecks.ensureNonNull("gridToCRS", gridToCRS);
+        if (gridSize == null) {
+            throw new 
IllegalStateException(Resources.format(Resources.Keys.PointsAreNotOnRegularGrid));
+        }
+        final int srcDim = gridToCRS.getSourceDimensions();
+        if (srcDim != gridSize.length) {
+            throw new MismatchedDimensionException(Errors.format(
+                    Errors.Keys.MismatchedTransformDimension_4, "gridToCRS", 
0, gridSize.length, srcDim));
+        }
+        final int tgtDim = gridToCRS.getTargetDimensions();
+        allocate(tgtDim);
+        final int width       = gridSize[0];        // We will transform 
points by chunks of 1 line.
+        final int widthTarget = width * tgtDim;
+        final double[] buffer = new double[Math.max(srcDim, tgtDim) * width];
+        final double[] point  = new double[srcDim];
+        for (int j=0; j<gridLength; j += width) {
+            // Prepare one row of coordinates with only x varying: (0,y,z), 
(1,y,z), (2,y,z), (3,y,z), etc.
+            for (int i=0; i<width; i++) {
+                point[0] = i;
+                System.arraycopy(point, 0, buffer, i*srcDim, srcDim);
+            }
+            // Transform and store the result in the target arrays.
+            gridToCRS.transform(buffer, 0, buffer, 0, width);
+            for (int dim=0; dim<tgtDim; dim++) {
+                final double[] target = targets[dim];
+                int i = j;
+                for (int t=dim; t < widthTarget; t += tgtDim) {
+                    target[i++] = buffer[t];
+                }
+            }
+            // Increment y coordinaate, then z (if any) if needed.
+            for (int i=1; i<gridSize.length; i++) {
+                if (point[i]++ < gridSize[i]) break;
+                point[i] = 0;
+            }
+        }
+    }
+
+    /**
+     * Sets all control point (source, target) pairs, overwriting any previous 
setting.
      * The source positions are the keys in given map, and the target 
positions are the associated values.
      * The map should not contain two entries with the same source position.
      * Coordinate reference systems are ignored.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ResidualGrid.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ResidualGrid.java
index a3078f7..bce70ef 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ResidualGrid.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ResidualGrid.java
@@ -22,14 +22,19 @@ import javax.measure.quantity.Dimensionless;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.referencing.datum.DatumShiftGrid;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.ContextualParameters;
+import org.apache.sis.referencing.operation.transform.InterpolatedTransform;
 import org.apache.sis.internal.referencing.WKTUtilities;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.io.wkt.FormattableObject;
@@ -37,6 +42,7 @@ import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
+import org.apache.sis.util.ArraysExt;
 
 
 /**
@@ -96,9 +102,8 @@ final class ResidualGrid extends 
DatumShiftGrid<Dimensionless,Dimensionless> {
             MatrixSIS m = ((ContextualParameters) 
parameters).getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
             m.setMatrix(denormalization);
         }
-        final int[] size = getGridSize();
-        parameters.parameter(Constants.NUM_ROW).setValue(size[1]);
-        parameters.parameter(Constants.NUM_COL).setValue(size[0]);
+        parameters.parameter(Constants.NUM_ROW).setValue(getGridSize(1));
+        parameters.parameter(Constants.NUM_COL).setValue(getGridSize(0));
         parameters.parameter("grid_x").setValue(new Data(0, denormalization));
         parameters.parameter("grid_y").setValue(new Data(1, denormalization));
     }
@@ -177,7 +182,6 @@ final class ResidualGrid extends 
DatumShiftGrid<Dimensionless,Dimensionless> {
         this.offsets        = residuals;
         this.accuracy       = precision;
         this.scanlineStride = nx;
-        double[] periodVector = null;
         if (periods != null && linearizer == null && gridToTarget.isAffine()) {
             /*
              * We require the transform to be affine because it makes the 
Jacobian independent of
@@ -199,8 +203,9 @@ final class ResidualGrid extends 
DatumShiftGrid<Dimensionless,Dimensionless> {
              * period of 12 months, then `replaceOutsideGridCoordinates(…)` 
will only shift by 360°
              * AND 12 months together, never 360° only or 12 months only.
              */
+        } else {
+            periodVector = null;
         }
-        this.periodVector = periodVector;
     }
 
     /**
@@ -300,7 +305,7 @@ final class ResidualGrid extends 
DatumShiftGrid<Dimensionless,Dimensionless> {
      * Geocentric interpolations add the translation to coordinates converted 
to geocentric coordinates.</p>
      *
      * @author  Martin Desruisseaux (Geomatys)
-     * @version 1.0
+     * @version 1.1
      * @since   1.0
      * @module
      */
@@ -325,6 +330,9 @@ final class ResidualGrid extends 
DatumShiftGrid<Dimensionless,Dimensionless> {
 
         /** Computes the matrix element in the given row and column. */
         @Override public double  getElement(final int y, final int x) {
+            if ((x | y) < 0 || x >= scanlineStride) {
+                throw new IndexOutOfBoundsException();
+            }
             return c0 * (x + getCellValue(0, x, y)) +                // TODO: 
use Math.fma with JDK9.
                    c1 * (y + getCellValue(1, x, y)) +
                    c2;
@@ -387,6 +395,57 @@ final class ResidualGrid extends 
DatumShiftGrid<Dimensionless,Dimensionless> {
     }
 
     /**
+     * If a linear approximation can be extracted from the given transform, 
returns that approximation.
+     * The approximation applies to source coordinates in the range 0 to 
{@code size[i]} for each source
+     * dimension <var>i</var>.
+     *
+     * <p>This method does not perform expansive calculation; it searches only 
for transforms than can
+     * be processed easily. This method is defined in this class because it 
checks for transformations
+     * backed by {@link ResidualGrid} instances.</p>
+     *
+     * @param  size  upper coordinate values of the source domain for which to 
get a linear approximation.
+     * @return the linear approximation of given transform, or {@code null} if 
none readily available.
+     */
+    static LinearTransform approximate(final MathTransform gridToCRS, final 
int[] size) throws TransformException {
+        if (size.length != SOURCE_DIMENSION) {
+            return null;
+        }
+        MathTransform result = null;
+        for (final MathTransform step : MathTransforms.getSteps(gridToCRS)) {
+            if (step instanceof LinearTransform) {
+                if (result == null) {
+                    result = step;
+                } else {
+                    result = MathTransforms.concatenate(result, step);
+                }
+            } else {
+                /*
+                 * Non-linear transform found. If it is backed by a 
`ResidualGrid` and the specified domain
+                 * contains the full `ResidualGrid` domain, then we consider 
that `step` is approximated by
+                 * an identity transform. Otherwise this method can not 
process `gridToCRS`.
+                 */
+                if (step instanceof InterpolatedTransform) {
+                    final DatumShiftGrid<?,?> grid = ((InterpolatedTransform) 
step).getShiftGrid();
+                    if (grid instanceof ResidualGrid) {
+                        GeneralEnvelope bounds = new GeneralEnvelope(new 
double[size.length], ArraysExt.copyAsDoubles(size));
+                        if (result != null) {
+                            bounds = Envelopes.transform(result, bounds);
+                        }
+                        for (int i=0; i<size.length; i++) {
+                            if (bounds.getMinimum(i) > 0 || 
bounds.getMaximum(i) < grid.getGridSize(i)) {
+                                return null;
+                            }
+                        }
+                        continue;
+                    }
+                }
+                return null;
+            }
+        }
+        return (LinearTransform) result;
+    }
+
+    /**
      * Returns {@code true} if the given object is a grid containing the same 
data than this grid.
      */
     @Override
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
index f1115b6..9f0de32 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
@@ -97,6 +97,8 @@ public abstract class DatumShiftTransform extends 
AbstractMathTransform implemen
 
     /**
      * The grid of datum shifts from source datum to target datum, or {@code 
null} if none.
+     *
+     * @see InterpolatedTransform#getShiftGrid()
      */
     final DatumShiftGrid<?,?> grid;
 
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedTransform.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedTransform.java
index 6e736fb..53800ce 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedTransform.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedTransform.java
@@ -68,7 +68,7 @@ import org.apache.sis.internal.referencing.DirectPositionView;
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Simon Reynard (Geomatys)
  * @author  Rueben Schulz (UBC)
- * @version 1.0
+ * @version 1.1
  *
  * @see DatumShiftGrid
  * @see org.apache.sis.referencing.operation.builder.LocalizationGridBuilder
@@ -232,6 +232,17 @@ public class InterpolatedTransform extends 
DatumShiftTransform {
     }
 
     /**
+     * Returns the grid of datum shifts specified at construction time.
+     *
+     * @return the grid of datum shifts from source to target datum.
+     *
+     * @since 1.1
+     */
+    public final DatumShiftGrid<?,?> getShiftGrid() {
+        return grid;
+    }
+
+    /**
      * Returns the number of input dimensions.
      * This fixed to {@link DatumShiftGrid#getTranslationDimensions()}.
      *
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilderTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilderTest.java
index 0a4ff0b..4cee9c2 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilderTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilderTest.java
@@ -24,8 +24,10 @@ import java.awt.geom.AffineTransform;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.HardCodedConversions;
 import org.apache.sis.geometry.DirectPosition1D;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.test.DependsOnMethod;
@@ -411,6 +413,41 @@ public final strictfp class LinearTransformBuilderTest 
extends TestCase {
     }
 
     /**
+     * Tests {@link LinearTransformBuilder#setControlPoints(MathTransform)}.
+     * This test uses the Mercator projection as a source of non-linear 
transform
+     * and verify that we can get a linear approximation from it for a small 
region.
+     *
+     * @throws TransformException if an error occurred during map projection.
+     * @throws FactoryException if the transform can not be created.
+     */
+    @Test
+    public void testSetPointsFromTransform() throws TransformException, 
FactoryException {
+        final LinearTransformBuilder builder = new LinearTransformBuilder(3, 
5);
+        
builder.setControlPoints(HardCodedConversions.mercator().getConversionFromBase().getMathTransform());
+        assertPointEquals(builder, 0, 0,      0,      0);
+        assertPointEquals(builder, 1, 0, 111319,      0);
+        assertPointEquals(builder, 2, 0, 222639,      0);
+        assertPointEquals(builder, 2, 1, 222639, 110580);
+        assertPointEquals(builder, 2, 2, 222639, 221194);
+        assertPointEquals(builder, 1, 3, 111319, 331877);
+        assertPointEquals(builder, 1, 4, 111319, 442662);
+        final Matrix m = builder.create(null).getMatrix();
+        assertMatrixEquals("linear",
+                new Matrix3(111319, 0,   0,
+                            0, 110662, -62,
+                            0, 0, 1), m, 0.5);
+    }
+
+    /**
+     * Asserts that the builder contains the expected target coordinates for a 
given source grid coordinates.
+     */
+    private static void assertPointEquals(final LinearTransformBuilder builder,
+            final int gridX, final int gridY, final int expectedX, final int 
expectedY)
+    {
+        assertArrayEquals(new double[] {expectedX, expectedY}, 
builder.getControlPoint(new int[] {gridX,gridY}), 0.5);
+    }
+
+    /**
      * Tests the effect of {@link LinearTransformBuilder#addLinearizers(Map, 
int...)}.
      *
      * @throws FactoryException if the transform can not be created.
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
index 803cefa..0d10365 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
@@ -19,9 +19,13 @@ package org.apache.sis.referencing.operation.builder;
 import java.awt.geom.Point2D;
 import java.awt.geom.AffineTransform;
 import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.test.referencing.TransformTestCase;
+import org.apache.sis.referencing.operation.HardCodedConversions;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.geometry.Envelope2D;
+import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.test.DependsOn;
 import org.junit.Test;
 
@@ -32,7 +36,7 @@ import static org.apache.sis.test.ReferencingAssert.*;
  * Tests {@link LocalizationGridBuilder}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.8
  * @module
  */
@@ -146,4 +150,36 @@ public final strictfp class LocalizationGridBuilderTest 
extends TransformTestCas
         assertArrayEquals(new double[] {-8.5, -123.7}, builder.getRow(1, 
2).doubleValues(), STRICT);
         assertArrayEquals(new double[] {-21.7, -26.2, -123.7}, 
builder.getColumn(1, 1).doubleValues(), STRICT);
     }
+
+    /**
+     * Tests {@link LinearTransformBuilder#approximate(MathTransform, int...)} 
on a transform created by
+     * {@link LocalizationGridBuilder}. We verify that the {@link 
ResidualGrid#approximate(MathTransform, int[])}
+     * short path is executed.
+     *
+     * @throws TransformException if an error occurred while computing test 
points.
+     * @throws FactoryException if an error occurred while computing the 
localization grid.
+     *
+     * @see LinearTransformBuilderTest#testSetPointsFromTransform()
+     */
+    @Test
+    public void testApproximate() throws TransformException, FactoryException {
+        // Same set of points than 
`LinearTransformBuilderTest.testSetPointsFromTransform()`.
+        final int[] gridSize = {3, 5};
+        final LinearTransformBuilder points = new 
LinearTransformBuilder(gridSize);
+        
points.setControlPoints(HardCodedConversions.mercator().getConversionFromBase().getMathTransform());
+
+        // Non-linear transform producing the same values than the set of 
points;
+        final LocalizationGridBuilder builder = new 
LocalizationGridBuilder(points);
+        transform = builder.create(null);
+        assertFalse(transform instanceof LinearTransform);
+
+        // Linear approximation by Least Square Root method.
+        final LinearTransform linear = 
LinearTransformBuilder.approximate(transform, gridSize);
+        org.opengis.test.Assert.assertMatrixEquals("linear",
+                new Matrix3(111319, 0,   0,
+                            0, 110662, -62,
+                            0, 0, 1), linear.getMatrix(), 0.5);
+
+        assertSame("Should have extracted the existing instance instead of 
computing a new one.", points.create(null), linear);
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
index 4167311..adb4433 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
@@ -1863,6 +1863,24 @@ public final class ArraysExt extends Static {
     }
 
     /**
+     * Returns a copy of the given array where each value has been casted to 
the {@code double} type.
+     *
+     * @param  data  the array to copy, or {@code null}.
+     * @return a copy of the given array with values casted to the {@code 
double} type,
+     *         or {@code null} if the given array was null.
+     *
+     * @since 1.1
+     */
+    public static double[] copyAsDoubles(final int[] data) {
+        if (data == null) return null;
+        final double[] result = new double[data.length];
+        for (int i=0; i<data.length; i++) {
+            result[i] = data[i];
+        }
+        return result;
+    }
+
+    /**
      * Returns a copy of the given array where each value has been casted to 
the {@code float} type.
      * This method does not verify if the casts would cause data loss.
      *

Reply via email to