You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/03/22 15:05:25 UTC

[sis] branch geoapi-4.0 updated: Refactor `LinearTransformBuilder.approximate(…)` implementation in a separated class. Change method signature and add a `GridGeometry.linearize(…)` method.

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 07257ce  Refactor `LinearTransformBuilder.approximate(…)` implementation in a separated class. Change method signature and add a `GridGeometry.linearize(…)` method.
07257ce is described below

commit 07257cef751d7698e3a3c15b63882273ff54ebe8
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Mar 22 14:26:44 2021 +0100

    Refactor `LinearTransformBuilder.approximate(…)` implementation in a separated class.
    Change method signature and add a `GridGeometry.linearize(…)` method.
---
 .../org/apache/sis/coverage/grid/GridExtent.java   |   6 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java |  52 +++++++
 .../operation/builder/LinearTransformBuilder.java  |  31 ++--
 .../referencing/operation/builder/Linearizer.java  | 158 +++++++++++++++++++++
 .../operation/builder/ResidualGrid.java            |  57 --------
 .../operation/builder/LinearizerTest.java          |  72 ++++++++++
 .../builder/LocalizationGridBuilderTest.java       |  38 +----
 .../sis/test/suite/ReferencingTestSuite.java       |   1 +
 8 files changed, 299 insertions(+), 116 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index 293a864..0e4b37c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -916,8 +916,9 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable
     /**
      * Transforms this grid extent to a "real world" envelope using the given transform.
      * The transform shall map <em>cell corner</em> to real world coordinates.
+     * This method does not set the envelope coordinate reference system.
      *
-     * @param  cornerToCRS  a transform from <em>cell corners</em> to real world coordinates.
+     * @param  cornerToCRS  a transform from <em>cell corners</em> to real world coordinates, or {@code null} if none.
      * @param  gridToCRS    the transform specified by the user. May be the same as {@code cornerToCRS}.
      *                      If different, then this is assumed to map cell centers instead of cell corners.
      * @param  fallback     bounds to use if some values still NaN, or {@code null} if none.
@@ -936,6 +937,9 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable
             if (high != Long.MAX_VALUE) high++;             // Make the coordinate exclusive before cast.
             envelope.setRange(i, coordinates[i], high);     // Possible loss of precision in cast to `double` type.
         }
+        if (cornerToCRS == null) {
+            return envelope;
+        }
         envelope = Envelopes.transform(cornerToCRS, envelope);
         if (envelope.isEmpty()) try {
             /*
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 833740f..768ab7e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -51,6 +51,7 @@ import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.PassThroughTransform;
+import org.apache.sis.referencing.operation.builder.LinearTransformBuilder;
 import org.apache.sis.internal.referencing.DirectPositionView;
 import org.apache.sis.internal.referencing.TemporalAccessor;
 import org.apache.sis.internal.referencing.AxisDirections;
@@ -1290,6 +1291,57 @@ public class GridGeometry implements LenientComparable, Serializable {
     }
 
     /**
+     * Creates a grid geometry with a linear approximation of the <cite>grid to CRS</cite> transform.
+     * The approximation is computed by <cite>Least Mean Squares</cite> method: the affine transform
+     * coefficients are chosen in way making the average value of (<var>position</var> − <var>linear
+     * approximation of position</var>)² as small as possible for all cells in this grid geometry.
+     * If the <cite>grid to CRS</cite> transform of this grid geometry is already linear, then this
+     * method returns {@code this}.
+     *
+     * @param  forceLowerToZero  whether to force lower grid coordinates to (0,0,…).
+     * @return a grid geometry with a linear approximation of the <cite>grid to CRS</cite> transform.
+     * @throws TransformException if some cell coordinates can not be computed.
+     *
+     * @see LinearTransformBuilder#approximate(MathTransform, Envelope)
+     * @see GridExtent#startsAtZero()
+     */
+    public GridGeometry linearize(final boolean forceLowerToZero) throws TransformException {
+        if (nonLinears != 0 && extent != null) try {
+            GeneralEnvelope domain    = extent.toCRS(null, null, null);
+            MathTransform approximate = LinearTransformBuilder.approximate(gridToCRS, domain);
+            MathTransform gridToGrid  = MathTransforms.concatenate(gridToCRS, approximate.inverse());
+            domain = Envelopes.transform(gridToGrid, domain);
+            final int dimension = domain.getDimension();
+            final long[] coordinates = new long[dimension * 2];
+            final double[] shift = new double[dimension];
+            for (int i=0; i<dimension; i++) {
+                long low  = Math.round(domain.getMinimum(i));
+                long high = Math.round(domain.getMaximum(i));
+                high = Math.max(low, Math.decrementExact(high));
+                if (forceLowerToZero) {
+                    high = Math.subtractExact(high, low);
+                    shift[i] = low;
+                } else {
+                    coordinates[i] = low;
+                }
+                coordinates[i + dimension] = high;
+            }
+            approximate = MathTransforms.concatenate(MathTransforms.translation(shift), approximate);
+            if (!approximate.equals(gridToCRS)) {
+                return new GridGeometry(new GridExtent(extent, coordinates), PixelInCell.CELL_CENTER,
+                                        approximate, envelope.getCoordinateReferenceSystem());
+            }
+        } catch (FactoryException e) {
+            final Throwable cause = e.getCause();
+            if (cause instanceof TransformException) {
+                throw (TransformException) cause;
+            }
+            throw new TransformException(e);
+        }
+        return this;
+    }
+
+    /**
      * Creates a transform from cell coordinates in this grid to cell coordinates in the given grid.
      * The returned transform handles change of Coordinate Reference System and wraparound axes
      * (e.g. longitude axis crossing the ±180° meridian) if applicable.
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 c70b38a..fc78b3e 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
@@ -228,7 +228,7 @@ public class LinearTransformBuilder extends TransformBuilder {
      * @since 0.8
      */
     public LinearTransformBuilder(int... gridSize) {
-        ArgumentChecks.ensureNonNull("gridSize", gridSize);
+        ArgumentChecks.ensureNonEmpty("gridSize", gridSize, 1, Integer.MAX_VALUE, false);
         if (gridSize.length == 0) {
             this.gridSize = null;
             this.gridLength = 0;
@@ -250,36 +250,25 @@ 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);
-     * }
+     * The source positions are integer coordinates included in the given envelope.
+     * The target positions are the results of transforming source coordinates with
+     * the given {@code gridToCRS} transform.
      *
      * @param  gridToCRS  the transform from source coordinates (grid indices) to target coordinates.
-     * @param  gridSize   the upper values (exclusive) of source coordinates.
+     * @param  domain  domain of integer source coordinates for which to get a linear approximation.
      * @return a linear approximation of given transform for the specified domain.
      * @throws FactoryException if the transform approximation can not be computed.
      *
      * @see #setControlPoints(MathTransform)
+     * @see org.apache.sis.coverage.grid.GridGeometry#linearize(boolean)
      *
      * @since 1.1
      */
-    public static LinearTransform approximate(final MathTransform gridToCRS, final int... gridSize) throws FactoryException {
+    public static LinearTransform approximate(final MathTransform gridToCRS, final Envelope domain) throws FactoryException {
         ArgumentChecks.ensureNonNull("gridToCRS", gridToCRS);
+        ArgumentChecks.ensureNonNull("domain",    domain);
         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);
+            return (LinearTransform) Linearizer.approximate(gridToCRS, domain);
         } catch (TransformException e) {
             throw new FactoryException(e);
         }
@@ -579,7 +568,7 @@ search: for (int j=numPoints; --j >= 0;) {
      * @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...)
+     * @see #approximate(MathTransform, Envelope)
      *
      * @since 1.1
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/Linearizer.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/Linearizer.java
new file mode 100644
index 0000000..2379791
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/Linearizer.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.operation.builder;
+
+import java.util.Iterator;
+import org.opengis.geometry.Envelope;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.referencing.datum.DatumShiftGrid;
+import org.apache.sis.referencing.operation.transform.InterpolatedTransform;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * Computes a linear approximation of a {@link MathTransform}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class Linearizer {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private Linearizer() {
+    }
+
+    /**
+     * Returns a linear approximation of the given transform for the specified domain.
+     * The source positions are integer coordinates included in the given envelope.
+     * The target positions are the results of transforming source coordinates with
+     * the given {@code gridToCRS} transform.
+     *
+     * <p>If a linear approximation can be extracted from the given transform, this method returns
+     * that approximation directly. This method tries to avoid expensive calculation; it searches
+     * for transforms than can be processed easily.</p>
+     *
+     * @param  gridToCRS  the transform from source coordinates (grid indices) to target coordinates.
+     * @param  domain  domain of integer source coordinates for which to get a linear approximation.
+     * @return a linear approximation of given transform for the specified domain.
+     * @throws FactoryException if the transform approximation can not be computed.
+     */
+    static MathTransform approximate(final MathTransform gridToCRS, final Envelope domain)
+            throws TransformException, FactoryException
+    {
+        if (domain.getDimension() != ResidualGrid.SOURCE_DIMENSION) {
+            return compute(gridToCRS, domain, null);
+        }
+        MathTransform result = null;
+        final Iterator<MathTransform> it = MathTransforms.getSteps(gridToCRS).iterator();
+        while (it.hasNext()) {
+            MathTransform step = it.next();
+            if (step instanceof LinearTransform) {
+                result = (result != null) ? MathTransforms.concatenate(result, step) : step;
+            } else if (step instanceof InterpolatedTransform) {
+                /*
+                 * 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`.
+                 */
+                final DatumShiftGrid<?,?> grid = ((InterpolatedTransform) step).getShiftGrid();
+                MathTransform sourceToGrid = grid.getCoordinateToGrid();
+                if (result != null) {
+                    sourceToGrid = MathTransforms.concatenate(result, sourceToGrid);
+                }
+                final GeneralEnvelope gd = Envelopes.transform(sourceToGrid, domain);
+checkResidual:  if (grid instanceof ResidualGrid) {
+                    /*
+                     * Verifiy pixel coordinates as if they were rounded to nearest integers and assuming that
+                     * the envelope maximal values are inclusive (this is why we subtract 1.5 instead of 0.5).
+                     * If the specified envelope is inside the `ResidualGrid` domain, then the approximation
+                     * should be computed using a subset of the points included in this grid.
+                     */
+                    for (int i=0; i<ResidualGrid.SOURCE_DIMENSION; i++) {
+                        if (gd.getMinimum(i) >= 0.5 || gd.getMaximum(i) <= grid.getGridSize(i) - 1.5) {
+                            break checkResidual;
+                        }
+                    }
+                    /*
+                     * The `step` transform matches criterion. Consider that we can approximate this step by the
+                     * identity transform. If we applied `LinearTransformBuilder` on this step, we should indeed
+                     * get a result close to identity transform because of the way that `LocalizationGridBuilder`
+                     * computes `ResidualGrid`: if we invoke `LinearTransformBuilder.setControlPoints(gridToCRS)`
+                     * we would reconstitute the same set of points than the one that `LocalizationGridBuilder`
+                     * used for computing a linear approximation before to compute the `ResidualGrid`.
+                     */
+                    continue;
+                }
+                /*
+                 * The given domain does not use all grid points, or the grid is not a type that we known to be
+                 * close to identity. We have to compute the approximation. But instead of computing it on all
+                 * points in source domain, we use only the point in the grid. It can be much less computation.
+                 */
+                while (it.hasNext()) {
+                    // Complete the transform will all remaining steps.
+                    step = MathTransforms.concatenate(step, it.next());
+                }
+                return MathTransforms.concatenate(sourceToGrid, compute(step, gd, grid));
+            } else {
+                /*
+                 * Non-linear transform of unknown type. There is no optimization we can do.
+                 */
+                return compute(gridToCRS, domain, null);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Computes a linear approximation of the given transform. This is an expensive fallback used only
+     * when we could not find an existing value by inspection of {@code gridToCRS} transform steps.
+     */
+    private static MathTransform compute(final MathTransform gridToCRS, final Envelope domain,
+            final DatumShiftGrid<?,?> grid) throws TransformException, FactoryException
+    {
+        final int[] size = new int[domain.getDimension()];
+        final double[] shift = new double[size.length];
+        for (int i=0; i<size.length; i++) {
+            double lower = Math.rint(domain.getMinimum(i));
+            double upper = Math.rint(domain.getMaximum(i));
+            if (grid != null) {
+                final double h = grid.getGridSize(i);
+                if (upper > h) upper = h;
+                if (lower < 0) lower = 0;
+            }
+            final double span = upper - lower;
+            if (!(span <= Integer.MAX_VALUE)) {                               // Use `!` for catching NaN.
+                throw new ArithmeticException(Errors.format(Errors.Keys.IntegerOverflow_1, Integer.SIZE));
+            }
+            shift[i] = lower;
+            size [i] = (int) span;
+        }
+        final LinearTransformBuilder builder = new LinearTransformBuilder(size);
+        final LinearTransform translate = MathTransforms.translation(shift);
+        builder.setControlPoints(MathTransforms.concatenate(translate, gridToCRS));
+        return MathTransforms.concatenate(translate.inverse(), builder.create(null));
+    }
+}
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 bce70ef..b74069b 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,19 +22,14 @@ 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;
@@ -42,7 +37,6 @@ 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;
 
 
 /**
@@ -395,57 +389,6 @@ 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/test/java/org/apache/sis/referencing/operation/builder/LinearizerTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LinearizerTest.java
new file mode 100644
index 0000000..d3db170
--- /dev/null
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LinearizerTest.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.operation.builder;
+
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.matrix.Matrix3;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.referencing.operation.HardCodedConversions;
+import org.apache.sis.geometry.Envelope2D;
+import org.apache.sis.test.DependsOn;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link Linearizer}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+@DependsOn(LocalizationGridBuilderTest.class)
+public final strictfp class LinearizerTest {
+    /**
+     * Tests {@link LinearTransformBuilder#approximate(MathTransform, Envelope)} on a transform created by
+     * {@link LocalizationGridBuilder}. We verify that the {@link Linearizer#approximate(MathTransform,
+     * Envelope)} 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 LinearTransformBuilder points = new LinearTransformBuilder(3, 5);
+        points.setControlPoints(HardCodedConversions.mercator().getConversionFromBase().getMathTransform());
+
+        // Non-linear transform producing the same values than the set of points;
+        final LocalizationGridBuilder builder = new LocalizationGridBuilder(points);
+        final MathTransform transform = builder.create(null);
+        assertFalse(transform instanceof LinearTransform);
+
+        // Linear approximation by Least Square Root method.
+        final LinearTransform linear = LinearTransformBuilder.approximate(transform, new Envelope2D(null, 0, 0, 3, 5));
+        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-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 0d10365..803cefa 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,13 +19,9 @@ 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;
 
@@ -36,7 +32,7 @@ import static org.apache.sis.test.ReferencingAssert.*;
  * Tests {@link LocalizationGridBuilder}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -150,36 +146,4 @@ 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-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
index cd66eb8..d3165e7 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
@@ -264,6 +264,7 @@ import org.junit.BeforeClass;
     org.apache.sis.referencing.operation.builder.ResidualGridTest.class,
     org.apache.sis.referencing.operation.builder.LinearTransformBuilderTest.class,
     org.apache.sis.referencing.operation.builder.LocalizationGridBuilderTest.class,
+    org.apache.sis.referencing.operation.builder.LinearizerTest.class,
 
     // GeoAPI conformance test suite.
     org.apache.sis.referencing.geoapi.AuthorityFactoryTest.class,