sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Make possible to apply linearizers on localization grid and have the final coordinates in the "linearized" CRS, without converting them back to the original CRS. The use case is the application of Universal Transverse Mercator (UTM) projection on GCOM rasters for making their "grid to CRS" closer to affine transforms. In this case, we want to keep UTM coordinates in the final result, not reconverting them back to geographic coordinates.
Date Tue, 16 Feb 2021 23:40:55 GMT
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 33701c6  Make possible to apply linearizers on localization grid and have the final coordinates in the "linearized" CRS, without converting them back to the original CRS. The use case is the application of Universal Transverse Mercator (UTM) projection on GCOM rasters for making their "grid to CRS" closer to affine transforms. In this case, we want to keep UTM coordinates in the final result, not reconverting them back to geographic coordinates.
33701c6 is described below

commit 33701c6bcacaadb984ad7f3b1d8629fc5b7ef8dd
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Feb 17 00:34:37 2021 +0100

    Make possible to apply linearizers on localization grid and have the final coordinates in the "linearized" CRS, without converting them back to the original CRS.
    The use case is the application of Universal Transverse Mercator (UTM) projection on GCOM rasters for making their "grid to CRS" closer to affine transforms.
    In this case, we want to keep UTM coordinates in the final result, not reconverting them back to geographic coordinates.
---
 .../operation/builder/LinearTransformBuilder.java  | 261 ++++++++++++++-------
 .../operation/builder/LocalizationGridBuilder.java | 115 +++++++--
 .../operation/builder/ProjectedTransformTry.java   | 169 ++++++++++---
 .../operation/builder/ResidualGrid.java            |   6 +-
 .../operation/transform/InterpolatedTransform.java |  28 ++-
 .../builder/LinearTransformBuilderTest.java        |   7 +-
 .../operation/builder/ResidualGridTest.java        |   2 +-
 .../transform/InterpolatedTransformTest.java       |   2 +-
 .../apache/sis/internal/earth/netcdf/GCOM_C.java   |  13 +-
 .../apache/sis/internal/earth/netcdf/GCOM_W.java   |  24 +-
 .../java/org/apache/sis/internal/netcdf/Axis.java  |  17 +-
 .../org/apache/sis/internal/netcdf/CRSBuilder.java |  25 +-
 .../org/apache/sis/internal/netcdf/Convention.java |   8 +
 .../org/apache/sis/internal/netcdf/Decoder.java    |  17 +-
 .../java/org/apache/sis/internal/netcdf/Grid.java  |  63 +++--
 .../apache/sis/internal/netcdf/GridCacheKey.java   |  29 ++-
 .../apache/sis/internal/netcdf/GridCacheValue.java |  77 ++++++
 .../org/apache/sis/internal/netcdf/Linearizer.java | 258 +++++++++++++-------
 .../org/apache/sis/internal/netcdf/Resources.java  |   5 +
 .../sis/internal/netcdf/Resources.properties       |   1 +
 .../sis/internal/netcdf/Resources_fr.properties    |   1 +
 .../sis/internal/netcdf/SatelliteGroundTrack.java  | 246 -------------------
 .../internal/netcdf/SatelliteGroundTrackTest.java  |  81 -------
 .../org/apache/sis/test/suite/NetcdfTestSuite.java |   1 -
 24 files changed, 828 insertions(+), 628 deletions(-)

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 d5849ab..1f8a882 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
@@ -22,7 +22,6 @@ import java.util.Queue;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.ArrayDeque;
-import java.util.Collections;
 import java.util.NoSuchElementException;
 import java.util.Optional;
 import java.util.Locale;
@@ -54,6 +53,7 @@ import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix;
 import org.apache.sis.internal.referencing.DirectPositionView;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.internal.util.AbstractMap;
+import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
@@ -88,7 +88,7 @@ import org.apache.sis.util.Classes;
  * That selected projection is given by {@link #linearizer()}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  *
  * @see LocalizationGridBuilder
  * @see LinearTransform
@@ -185,7 +185,7 @@ public class LinearTransformBuilder extends TransformBuilder {
      * Calculated fields, namely {@link #correlations} and {@link #transform}, are left uninitialized.
      * Arrays are copied by references and their content shall not be modified. The new builder should
      * not be made accessible to users since changes in this builder would be reflected in the source
-     * values or original builder. This constructor is reserved to {@link #create(MathTransformFactory)}
+     * values of original builder. This constructor is reserved to {@link #create(MathTransformFactory)}
      * internal usage.
      *
      * @param original  the builder from which to take array references of source values.
@@ -486,8 +486,8 @@ search: for (int j=numPoints; --j >= 0;) {
     /**
      * Returns the envelope of target points (<em>values</em> of the map returned by {@link #getControlPoints()}).
      * The number of dimensions is equal to {@link #getTargetDimensions()}. The lower and upper values are inclusive.
-     * If a {@linkplain #linearizer() linearizer has been applied}, then coordinates of
-     * the returned envelope are projected by that linearizer.
+     * If a {@linkplain #linearizer() linearizer has been applied}, then coordinates of the returned envelope
+     * are projected by that linearizer.
      *
      * @return the envelope of target points.
      * @throws IllegalStateException if the target points are not yet known.
@@ -619,7 +619,7 @@ search: for (int j=numPoints; --j >= 0;) {
             }
             /*
              * If the point contains some NaN or infinite coordinate values, it is okay to leave it as-is
-             * (without incrementing 'numPoints') provided that we ensure that at least one value is NaN.
+             * (without incrementing `numPoints`) provided that we ensure that at least one value is NaN.
              * For convenience, we set only the first coordinate to NaN. The ControlPoints map will check
              * for the first coordinate too, so we need to keep this policy consistent.
              */
@@ -700,7 +700,7 @@ search: for (int j=numPoints; --j >= 0;) {
             if (data != null && coord.length == data.length) {
 search:         for (int j=domain(); --j >= 0;) {
                     for (int i=0; i<coord.length; i++) {
-                        if (coord[i] != data[i][j]) {           // Intentionally want 'false' for NaN values.
+                        if (coord[i] != data[i][j]) {           // Intentionally want `false` for NaN values.
                             continue search;
                         }
                     }
@@ -1103,8 +1103,8 @@ search:         for (int j=domain(); --j >= 0;) {
         double minAfter = Double.POSITIVE_INFINITY;
         double maxAfter = Double.NEGATIVE_INFINITY;
         double previous = coordinates[0];
-        for (int x=0; x<stride; x++) {                          // For iterating over dimensions lower than 'dimension'.
-            for (int y=0; y<gridLength; y += page) {            // For iterating over dimensions greater than 'dimension'.
+        for (int x=0; x<stride; x++) {                          // For iterating over dimensions lower than `dimension`.
+            for (int y=0; y<gridLength; y += page) {            // For iterating over dimensions greater than `dimension`.
                 final int stop = y + page;
                 for (int i = x+y; i<stop; i += stride) {
                     double value = coordinates[i];
@@ -1188,50 +1188,92 @@ search:         for (int j=domain(); --j >= 0;) {
     }
 
     /**
-     * Adds transforms to potentially apply on target coordinates before to compute the linear transform.
-     * This method can be invoked if one suspects that the <cite>source to target</cite> transform may be
-     * more linear when the target is another space than the current space of {@linkplain #getTargetEnvelope()
-     * target coordinates}. If linearizers have been specified, then the {@link #create(MathTransformFactory)}
-     * method will try to apply each transform on target coordinates and check which one results in the best
-     * {@linkplain #correlation() correlation} coefficients. It may be none.
-     *
-     * <p>The linearizers are specified as {@link MathTransform}s from current {@linkplain #getTargetEnvelope()
-     * target coordinates} to other spaces where <cite>sources to new targets</cite> transforms may be more linear.
-     * Keys in the map are arbitrary identifiers used in {@link #toString()} for debugging purpose.
-     * Values in the map are non-{@link LinearTransform}s (linear transforms are not forbidden, but are useless for this process).</p>
-     *
-     * <p>The {@code projToGrid} argument maps {@code projections} dimensions to this builder target dimensions.
-     * For example if {@code projToGrid} array is {@code {2,1}}, then dimensions 0 and 1 of given {@code projections}
-     * (both source and target dimensions) will map to dimensions 2 and 1 of this builder target dimensions, respectively.
-     * The {@code projToGrid} argument can be omitted or null, in which {0, 1, 2 … {@link #getTargetDimensions()} - 1} is assumed.
-     * All given {@code projections} shall have a number of source and target dimensions equals to the length of the given or assumed
-     * {@code projToGrid} array. It is possible to invoke this method many times with different {@code projToGrid} argument values.</p>
+     * Adds transforms to potentially apply on target control points before to compute the linear transform.
+     * This method can be invoked when the <cite>source to target</cite> transform would possibly be more
+     * linear if <cite>target</cite> was another space than the {@linkplain #getTargetEnvelope() current one}.
+     * If linearizers have been specified, then the {@link #create(MathTransformFactory)} method will try to
+     * apply each transform on target coordinates and check which one get the best
+     * {@linkplain #correlation() correlation} coefficients.
+     *
+     * <p>Exactly one of the specified transforms will be selected. If applying no transform is an acceptable solution,
+     * then an {@linkplain org.apache.sis.referencing.operation.transform.MathTransforms#identity(int) identity transform}
+     * should be included in the given {@code projections} map. The transform selected by {@code LinearTransformBuilder}
+     * will be given by {@link #linearizer()}.</p>
+     *
+     * <p>Linearizers are specified as a collection of {@link MathTransform}s from current {@linkplain #getTargetEnvelope()
+     * target coordinates} to some other spaces where <cite>sources to new targets</cite> transforms may be more linear.
+     * Keys in the map are arbitrary identifiers.
+     * Values in the map should be non-linear transforms; {@link LinearTransform}s (other than identity)
+     * should be avoided because they will consume processing power for no correlation improvement.</p>
+     *
+     * <h4>Error handling</h4>
+     * If a {@link org.opengis.referencing.operation.TransformException} occurred or if some transform results
+     * were NaN or infinite, then the {@link MathTransform} that failed will be ignored. If all transforms fail,
+     * then a {@link FactoryException} will be thrown by the {@code create(…)} method.
+     *
+     * <h4>Dimensions mapping</h4>
+     * The {@code projToGrid} argument maps {@code projections} dimensions to target dimensions of this builder.
+     * For example if {@code projToGrid} array is {@code {2,1}}, then coordinate values in target dimensions 2 and 1
+     * of this grid will be used as source coordinates in dimensions 0 and 1 respectively for all given projections.
+     * Likewise, the projection results in dimensions 0 and 1 of all projections will be stored in target dimensions
+     * 2 and 1 respectively of this grid.
+     *
+     * <p>The {@code projToGrid} argument can be omitted or null, in which case {0, 1, 2 …
+     * {@link #getTargetDimensions()} - 1} is assumed. All given {@code projections} shall have
+     * a number of source and target dimensions equals to the length of the {@code projToGrid} array.
+     * It is possible to invoke this method many times with different {@code projToGrid} argument values.</p>
      *
      * @param  projections  projections from current target coordinates to other spaces which may result in more linear transforms.
-     * @param  projToGrid   the target dimensions to project, or null or omitted for projecting all target dimensions.
+     * @param  projToGrid   the target dimensions to project, or null or omitted for projecting all target dimensions in same order.
      * @throws IllegalStateException if {@link #create(MathTransformFactory) create(…)} has already been invoked.
+     * @throws MismatchedDimensionException if a projection does not have the expected number of dimensions.
      *
      * @see #linearizer()
      * @see #correlation()
      *
      * @since 1.0
      */
-    public void addLinearizers(final Map<String,MathTransform> projections, int... projToGrid) {
+    public void addLinearizers(final Map<String,MathTransform> projections, final int... projToGrid) {
+        ArgumentChecks.ensureNonNull("projections", projections);
+        addLinearizers(projections, false, projToGrid);
+    }
+
+    /**
+     * Implementation of {@link #addLinearizers(Map, int...)} with a flag telling whether the inverse of selected projection
+     * shall be concatenated to the final transform. This method is non-public because the {@code reverseAfterLinearization}
+     * flag has no purpose for this {@link LinearTransformBuilder} class; it is useful only for {@link LocalizationGridBuilder}.
+     *
+     * @see ProjectedTransformTry#reverseAfterLinearization
+     */
+    final void addLinearizers(final Map<String,MathTransform> projections, final boolean compensate, int[] projToGrid) {
         ensureModifiable();
         final int tgtDim = getTargetDimensions();
         if (projToGrid == null || projToGrid.length == 0) {
             projToGrid = ArraysExt.range(0, tgtDim);
+        } else {
+            long defined = 0;
+            projToGrid = projToGrid.clone();
+            for (final int d : projToGrid) {
+                ArgumentChecks.ensureValidIndex(tgtDim, d);
+                if (defined == (defined |= Numerics.bitmask(d))) {
+                    // Note: if d ≥ 64, there will be no check (mask = 0).
+                    throw new IllegalArgumentException(Errors.format(Errors.Keys.DuplicatedNumber_1, d));
+                }
+            }
         }
         if (linearizers == null) {
-            linearizers = new ArrayList<>();
+            linearizers = new ArrayList<>(projections.size());
         }
         for (final Map.Entry<String,MathTransform> entry : projections.entrySet()) {
-            linearizers.add(new ProjectedTransformTry(entry.getKey(), entry.getValue(), projToGrid, tgtDim));
+            linearizers.add(new ProjectedTransformTry(entry.getKey(), entry.getValue(), projToGrid, tgtDim, compensate));
         }
     }
 
     /**
      * Sets the linearizers to a copy of those of the given builder.
+     * This is used by copy constructors.
+     *
+     * @see LocalizationGridBuilder#LocalizationGridBuilder(LinearTransformBuilder)
      */
     final void setLinearizers(final LinearTransformBuilder other) {
         if (other.linearizers != null) {
@@ -1242,8 +1284,8 @@ search:         for (int j=domain(); --j >= 0;) {
 
     /**
      * Creates a linear transform approximation from the source positions to the target positions.
-     * This method assumes that source positions are precise and that all uncertainty is in the target positions.
-     * If {@linkplain #addLinearizers linearizers have been specified}, then this method may project all target
+     * This method assumes that source positions are precise and that all uncertainties are in target positions.
+     * If {@linkplain #addLinearizers linearizers have been specified}, then this method will project all target
      * coordinates using one of those linearizers in order to get a more linear transform.
      * If such projection is applied, then {@link #linearizer()} will return a non-empty value after this method call.
      *
@@ -1261,61 +1303,102 @@ search:         for (int j=domain(); --j >= 0;) {
     @Override
     public LinearTransform create(final MathTransformFactory factory) throws FactoryException {
         if (transform == null) {
-            MatrixSIS matrix = fit();
-            if (linearizers != null) {
+            MatrixSIS bestTransform;
+            if (linearizers == null || linearizers.isEmpty()) {
+                bestTransform = fit();
+            } else {
                 /*
-                 * We are going to try to project target coordinates in an attempt to find a more linear transform.
-                 * If a projection allows better results than unprojected coordinates, the following variables will
-                 * be set to values to assign to this 'LinearTransformBuilder' after the loop. We do not assign new
-                 * values to this 'LinearTransformBuilder' directly (as we find them) in the loop because the checks
-                 * for a better transform require the original values.
+                 * We are going to try to project target coordinates in search for the most linear transform.
+                 * If a projection allows better results, the following variables will be set to values to assign
+                 * to this `LinearTransformBuilder` after the loop. We do not assign new values to this builder
+                 * directly (as we find them) in the loop because the search for a better transform requires the
+                 * original values.
                  */
-                final double sqrtLength      = Math.sqrt(correlations.length);
-                double     bestCorrelation   = rms(correlations, sqrtLength);
+                           bestTransform     = null;
+                double     bestCorrelation   = 0;
                 double[]   bestCorrelations  = null;
-                MatrixSIS  bestTransform     = null;
                 double[][] transformedArrays = null;
+                final double sqrtCorrLength  = Math.sqrt(targets.length);   // For `bestCorrelation` calculation.
                 /*
-                 * Store the correlation when using no conversions, only for this.toString() purpose. We copy
-                 * 'ProjectedTransformTry' list in an array both for excluding the dummy entry, and also for
-                 * avoiding ConcurrentModificationException if a debugger invokes toString() during the loop.
+                 * If one of the transforms is identity, we can do the computation directly on `this` because the
+                 * `targets` arrays do not need to be transformed. This special case avoids the need to allocate
+                 * arrays from the `pool` and to copy data.
                  */
-                final ProjectedTransformTry[] alternatives = linearizers.toArray(new ProjectedTransformTry[linearizers.size()]);
-                linearizers.add(new ProjectedTransformTry((float) bestCorrelation));
+                ProjectedTransformTry identity = null;
+                for (final ProjectedTransformTry alt : linearizers) {
+                    if (alt.projection.isIdentity()) {
+                        bestTransform     = fit();
+                        bestCorrelations  = correlations;
+                        bestCorrelation   = rms(bestCorrelations, sqrtCorrLength);
+                        transformedArrays = targets;
+                        appliedLinearizer = alt;
+                        identity          = alt;
+                        alt.correlation   = (float) bestCorrelation;
+                        break;
+                    }
+                }
                 /*
-                 * 'tmp' and 'pool' are temporary objects for this computation only. We use a pool because the
-                 * 'double[]' arrays may be large (e.g. megabytes) and we want to avoid creating new arrays of
+                 * `tmp` and `pool` are temporary objects for this computation only. We use a pool because the
+                 * `double[]` arrays may be large (e.g. megabytes) and we want to avoid creating new arrays of
                  * such size for each projection to try.
                  */
                 final Queue<double[]> pool = new ArrayDeque<>();
-                final int n = (gridLength != 0) ? gridLength : numPoints;
                 final LinearTransformBuilder tmp = new LinearTransformBuilder(this);
-                for (final ProjectedTransformTry alt : alternatives) {
-                    if ((tmp.targets = alt.transform(targets, n, pool)) != null) {
+                final int numPoints = (gridLength != 0) ? gridLength : this.numPoints;
+                boolean needTargetReplace = false;
+                for (final ProjectedTransformTry alt : linearizers) {
+                    if (alt == identity || (tmp.targets = alt.transform(targets, numPoints, pool)) == null) {
+                        continue;
+                    }
+                    /*
+                     * At this point, a transformation has been successfully applied on the target arrays of `tmp`.
+                     * If we never invoked `fit()` before, its first call must be done with all dimensions in `tmp`,
+                     * not only the dimensions on which we apply the `MathTransform`.
+                     */
+                    if (bestTransform == null) {
+                        transformedArrays = tmp.targets = alt.replaceTransformed(targets, tmp.targets);
+                        bestTransform     = tmp.fit();
+                        bestCorrelations  = tmp.correlations;
+                        bestCorrelation   = rms(bestCorrelations, sqrtCorrLength);
+                        alt.correlation   = (float) bestCorrelation;
+                        appliedLinearizer = alt;
+                    } else {
+                        /*
+                         * For all invocations of `fit()` after the first one (including the identity case if any),
+                         * we need to do calculation only on the dimensions on which `MathTransform` operates because
+                         * calculation on other dimensions will be unchanged.
+                         */
                         final MatrixSIS altTransform    = tmp.fit();
-                        final double[]  altCorrelations = alt.replace(correlations, tmp.correlations);
-                        final double    altCorrelation  = rms(altCorrelations, sqrtLength);
+                        final double[]  altCorrelations = alt.replaceTransformed(bestCorrelations, tmp.correlations);
+                        final double    altCorrelation  = rms(altCorrelations, sqrtCorrLength);
                         alt.correlation = (float) altCorrelation;
                         if (altCorrelation > bestCorrelation) {
                             ProjectedTransformTry.recycle(transformedArrays, pool);
                             transformedArrays = tmp.targets;
                             bestCorrelation   = altCorrelation;
                             bestCorrelations  = altCorrelations;
-                            bestTransform     = alt.replace(matrix, altTransform);
+                            bestTransform     = alt.replaceTransformed(bestTransform, altTransform);
                             appliedLinearizer = alt;
+                            needTargetReplace = true;
                         } else {
                             ProjectedTransformTry.recycle(tmp.targets, pool);
                         }
                     }
                 }
-                if (bestTransform != null) {
-                    matrix       = bestTransform;
-                    targets      = transformedArrays;
-                    correlations = bestCorrelations;
+                /*
+                 * Finished to try all transforms. If all of them failed, wrap the `TransformException`.
+                 */
+                if (bestTransform == null) {
+                    throw new FactoryException(ProjectedTransformTry.getError(linearizers));
+                }
+                if (needTargetReplace) {
+                    transformedArrays = appliedLinearizer.replaceTransformed(targets, transformedArrays);
                 }
+                targets      = transformedArrays;
+                correlations = bestCorrelations;
             }
             // Set only on success.
-            transform = (LinearTransform) nonNull(factory).createAffineTransform(matrix);
+            transform = (LinearTransform) nonNull(factory).createAffineTransform(bestTransform);
         }
         return transform;
     }
@@ -1404,39 +1487,48 @@ search:         for (int j=domain(); --j >= 0;) {
     /**
      * Returns a global estimation of correlation by computing the root mean square of values.
      */
-    private static double rms(final double[] correlations, final double sqrtLength) {
-        return org.apache.sis.math.MathFunctions.magnitude(correlations) / sqrtLength;
+    private static double rms(final double[] correlations, final double sqrtCorrLength) {
+        return org.apache.sis.math.MathFunctions.magnitude(correlations) / sqrtCorrLength;
     }
 
     /**
      * If target coordinates have been projected to another space, returns that projection.
-     * This method returns a non-empty value only if all the following conditions are met:
-     *
-     * <ol>
-     *   <li>{@link #addLinearizers(Map, int...)} has been invoked.</li>
-     *   <li>{@link #create(MathTransformFactory)} has been invoked.</li>
-     *   <li>The {@code create(…)} method at step 2 found that projecting target coordinates using
-     *       one of the linearizers specified at step 1 results in a more linear transform.</li>
-     * </ol>
-     *
-     * If this method returns a non-empty value, then the envelope returned by {@link #getTargetEnvelope()}
-     * and all control points returned by {@link #getControlPoint(int[])} are projected by this transform.
-     * The returned transform includes axes swapping specified by the {@code dimensions} argument given to
-     * <code>{@linkplain #addLinearizers(Map, int...) addLinearizers}(…, dimensions)</code>.
+     * This method returns a non-empty value if {@link #addLinearizers(Map, int...)} has been
+     * invoked with a non-empty map, followed by a {@link #create(MathTransformFactory)} call.
+     * In such case, {@code LinearTransformBuilder} selects a linearizer identified by the returned
+     * <var>key</var> - <var>value</var> entry. The entry key is one of the keys of the maps given
+     * to {@code addLinearizers(…)}. The entry value is the associated {@code MathTransform},
+     * possibly modified as described in the <cite>axis order</cite> section below.
+     *
+     * <p>The envelope returned by {@link #getTargetEnvelope()} and all control points
+     * returned by {@link #getControlPoint(int[])} are projected by the selected transform.
+     * Consequently if the target coordinates of original control points are desired,
+     * then the transform returned by {@code create(…)} needs to be concatenated with
+     * the {@linkplain MathTransform#inverse() inverse} of the transform returned by
+     * this {@code linearizer()} method.</p>
+     *
+     * <h4>Axis order</h4>
+     * The source coordinates expected by the returned transform are the {@linkplain #getControlPoint(int[])
+     * control points target coordinates}. The returned transform will contain an operation step performing
+     * axis filtering and swapping implied by the {@code projToGrid} argument that was given to the
+     * <code>{@linkplain #addLinearizers(Map, int...) addLinearizers}(…, projToGrid)}</code> method.
+     * Consequently if the {@code projToGrid} argument was not an arithmetic progression,
+     * then the transform returned by this method will not be one of the instances given to
+     * {@code addLinearizers(…)}.
      *
      * @return the projection applied on target coordinates before to compute a linear transform.
      *
-     * @since 1.0
+     * @since 1.1
      */
-    public Optional<MathTransform> linearizer() {
-        return (appliedLinearizer != null) ? Optional.of(appliedLinearizer.projection()) : Optional.empty();
+    public Optional<Map.Entry<String,MathTransform>> linearizer() {
+        return Optional.ofNullable(appliedLinearizer);
     }
 
     /**
-     * Returns the identifier of the linearizer, or {@code null} if none.
+     * Returns linearizer which has been applied, or {@code null} if none.
      */
-    final String linearizerID() {
-        return (appliedLinearizer != null) ? appliedLinearizer.name() : null;
+    final ProjectedTransformTry appliedLinearizer() {
+        return appliedLinearizer;
     }
 
     /**
@@ -1513,17 +1605,18 @@ search:         for (int j=domain(); --j >= 0;) {
          * └────────────┴─────────────┘
          */
         if (linearizers != null) {
+            final ProjectedTransformTry[] alternatives = linearizers.toArray(new ProjectedTransformTry[linearizers.size()]);
+            Arrays.sort(alternatives);
             buffer.append(Strings.CONTINUATION_ITEM);
             vocabulary.appendLabel(Vocabulary.Keys.Preprocessing, buffer);
             buffer.append(lineSeparator);
-            Collections.sort(linearizers);
             NumberFormat nf = null;
             final TableAppender table = new TableAppender(buffer, " │ ");
             table.appendHorizontalSeparator();
             table.append(vocabulary.getString(Vocabulary.Keys.Conversion)).nextColumn();
             table.append(vocabulary.getString(Vocabulary.Keys.Correlation)).nextLine();
             table.appendHorizontalSeparator();
-            for (final ProjectedTransformTry alt : linearizers) {
+            for (final ProjectedTransformTry alt : alternatives) {
                 nf = alt.summarize(table, nf, locale);
             }
             table.appendHorizontalSeparator();
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
index 41e228d..fca7409 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
@@ -206,6 +206,9 @@ public class LocalizationGridBuilder extends TransformBuilder {
      *     after construction will not be reflected in this new builder.</li>
      * </ul>
      *
+     * The new builder inherits the {@linkplain LinearTransformBuilder#addLinearizers linearizers}
+     * of {@code localizations}.
+     *
      * @param  localizations  the provider of control points for which to create a localization grid.
      * @throws ArithmeticException if this constructor can not infer a reasonable grid size from the given localizations.
      *
@@ -267,7 +270,7 @@ public class LocalizationGridBuilder extends TransformBuilder {
                 double v = source.doubleValue(i) - min;
                 if (Math.abs(v % inc) > EPS) {
                     do {
-                        final double r = (inc % v);     // Both 'inc' and 'v' are positive, so 'r' will be positive too.
+                        final double r = (inc % v);     // Both `inc` and `v` are positive, so `r` will be positive too.
                         inc = v;
                         v = r;
                     } while (Math.abs(v) > EPS);
@@ -283,7 +286,7 @@ public class LocalizationGridBuilder extends TransformBuilder {
         fromGrid.setElement(dim, dim, inc);
         fromGrid.setElement(dim, SOURCE_DIMENSION, min);
         final double n = span / inc;
-        if (n >= 0.5 && n < source.size() - 0.5) {          // Compare as 'double' in case the value is large.
+        if (n >= 0.5 && n < source.size() - 0.5) {          // Compare as `double` in case the value is large.
             return ((int) Math.round(n)) + 1;
         }
         throw new ArithmeticException(Resources.format(Resources.Keys.CanNotInferGridSizeFromValues_1, range));
@@ -582,33 +585,59 @@ public class LocalizationGridBuilder extends TransformBuilder {
     }
 
     /**
-     * Adds transforms to potentially apply on target coordinates before to compute the transform.
+     * Adds transforms to potentially apply on target control points before to compute the transform.
      * This method can be invoked if the departure from a linear transform is too large, resulting
      * in {@link InterpolatedTransform} to fail with "no convergence error" messages.
      * If linearizers have been specified, then the {@link #create(MathTransformFactory)} method
      * will try to apply each transform on target coordinates and check which one results in the
-     * best correlation coefficients. It may be none.
-     *
-     * <p>The linearizers are specified as {@link MathTransform}s from current target coordinates
-     * to other spaces where <cite>sources to new targets</cite> transforms may be more linear.
-     * The keys in the map are arbitrary identifiers used in {@link #toString()} for debugging purpose.
-     * The {@code dimensions} argument specifies which target dimensions to project and can be null or omitted
-     * if the projections shall be applied on all target coordinates. It is possible to invoke this method many
-     * times with different {@code dimensions} argument values.</p>
+     * best correlation coefficients. Exactly one of the specified transforms will be selected.
+     * If applying no transform is an acceptable solution, then an
+     * {@linkplain org.apache.sis.referencing.operation.transform.MathTransforms#identity(int)
+     * identity transform} should be included in the given {@code projections} map.
+     *
+     * <p>The linearizers are specified as {@link MathTransform}s from current {@linkplain #getControlPoint(int, int)
+     * target coordinates of control points} to other spaces where <cite>sources to new targets</cite> transforms may
+     * be more linear. The keys in the map are arbitrary identifiers.
+     * The {@code projToGrid} argument specifies which control point dimensions to use as {@code projections} source
+     * coordinates and can be null or omitted if the projections shall be applied on all target coordinates.
+     * It is possible to invoke this method many times with different {@code dimensions} argument values.</p>
+     *
+     * <p>The {@code compensate} argument tell whether the inverse of specified transform shall be concatenated
+     * to the final {@linkplain #create interpolated transform}. If {@code true}, the {@code projection} effect
+     * will be cancelled in the final result, i.e. the target coordinates will be approximately the same as if
+     * no projection were applied. In such case, the advantage of applying a projection is to improve numerical
+     * stability with a better linear approximation in used by the coordinate transformation process.</p>
      *
      * @param  projections  projections from current target coordinates to other spaces which may result in more linear transforms.
-     * @param  dimensions   the target dimensions to project, or null or omitted for projecting all target dimensions.
+     * @param  compensate   whether the inverse of selected projection shall be concatenated to the final interpolated transform.
+     * @param  projToGrid   the target dimensions to project, or null or omitted for projecting all target dimensions.
      *                      If non-null and non-empty, then all transforms in the {@code projections} map shall have a
      *                      number of source and target dimensions equals to the length of this array.
      * @throws IllegalStateException if {@link #create(MathTransformFactory) create(…)} has already been invoked.
      *
      * @see LinearTransformBuilder#addLinearizers(Map, int...)
      *
-     * @since 1.0
+     * @since 1.1
      */
-    public void addLinearizers(final Map<String,MathTransform> projections, int... dimensions) {
+    public void addLinearizers(final Map<String,MathTransform> projections, final boolean compensate, final int... projToGrid) {
+        ArgumentChecks.ensureNonNull("projections", projections);
         ensureModifiable();
-        linear.addLinearizers(projections, dimensions);
+        linear.addLinearizers(projections, compensate, projToGrid);
+    }
+
+    /**
+     * Adds transforms to potentially apply on target control points before to compute the transform.
+     *
+     * @param  projections  projections from current target coordinates to other spaces which may result in more linear transforms.
+     * @param  projToGrid   the target dimensions to project, or null or omitted for projecting all target dimensions.
+     *
+     * @deprecated Replaced by {@link #addLinearizers(Map, boolean, int...)} with {@code compensate = true}.
+     *
+     * @since 1.0
+     */
+    @Deprecated
+    public void addLinearizers(final Map<String,MathTransform> projections, final int... projToGrid) {
+        addLinearizers(projections, true, projToGrid);
     }
 
     /**
@@ -695,7 +724,8 @@ public class LocalizationGridBuilder extends TransformBuilder {
                     } else {
                         step = InterpolatedTransform.createGeodeticTransformation(nonNull(factory),
                                 new ResidualGrid(sourceToGrid, gridToCoord, width, height, residual,
-                                (gridPrecision > 0) ? gridPrecision : DEFAULT_PRECISION, periods));
+                                (gridPrecision > 0) ? gridPrecision : DEFAULT_PRECISION, periods,
+                                linear.appliedLinearizer()));
                     }
                 } catch (TransformException e) {
                     throw new FactoryException(e);                                          // Should never happen.
@@ -706,12 +736,12 @@ public class LocalizationGridBuilder extends TransformBuilder {
              * If those target coordinates have been modified in order to make that step more
              * linear, apply the inverse transformation after the step.
              */
-            final Optional<MathTransform> linearizer = linear.linearizer();
-            if (linearizer.isPresent()) try {
-                step = factory.createConcatenatedTransform(step, linearizer.get().inverse());
+            final ProjectedTransformTry linearizer = linear.appliedLinearizer();
+            if (linearizer != null && linearizer.reverseAfterLinearization) try {
+                step = factory.createConcatenatedTransform(step, linearizer.getValue().inverse());
             } catch (NoninvertibleTransformException e) {
                 throw new InvalidGeodeticParameterException(Resources.format(
-                        Resources.Keys.NonInvertibleOperation_1, linear.linearizerID()), e);
+                        Resources.Keys.NonInvertibleOperation_1, linearizer.getKey()), e);
             }
             transform = step;                               // Set only after everything succeeded.
         }
@@ -719,6 +749,45 @@ public class LocalizationGridBuilder extends TransformBuilder {
     }
 
     /**
+     * Returns the linearizer applied on target control points.
+     * This method returns a non-empty value if {@link #addLinearizers(Map, boolean, int...)} has
+     * been invoked with a non-empty map, followed by a {@link #create(MathTransformFactory)} call.
+     * In such case, {@link LinearTransformBuilder} selects a linearizer identified by the returned
+     * <var>key</var> - <var>value</var> entry. The entry key is one of the keys of the maps given
+     * to {@code addLinearizers(…)}. The entry value is the associated {@code MathTransform},
+     * possibly modified as described in the <cite>axis order</cite> section below.
+     *
+     * <p>All control points returned by {@link #getControlPoint(int, int)} are projected by the selected transform.
+     * Consequently if the target coordinates of original control points are desired, then the transform computed by
+     * this builder needs to be concatenated with the {@linkplain MathTransform#inverse() inverse} of the transform
+     * returned by this method. This is done automatically in the {@link #create(MathTransformFactory) create(…)}
+     * method if the {@code compensate} flag given to {@code addLinearizers(…)} method was {@code true}.
+     * Otherwise the compensation, if desired, needs to be done by the caller.</p>
+     *
+     * <h4>Axis order</h4>
+     * The returned transform will contain an operation step performing axis filtering and swapping implied by the
+     * {@code projToGrid} argument that was given to the <code>{@linkplain #addLinearizers(Map, boolean, int...)
+     * addLinearizers}(…, projToGrid)}</code> method. Consequently if the {@code projToGrid} argument was not an
+     * arithmetic progression, then the transform returned by this method will not be one of the instances given
+     * to {@code addLinearizers(…)}.
+     *
+     * @param  ifNotCompensated  whether to return the transform only if not already compensated by {@code create(…)}.
+     *         A value of {@code true} is useful if the caller wants the transform only if it needs to compensate itself.
+     * @return the projection applied on target coordinates before to compute a linear transform.
+     *
+     * @see LinearTransformBuilder#linearizer()
+     *
+     * @since 1.1
+     */
+    public Optional<Map.Entry<String,MathTransform>> linearizer(final boolean ifNotCompensated) {
+        ProjectedTransformTry linearizer = linear.appliedLinearizer();
+        if (ifNotCompensated && linearizer != null && linearizer.reverseAfterLinearization) {
+            linearizer = null;
+        }
+        return Optional.ofNullable(linearizer);
+    }
+
+    /**
      * Returns statistics of differences between values calculated by the given transform and actual values.
      * The given math transform is typically the transform computed by {@link #create(MathTransformFactory)},
      * but not necessarily. The returned statistics are:
@@ -765,9 +834,11 @@ public class LocalizationGridBuilder extends TransformBuilder {
         /*
          * If a linearizer has been applied, all target coordinates in this builder have been projected using
          * that transform. We will need to apply the inverse transform in order to get back the original values.
+         * The way that we get the transform below should be the same way than in `create(…)`, except that we
+         * apply the inverse transform unconditionally.
          */
-        final Optional<MathTransform> linearizer = linear.linearizer();
-        final MathTransform complete = linearizer.isPresent() ? linearizer.get().inverse() : null;
+        final ProjectedTransformTry linearizer = linear.appliedLinearizer();
+        final MathTransform complete = (linearizer != null) ? linearizer.getValue().inverse() : null;
         final MathTransform inverse = mt.inverse();
         final int width  = linear.gridSize(0);
         final int height = linear.gridSize(1);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ProjectedTransformTry.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ProjectedTransformTry.java
index ff43ea8..7d55c78 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ProjectedTransformTry.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ProjectedTransformTry.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.referencing.operation.builder;
 
+import java.util.Map;
+import java.util.List;
 import java.util.Queue;
 import java.util.Arrays;
 import java.util.Locale;
@@ -29,7 +31,6 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 
@@ -54,12 +55,15 @@ import org.apache.sis.referencing.operation.transform.MathTransforms;
  * it will resolve the "no convergence" errors.</p>
  * </div>
  *
+ * <p><b>Note:</b> {@link #compareTo(ProjectedTransformTry)} is inconsistent with {@link #equals(Object)}.
+ * The fact that {@link ProjectedTransformTry} instances are comparable should not be visible in public API.</p>
+ *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
-final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
+final class ProjectedTransformTry implements Comparable<ProjectedTransformTry>, Map.Entry<String,MathTransform> {
     /**
      * Number of points in the temporary buffer used for transforming data.
      * The buffer length will be this capacity multiplied by the number of dimensions.
@@ -69,70 +73,82 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
     private static final int BUFFER_CAPACITY = 512;
 
     /**
-     * A name by witch this projection attempt is identified, or {@code null} for the identity transform.
+     * A name by witch this projection attempt is identified.
+     *
+     * @see #getKey()
      */
-    private String name;
+    private final String name;
 
     /**
      * A conversion from a non-linear grid (typically with longitude and latitude values) to
      * something that may be more linear (typically, but not necessarily, a map projection).
      *
-     * @see #projection()
+     * @see #getValue()
      */
-    private final MathTransform projection;
+    final MathTransform projection;
 
     /**
      * Maps {@link #projection} dimensions to {@link LinearTransformBuilder} target dimensions.
      * For example if this array is {@code {2,1}}, then dimensions 0 and 1 of {@link #projection}
-     * (both source and target dimensions) will map dimensions 2 and 1 of {@link LinearTransformBuilder#targets}, respectively.
+     * (both source and target dimensions) will map dimensions 2 and 1 of {@link LinearTransformBuilder#targets}.
      * The length of this array shall be equal to the number of {@link #projection} source dimensions.
      */
     private final int[] projToGrid;
 
     /**
+     * Whether the inverse of {@link #projection} shall be concatenated to the
+     * final {@linkplain LocalizationGridBuilder#create interpolated transform}.
+     * If {@code true}, the {@code projection} effect will be cancelled in the final result,
+     * i.e. the target coordinates will be approximately the same as if no projection were applied.
+     * In such case, the advantage of applying a projection is to improve numerical stability with
+     * a better linear approximation in the first step of the coordinate transformation process.
+     */
+    final boolean reverseAfterLinearization;
+
+    /**
      * A global correlation factor, stored for information purpose only.
      */
     float correlation;
 
     /**
      * If an error occurred during coordinate operations, the error. Otherwise {@code null}.
+     *
+     * @see #getError(List)
      */
     private TransformException error;
 
     /**
      * Creates a new instance initialized to a copy of the given instance but without result.
+     * This is used by copy constructors.
+     *
+     * @see LocalizationGridBuilder#LocalizationGridBuilder(LinearTransformBuilder)
      */
     ProjectedTransformTry(final ProjectedTransformTry other) {
         name       = other.name;
         projection = other.projection;
         projToGrid = other.projToGrid;
-    }
-
-    /**
-     * Creates a new instance with only the given correlation coefficient. This instance can not be used for
-     * computation purpose. Its sole purpose is to hold the given coefficient when no projection is applied.
-     */
-    ProjectedTransformTry(final float corr) {
-        projection  = null;
-        projToGrid  = null;
-        correlation = corr;
+        reverseAfterLinearization = other.reverseAfterLinearization;
     }
 
     /**
      * Prepares a new attempt to project a localization grid.
      * All arguments are stored as-is (arrays are not cloned).
      *
-     * @param name               a name by witch this projection attempt is identified, or {@code null}.
+     * @param name               a name by witch this projection attempt is identified.
      * @param projection         conversion from non-linear grid to something that may be more linear.
      * @param projToGrid         maps {@code projection} dimensions to {@link LinearTransformBuilder} target dimensions.
      * @param expectedDimension  number of {@link LinearTransformBuilder} target dimensions.
+     * @throws MismatchedDimensionException if the projection does not have the expected number of dimensions.
      */
-    ProjectedTransformTry(final String name, final MathTransform projection, final int[] projToGrid, int expectedDimension) {
+    ProjectedTransformTry(final String name, final MathTransform projection, final int[] projToGrid, int expectedDimension,
+                          final boolean reverseAfterLinearization)
+    {
         ArgumentChecks.ensureNonNull("name", name);
         ArgumentChecks.ensureNonNull("projection", projection);
         this.name       = name;
         this.projection = projection;
         this.projToGrid = projToGrid;
+        this.reverseAfterLinearization = reverseAfterLinearization;
         int side = 0;                           // 0 = problem with source dimensions, 1 = problem with target dimensions.
         int actual = projection.getSourceDimensions();
         if (actual <= expectedDimension) {
@@ -150,22 +166,34 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
     }
 
     /**
-     * Returns the name of this object, or {@code null} if this is the identity transform created by
-     * {@link #ProjectedTransformTry(float)}. Should never be {@code null} for name returned to user.
+     * Returns the name by witch this projection attempt is identified.
      */
-    final String name() {
+    @Override
+    public String getKey() {
         return name;
     }
 
     /**
      * Returns the projection, taking in account axis swapping if {@link #projToGrid} is not an arithmetic progression.
+     *
+     * @todo We needs a pass-through transform if the number of grid dimensions
+     *       is greater than the number of projection dimensions.
      */
-    final MathTransform projection() {
+    @Override
+    public MathTransform getValue() {
         MathTransform mt = MathTransforms.linear(Matrices.createDimensionSelect(projToGrid.length, projToGrid));
         return MathTransforms.concatenate(mt, projection);
     }
 
     /**
+     * Do not allow modification of this entry.
+     */
+    @Override
+    public MathTransform setValue(MathTransform value) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
      * Transforms target coordinates of a localization grid. The {@code coordinates} argument is the value
      * of {@link LinearTransformBuilder#targets}, without clone (this method will only read those arrays).
      * Only arrays at indices given by {@link #projToGrid} will be read; the other arrays will be ignored.
@@ -178,6 +206,10 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
      *   <li>{@code results[d][i]} is a coordinate of the point at index <var>i</var>.</li>
      * </ol>
      *
+     * The returned target shall be given to {@link #replaceTransformed(double[][], double[][])} before
+     * final storage in {@link LinearTransformBuilder}.
+     *
+     * <h4>Pool of arrays</h4>
      * The {@code pool} queue is initially empty. Arrays created by this method and later discarded will be added to
      * that queue, for recycling if this method is invoked again for another {@code ProjectedTransformTry} instance.
      *
@@ -233,7 +265,7 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
                         dataOffset = start;
                         int dst = d;
                         do {
-                            if (Double.isNaN(data[dataOffset] = buffer[dst])) {
+                            if (!Double.isFinite(data[dataOffset] = buffer[dst])) {
                                 recycle(results, pool);         // Make arrays available for other transforms.
                                 return null;
                             }
@@ -260,15 +292,43 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
     }
 
     /**
-     * Replaces old correlation values by new values in a copy of the given array.
+     * Returns {@code true} if {@code projToGrid[i] == i} for all <var>i</var>.
+     */
+    private boolean useSameDimensions() {
+        return ArraysExt.isRange(0, projToGrid);
+    }
+
+    /**
+     * Replaces old target arrays by new values in the dimensions where a projection has been applied.
+     * The {@code targets} array is copied if necessary, then values are replaced in the copied array.
+     * May return {@code newValues} directly if suitable.
+     *
+     * @param  targets    the original targets values. This array will not be modified.
+     * @param  newValues  targets computed by {@link #transform transform(…)} for the dimensions specified at construction time.
+     * @return a copy of the given {@code targets} array with new values overwriting the old values.
+     */
+    final double[][] replaceTransformed(double[][] targets, final double[][] newValues) {
+        if (newValues.length == targets.length && useSameDimensions()) {
+            return newValues;
+        }
+        targets = targets.clone();
+        for (int j=0; j<projToGrid.length; j++) {
+            targets[projToGrid[j]] = newValues[j];
+        }
+        return targets;
+    }
+
+    /**
+     * Replaces old correlation values by new values in the dimensions where a projection has been applied.
+     * The {@code correlations} array is copied if necessary, then values are replaced in the copied array.
      * May return {@code newValues} directly if suitable.
      *
      * @param  correlations  the original correlation values. This array will not be modified.
      * @param  newValues     correlations computed by {@link LinearTransformBuilder} for the dimensions specified at construction time.
      * @return a copy of the given {@code correlation} array with new values overwriting the old values.
      */
-    final double[] replace(double[] correlations, final double[] newValues) {
-        if (newValues.length == correlations.length && ArraysExt.isRange(0, projToGrid)) {
+    final double[] replaceTransformed(double[] correlations, final double[] newValues) {
+        if (newValues.length == correlations.length && useSameDimensions()) {
             return newValues;
         }
         correlations = correlations.clone();
@@ -279,21 +339,22 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
     }
 
     /**
-     * Replaces old transform coefficients by new values in a copy of the given matrix.
+     * Replaces old transform coefficients by new values in the dimensions where a projection has been applied.
+     * The {@code transform} matrix is copied if necessary, then values are replaced in the copied matrix.
      * May return {@code newValues} directly if suitable.
      *
      * @param  transform  the original affine transform. This matrix will not be modified.
      * @param  newValues  coefficients computed by {@link LinearTransformBuilder} for the dimensions specified at construction time.
      * @return a copy of the given {@code transform} matrix with new coefficients overwriting the old values.
      */
-    final MatrixSIS replace(MatrixSIS transform, final MatrixSIS newValues) {
+    final MatrixSIS replaceTransformed(MatrixSIS transform, final MatrixSIS newValues) {
         /*
          * The two matrices shall have the same number of columns because they were computed with
          * LinearTransformBuilder instances having the same sources. However the two matrices may
          * have a different number of rows since the number of target dimensions may differ.
          */
         assert newValues.getNumCol() == transform.getNumCol();
-        if (newValues.getNumRow() == transform.getNumRow() && ArraysExt.isRange(0, projToGrid)) {
+        if (newValues.getNumRow() == transform.getNumRow() && useSameDimensions()) {
             return newValues;
         }
         transform = transform.clone();
@@ -307,8 +368,31 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
     }
 
     /**
+     * Returns the first error in the given list of linearizers. Errors after the first one are added
+     * as suppressed exception. If no error are found, this method returns {@code null}.
+     */
+    static TransformException getError(final List<ProjectedTransformTry> linearizers) {
+        TransformException error = null;
+        for (final ProjectedTransformTry alt : linearizers) {
+            final TransformException e = alt.error;
+            if (e != null) {
+                if (error == null) {
+                    error = e;
+                } else {
+                    error.addSuppressed(e);
+                }
+            }
+        }
+        return error;
+    }
+
+    /**
      * Orders by the inverse of correlation coefficients. Highest coefficients (best correlations)
      * are first, lower coefficients are next, {@link Float#NaN} values are last.
+     *
+     * <p><b>Note:</b> this comparison is inconsistent with {@link #equals(Object)}.
+     * The fact that {@link ProjectedTransformTry} instances are comparable should
+     * not be visible in public API.</p>
      */
     @Override
     public int compareTo(final ProjectedTransformTry other) {
@@ -316,6 +400,26 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
     }
 
     /**
+     * Implements the {@link Map.Entry#equals(Object)} contract.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj instanceof Map.Entry<?,?>) {
+            final Map.Entry<?,?> other = (Map.Entry<?,?>) obj;
+            return name.equals(other.getKey()) && projection.equals(other.getValue());
+        }
+        return false;
+    }
+
+    /**
+     * Implements the {@link Map.Entry#hashCode()} contract.
+     */
+    @Override
+    public int hashCode() {
+        return name.hashCode() ^ projection.hashCode();
+    }
+
+    /**
      * Formats a summary of this projection attempt. This method formats the following columns:
      *
      * <ol>
@@ -329,9 +433,6 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
      * @return format used for writing coefficients, or {@code null}.
      */
     final NumberFormat summarize(final TableAppender table, NumberFormat nf, final Locale locale) {
-        if (name == null) {
-            name = Vocabulary.getResources(locale).getString(Vocabulary.Keys.Identity);
-        }
         table.append(name).nextColumn();
         String message = "";
         if (error != 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 2b31c52..a3078f7 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
@@ -165,10 +165,12 @@ final class ResidualGrid extends DatumShiftGrid<Dimensionless,Dimensionless> {
      * @param residuals     the residual data, as translations to apply on the result of affine transform.
      * @param precision     desired precision of inverse transformations in unit of grid cells.
      * @param periods       if grid coordinates in some dimensions are cyclic, their periods in units of target CRS.
+     * @param linearizer    the linearizer that have been applied, or {@code null} if none.
      */
     ResidualGrid(final LinearTransform sourceToGrid, final LinearTransform gridToTarget,
             final int nx, final int ny, final float[] residuals, final double precision,
-            final double[] periods) throws TransformException
+            final double[] periods, final ProjectedTransformTry linearizer)
+            throws TransformException
     {
         super(Units.UNITY, sourceToGrid, new int[] {nx, ny}, true, Units.UNITY);
         this.gridToTarget   = gridToTarget;
@@ -176,7 +178,7 @@ final class ResidualGrid extends DatumShiftGrid<Dimensionless,Dimensionless> {
         this.accuracy       = precision;
         this.scanlineStride = nx;
         double[] periodVector = null;
-        if (periods != null && gridToTarget.isAffine()) {
+        if (periods != null && linearizer == null && gridToTarget.isAffine()) {
             /*
              * We require the transform to be affine because it makes the Jacobian independent of
              * coordinate values. It allows us to replace a period in target CRS units by periods
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 8db3297..6e736fb 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
@@ -351,7 +351,7 @@ public class InterpolatedTransform extends DatumShiftTransform {
      * NOTE: we do not bother to override the methods expecting a 'float' array because those methods should
      *       be rarely invoked. Since there is usually LinearTransforms before and after this transform, the
      *       conversion between float and double will be handled by those LinearTransforms.  If nevertheless
-     *       this MolodenskyTransform is at the beginning or the end of a transformation chain,  the methods
+     *       this InterpolatedTransform is at the beginning or the end of a transformation chain, the methods
      *       inherited from the subclass will work (but may be slightly slower).
      */
 
@@ -405,6 +405,24 @@ public class InterpolatedTransform extends DatumShiftTransform {
      * Transforms target coordinates to source coordinates. This is done by iteratively finding target coordinates
      * that shift to the input coordinates. The input coordinates is used as the first approximation.
      *
+     * <h2>Algorithm</h2>
+     * The algorithm used in this class takes some inspiration from the
+     * <a href="https://en.wikipedia.org/wiki/Gradient_descent">Gradient descent</a> method, except that we do not use
+     * <em>gradient</em> direction. Instead we use <em>positional error</em> direction computed with Jacobian matrix.
+     * Instead of moving in the opposite of gradient direction, we move in the opposite of positional error vector.
+     * This algorithm works well when the errors are small, which is the case for datum shift grids such as NADCON.
+     * It may work not so well with strongly curved <cite>localization grids</cite> as found in some netCDF files.
+     * In such case, the iterative algorithm may throw a {@link TransformException} with "No convergence" message.
+     * We could improve the algorithm by multiplying the translation vector by some factor {@literal 0 < γ < 1},
+     * for example by taking inspiration from the Barzilai–Borwein method. But this is not yet done for avoiding
+     * a computation cost penalty for the main target of this class, which is datum shift grids.
+     *
+     * <h3>Possible improvement</h3>
+     * If strongly curved localization grids need to be supported, a possible strategy for choosing a γ factor could
+     * be to compare the translation vector at an iteration step with the translation vector at previous iteration.
+     * If the two vectors cancel each other, we can retry with γ=0.5. This is assuming that the position we are
+     * looking for is located midway betweeb the two positions explored by the two iteration steps.
+     *
      * @author  Rueben Schulz (UBC)
      * @author  Martin Desruisseaux (IRD, Geomatys)
      * @version 1.0
@@ -496,6 +514,7 @@ public class InterpolatedTransform extends DatumShiftTransform {
 
         /**
          * Transforms an arbitrary amount of coordinate tuples.
+         * See class javadoc for some information about the algorithm.
          *
          * @throws TransformException if a point can not be transformed.
          */
@@ -586,6 +605,13 @@ public class InterpolatedTransform extends DatumShiftTransform {
                             assert Math.abs(dx - d[0]) < tol &
                                    Math.abs(dy - d[1]) < tol : Arrays.toString(d) + " versus [" + dx + ", " + dy + "]";
                         }
+                        /*
+                         * At this point we got the gradient vector, which is (dx,dy). In the simplest "gradient descent" method,
+                         * we would just subtract this vector from (xi,yi). It works well with datum shift grids (which are close
+                         * to flat) but does not converge with some strongly curved localization grids of remote sensors.
+                         * A solution would be to multiply (dx,dy) by some γ factor where 0 < γ < 1, but this is not done
+                         * yet for avoiding the computation cost of determining the γ value (see class javadoc).
+                         */
                         xi -= dx;
                         yi -= dy;
                         if (!(Math.abs(ex) > tol || Math.abs(ey) > tol)) break;     // Use '!' for catching NaN.
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 02fe30c..0a4ff0b 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
@@ -25,6 +25,7 @@ import org.opengis.util.FactoryException;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.geometry.DirectPosition1D;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.test.DependsOnMethod;
@@ -39,7 +40,7 @@ import static org.apache.sis.test.Assert.*;
  * Tests {@link LinearTransformBuilder}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.5
  * @module
  */
@@ -433,9 +434,11 @@ public final strictfp class LinearTransformBuilderTest extends TestCase {
         final NonLinearTransform tr = new NonLinearTransform();
         builder.addLinearizers(Collections.singletonMap("x² y³", tr));
         builder.addLinearizers(Collections.singletonMap("x³ y²", tr), 1, 0);
+        builder.addLinearizers(Collections.singletonMap("identity", MathTransforms.identity(2)));
         final Matrix m = builder.create(null).getMatrix();
-        assertEquals("linearizer", "x³ y²", builder.linearizerID());
+        assertEquals("linearizer", "x³ y²", builder.linearizer().get().getKey());
         assertNotSame("linearizer", tr, builder.linearizer().get());    // Not same because axes should have been swapped.
+        assertArrayEquals("correlations", new double[] {1, 1}, builder.correlation(), 1E-15);
         assertMatrixEquals("linear",
                 new Matrix3(2, 0, 3,
                             0, 1, 1,
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/ResidualGridTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/ResidualGridTest.java
index 5cda046..b709276 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/ResidualGridTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/ResidualGridTest.java
@@ -53,7 +53,7 @@ public final strictfp class ResidualGridTest extends TestCase {
                 0,2  ,  1,2  ,  2,1,
                 1,3  ,  2,2  ,  1,1,
                 0,4  ,  2,3  ,  3,2,
-                1,4  ,  3,3  ,  3,2}, 0.1, null);
+                1,4  ,  3,3  ,  3,2}, 0.1, null, null);
     }
 
     /**
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/InterpolatedTransformTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/InterpolatedTransformTest.java
index e673b04..7dccfa9 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/InterpolatedTransformTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/InterpolatedTransformTest.java
@@ -41,7 +41,7 @@ import org.junit.Test;
  * Tested transformations are:
  *
  * <ul>
- *   <li>Simple case based on linear calculations (easier to debug).</li>
+ *   <li>Simple case based on sinusoidal calculations (easier to debug).</li>
  *   <li>From NTF to RGF93 using a NTv2 grid.</li>
  *   <li>From NAD27 to NAD83 using a NADCON grid.</li>
  * </ul>
diff --git a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
index d66a56c..d4e1dd3 100644
--- a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
+++ b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
@@ -285,15 +285,18 @@ public final class GCOM_C extends Convention {
     }
 
     /**
-     * Returns an enumeration of two-dimensional non-linear transforms that may be tried in attempts to make
-     * localization grid more linear.
+     * Returns the two-dimensional non-linear transforms to apply for making the localization grid more linear.
+     * This method returns a singleton without specifying the identity transform as an acceptable alternative.
+     * It means that the specified projection (UTM) is considered mandatory for this format.
      *
      * @param  decoder  the netCDF file for which to determine linearizers that may possibly apply.
-     * @return enumeration of two-dimensional non-linear transforms to try.
+     * @return enumeration of two-dimensional non-linear transforms to apply.
+     *
+     * @see #defaultHorizontalCRS(boolean)
      */
     @Override
     public Set<Linearizer> linearizers(final Decoder decoder) {
-        return Collections.singleton(Linearizer.GROUND_TRACK);
+        return Collections.singleton(new Linearizer(CommonCRS.WGS84, Linearizer.Type.UTM));
     }
 
     /**
@@ -416,7 +419,7 @@ public final class GCOM_C extends Convention {
      * Returns the default prime meridian, ellipsoid, datum or CRS to use if no information is found in the netCDF file.
      * GCOM documentation said that the datum is WGS 84.
      *
-     * @param  spherical  ignored, since we assume a sphere in all cases.
+     * @param  spherical  ignored, since we assume an ellipsoid in all cases.
      * @return information about geodetic objects to use if no explicit information is found in the file.
      */
     @Override
diff --git a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java
index b249a2e..3410a54 100644
--- a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java
+++ b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java
@@ -30,6 +30,7 @@ import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.netcdf.VariableRole;
 import org.apache.sis.internal.netcdf.Linearizer;
+import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
 
 
@@ -179,15 +180,30 @@ public final class GCOM_W extends Convention {
 
 
     /**
-     * Returns an enumeration of two-dimensional non-linear transforms that may be tried in attempts to make
-     * localization grid more linear.
+     * Returns the two-dimensional non-linear transforms to apply for making the localization grid more linear.
+     * This method returns a singleton without specifying the identity transform as an acceptable alternative.
+     * It means that the specified projection (UTM) is considered mandatory for this format.
      *
      * @param  decoder  the netCDF file for which to determine linearizers that may possibly apply.
-     * @return enumeration of two-dimensional non-linear transforms to try.
+     * @return enumeration of two-dimensional non-linear transforms to apply.
+     *
+     * @see #defaultHorizontalCRS(boolean)
      */
     @Override
     public Set<Linearizer> linearizers(final Decoder decoder) {
-        return Collections.singleton(Linearizer.GROUND_TRACK);
+        return Collections.singleton(new Linearizer(CommonCRS.WGS84, Linearizer.Type.UTM));
+    }
+
+    /**
+     * Returns the default prime meridian, ellipsoid, datum or CRS to use if no information is found in the netCDF file.
+     * GCOM documentation said that the datum is WGS 84.
+     *
+     * @param  spherical  ignored, since we assume an ellipsoid in all cases.
+     * @return information about geodetic objects to use if no explicit information is found in the file.
+     */
+    @Override
+    public CommonCRS defaultHorizontalCRS(final boolean spherical) {
+        return CommonCRS.WGS84;
     }
 
 
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
index 5c1db79..65020c0 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
@@ -35,6 +35,7 @@ import org.opengis.referencing.cs.CoordinateSystemAxis;
 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.opengis.metadata.content.TransferFunctionType;
 import org.apache.sis.internal.referencing.AxisDirections;
 import org.apache.sis.internal.util.Numerics;
@@ -380,7 +381,7 @@ public final class Axis extends NamedElement {
      *
      * @see #getMainSize()
      */
-    final int getMainDirection() {
+    private int getMainDirection() {
         return (getNumDimensions() < 2 || gridDimensionIndices[0] <= gridDimensionIndices[1]) ? 0 : 1;
     }
 
@@ -767,8 +768,11 @@ public final class Axis extends NamedElement {
      * @return the localization grid, or {@code null} if none can be built.
      * @throws IOException if an error occurred while reading the data.
      * @throws DataStoreException if a logical error occurred.
+     * @throws TransformException if an unexpected error occurred during application of a linearizer.
      */
-    final MathTransform createLocalizationGrid(final Axis other) throws IOException, FactoryException, DataStoreException {
+    final GridCacheValue createLocalizationGrid(final Axis other)
+            throws IOException, FactoryException, TransformException, DataStoreException
+    {
         if (getNumDimensions() != 2 || other.getNumDimensions() != 2) {
             return null;
         }
@@ -803,7 +807,7 @@ public final class Axis extends NamedElement {
          */
         final Decoder decoder = coordinates.decoder;
         final GridCacheKey keyLocal = new GridCacheKey(width, height, this, other);
-        MathTransform tr = keyLocal.cached(decoder);
+        GridCacheValue tr = keyLocal.cached(decoder);
         if (tr != null) {
             return tr;
         }
@@ -816,7 +820,7 @@ public final class Axis extends NamedElement {
         final Vector vy = other.read();
         final Set<Linearizer> linearizers = decoder.convention().linearizers(decoder);
         final GridCacheKey.Global keyGlobal = new GridCacheKey.Global(keyLocal, vx, vy, linearizers);
-        final Cache.Handler<MathTransform> handler = keyGlobal.lock();
+        final Cache.Handler<GridCacheValue> handler = keyGlobal.lock();
         try {
             tr = handler.peek();
             if (tr == null) {
@@ -845,7 +849,8 @@ public final class Axis extends NamedElement {
                  */
                 final MathTransformFactory factory = decoder.getMathTransformFactory();
                 if (!linearizers.isEmpty()) {
-                    Linearizer.applyTo(linearizers, factory, grid, this, other);
+                    // Current version does not need the factory, but future version may use it.
+                    Linearizer.setCandidatesOnGrid(new Axis[] {this, other}, linearizers, grid);
                 }
                 /*
                  * There is usually a one-to-one relationship between localization grid cells and image pixels.
@@ -856,7 +861,7 @@ public final class Axis extends NamedElement {
                  * to take in account the case where dataToGridIndices() returns 0.1.
                  */
                 grid.setDesiredPrecision(0.001);
-                tr = grid.create(factory);
+                tr = new GridCacheValue(linearizers, grid, factory);
                 tr = keyLocal.cache(decoder, tr);
             }
         } finally {
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
index 782f947..e62c53e 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
@@ -41,6 +41,7 @@ import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.operation.CoordinateOperationFactory;
 import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.Conversion;
+import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.cs.AbstractCS;
 import org.apache.sis.referencing.cs.AxesConvention;
@@ -169,11 +170,21 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
     /**
      * Infers a new CRS for a {@link Grid}.
      *
-     * @param  decoder  the decoder of the netCDF from which the CRS are constructed.
-     * @param  grid     the grid for which the CRS are constructed.
+     * <h4>CRS replacements</h4>
+     * The {@code linearizationTargets} argument allows to replace some CRSs inferred by this method by hard-coded CRSs.
+     * This is non-empty only when reading a netCDF file for a specific profile, i.e. a file decoded with a subclass of
+     * {@link Convention}. The CRS to be replaced is inferred from the axis directions.
+     *
+     * @param  decoder               the decoder of the netCDF from which the CRS are constructed.
+     * @param  grid                  the grid for which the CRS are constructed.
+     * @param  linearizationTargets  CRS to use instead of CRS inferred by this method, or null or empty if none.
+     * @param  reorderGridToCRS      an affine transform doing a final step in a "grid to CRS" transform for ordering axes.
+     *         Not used by this method, but may be modified for taking in account axis order changes caused by replacements
+     *         defined in {@code linearizationTargets}. Ignored (can be null) if {@code linearizationTargets} is null.
      * @return coordinate reference system from the given axes, or {@code null}.
      */
-    public static CoordinateReferenceSystem assemble(final Decoder decoder, final Grid grid)
+    public static CoordinateReferenceSystem assemble(final Decoder decoder, final Grid grid,
+            final List<CoordinateReferenceSystem> linearizationTargets, final Matrix reorderGridToCRS)
             throws DataStoreException, FactoryException, IOException
     {
         final List<CRSBuilder<?,?>> builders = new ArrayList<>(4);
@@ -184,6 +195,14 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
         for (int i=0; i < components.length; i++) {
             components[i] = builders.get(i).build(decoder, true);
         }
+        /*
+         * If there is hard-coded CRS implied by `Convention.linearizers()`, use it now.
+         * We do not verify the datum; we assume that the linearizer that built the CRS
+         * was consistent with `Convention.defaultHorizontalCRS(false)`.
+         */
+        if ((linearizationTargets != null) && !linearizationTargets.isEmpty()) {
+            Linearizer.replaceInCompoundCRS(components, linearizationTargets, reorderGridToCRS);
+        }
         switch (components.length) {
             case 0: return null;
             case 1: return components[0];
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
index 3ece160..012e366 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
@@ -356,10 +356,18 @@ public class Convention {
      * one resulting in best {@linkplain org.apache.sis.math.Plane#fit linear regression correlation coefficients}
      * will be selected.
      *
+     * <p>If the returned set is non-empty, exactly one of the linearizers will be applied. If not applying any
+     * linearizer is an acceptable solution, then an identity linearizer should be explicitly returned.</p>
+     *
+     * <p>The returned set shall not contain two linearizers of the same {@linkplain Linearizer#type type}
+     * because the types (not the full linearizers) are used in keys for caching localization grids.</p>
+     *
      * <p>Default implementation returns an empty set.</p>
      *
      * @param  decoder  the netCDF file for which to get linearizer candidates.
      * @return enumeration of two-dimensional non-linear transforms to try on the localization grid.
+     *
+     * @see #defaultHorizontalCRS(boolean)
      */
     public Set<Linearizer> linearizers(final Decoder decoder) {
         return Collections.emptySet();
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
index 183569e..e5cfd4a 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
@@ -35,7 +35,6 @@ import org.opengis.util.NameSpace;
 import org.opengis.util.NameFactory;
 import org.opengis.referencing.datum.Datum;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.event.StoreListeners;
@@ -130,16 +129,18 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
 
     /**
      * Cache of localization grids created for a given pair of (<var>x</var>,<var>y</var>) axes. Localization grids
-     * are expensive to compute and consume a significant amount of memory.  The {@link Grid} instances returned by
-     * {@link #getGrids()} allow to share localization grids only between variables using the exact same list of dimensions.
+     * are expensive to compute and consume a significant amount of memory. The {@link Grid} instances returned by
+     * {@link #getGrids()} share localization grids only between variables using the exact same list of dimensions.
      * This {@code localizationGrids} cache allows to cover other cases.
-     * For example a netCDF file may have a variable with (<var>longitude</var>, <var>latitude</var>) dimensions
-     * and another variable with (<var>longitude</var>, <var>latitude</var>, <var>depth</var>) dimensions,
-     * with both variables using the same localization grid for the (<var>longitude</var>, <var>latitude</var>) part.
+     *
+     * <div class="note"><b>Example:</b>
+     * a netCDF file may have a variable with (<var>longitude</var>, <var>latitude</var>) dimensions and another
+     * variable with (<var>longitude</var>, <var>latitude</var>, <var>depth</var>) dimensions, with both variables
+     * using the same localization grid for the (<var>longitude</var>, <var>latitude</var>) part.</div>
      *
      * @see GridCacheKey#cached(Decoder)
      */
-    final Map<GridCacheKey,MathTransform> localizationGrids;
+    final Map<GridCacheKey,GridCacheValue> localizationGrids;
 
     /**
      * Where to send the warnings.
@@ -437,7 +438,7 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
         if (list.isEmpty()) {
             final List<Exception> warnings = new ArrayList<>();     // For internal usage by Grid.
             for (final Grid grid : getGrids()) {
-                addIfNotPresent(list, grid.getCoordinateReferenceSystem(this, warnings));
+                addIfNotPresent(list, grid.getCoordinateReferenceSystem(this, warnings, null, null));
             }
         }
         return list;
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
index f3f3d45..fe64f24 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
@@ -25,12 +25,11 @@ import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
-import org.opengis.referencing.crs.GeographicCRS;
+import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.apache.sis.internal.referencing.AxisDirections;
 import org.apache.sis.referencing.operation.matrix.Matrices;
-import org.apache.sis.referencing.CRS;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.IllegalGridGeometryException;
@@ -48,7 +47,7 @@ import org.apache.sis.util.ArraysExt;
  * if a variable dimensions should considered as bands instead than spatiotemporal dimensions.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  *
  * @see Decoder#getGrids()
  *
@@ -79,7 +78,7 @@ public abstract class Grid extends NamedElement {
      * The coordinate reference system, created when first needed.
      * May be {@code null} even after we attempted to create it.
      *
-     * @see #getCoordinateReferenceSystem(Decoder, List)
+     * @see #getCoordinateReferenceSystem(Decoder, List, List, Matrix)
      */
     private CoordinateReferenceSystem crs;
 
@@ -290,24 +289,34 @@ public abstract class Grid extends NamedElement {
      * this method because this CRS will be used for adjusting axis order or for completion if grid mapping does not include
      * information for all dimensions.</p>
      *
-     * @param   decoder   the decoder for which CRS are constructed.
-     * @param   warnings  previous warnings, for avoiding to log the same message twice. Can be null.
-     * @return  the CRS for this grid geometry, or {@code null}.
-     * @throws  IOException if an I/O operation was necessary but failed.
-     * @throws  DataStoreException if the CRS can not be constructed.
+     * @param  decoder               the decoder for which CRS are constructed.
+     * @param  warnings              previous warnings, for avoiding to log the same message twice. Can be null.
+     * @param  linearizationTargets  CRS to use instead of CRS inferred by this method, or null or empty if none.
+     * @param  reorderGridToCRS      an affine transform doing a final step in a "grid to CRS" transform for ordering axes.
+     *         Not used by this method, but may be modified for taking in account axis order changes caused by replacements
+     *         defined in {@code linearizationTargets}. Ignored (can be null) if {@code linearizationTargets} is null.
+     * @return the CRS for this grid geometry, or {@code null}.
+     * @throws IOException if an I/O operation was necessary but failed.
+     * @throws DataStoreException if the CRS can not be constructed.
      */
-    final CoordinateReferenceSystem getCoordinateReferenceSystem(final Decoder decoder, final List<Exception> warnings)
+    final CoordinateReferenceSystem getCoordinateReferenceSystem(final Decoder decoder, final List<Exception> warnings,
+            final List<CoordinateReferenceSystem> linearizationTargets, final Matrix reorderGridToCRS)
             throws IOException, DataStoreException
     {
-        if (!isCRSDetermined) try {
-            isCRSDetermined = true;                             // Set now for avoiding new attempts if creation fail.
-            crs = CRSBuilder.assemble(decoder, this);
+        final boolean isCached = (linearizationTargets == null) || linearizationTargets.isEmpty();
+        if (isCached & isCRSDetermined) {
+            return crs;
+        } else try {
+            if (isCached) isCRSDetermined = true;               // Set now for avoiding new attempts if creation fail.
+            final CoordinateReferenceSystem result = CRSBuilder.assemble(decoder, this, linearizationTargets, reorderGridToCRS);
+            if (isCached) crs = result;
+            return result;
         } catch (FactoryException | NullArgumentException ex) {
             if (isNewWarning(ex, warnings)) {
                 canNotCreate(decoder, "getCoordinateReferenceSystem", Resources.Keys.CanNotCreateCRS_3, ex);
             }
+            return null;
         }
-        return crs;
     }
 
     /**
@@ -439,6 +448,7 @@ findFree:       for (int srcDim : axis.gridDimensionIndices) {
              * two-dimensional localization grid. Those transforms require two variables, i.e. "two-dimensional"
              * axes come in pairs.
              */
+            final List<CoordinateReferenceSystem> linearizationTargets = new ArrayList<>();
             for (int i=0; i<nonLinears.size(); i++) {         // Length of 'nonLinears' may change in this loop.
                 if (nonLinears.get(i) == null) {
                     for (int j=i; ++j < nonLinears.size();) {
@@ -460,13 +470,13 @@ findFree:       for (int srcDim : axis.gridDimensionIndices) {
                                 case +1: ArraysExt.swap(gridAxes, 0, 1); break;
                                 default: continue;            // Needs axes at consecutive source dimensions.
                             }
-                            final MathTransform grid = gridAxes[0].createLocalizationGrid(gridAxes[1]);
+                            final GridCacheValue grid = gridAxes[0].createLocalizationGrid(gridAxes[1]);
                             if (grid != null) {
                                 /*
                                  * Replace the first transform by the two-dimensional localization grid and
                                  * remove the other transform. Removals need to be done in arrays too.
                                  */
-                                nonLinears.set(i, grid);
+                                nonLinears.set(i, grid.transform);
                                 nonLinears.remove(j);
                                 final int n = nonLinears.size() - j;
                                 System.arraycopy(deferred,             j+1, deferred,             j, n);
@@ -474,6 +484,7 @@ findFree:       for (int srcDim : axis.gridDimensionIndices) {
                                 if (otherDim < srcDim) {
                                     gridDimensionIndices[i] = otherDim;     // Index of the first dimension.
                                 }
+                                grid.getLinearizationTarget(linearizationTargets);
                                 break;                                      // Continue the 'i' loop.
                             }
                         }
@@ -491,12 +502,19 @@ findFree:       for (int srcDim : axis.gridDimensionIndices) {
                 if (s < 0) return null;
             }
             /*
+             * Create the coordinate reference system now, because this method may modify the `affine` transform.
+             * This modification happens only if `Convention.linearizers()` specified transforms to apply on the
+             * localization grid for making it more linear. This is a profile-dependent feature.
+             */
+            final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(decoder, null, linearizationTargets, affine);
+            /*
              * Final transform, as the concatenation of the non-linear transforms followed by the affine transform.
              * We concatenate the affine transform last because it may change axis order.
              */
             MathTransform gridToCRS = null;
             final int nonLinearCount = nonLinears.size();
             final MathTransformFactory factory = decoder.getMathTransformFactory();
+            // Not a non-linear transform, but we abuse this list for convenience.
             nonLinears.add(factory.createAffineTransform(affine));
             for (int i=0; i <= nonLinearCount; i++) {
                 MathTransform tr = nonLinears.get(i);
@@ -514,17 +532,14 @@ findFree:       for (int srcDim : axis.gridDimensionIndices) {
              * to be at the centers of the cells, but we do not require that in this standard". We nevertheless check
              * if an axis thinks otherwise.
              */
-            final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(decoder, null);
-            if (CRS.getHorizontalComponent(crs) instanceof GeographicCRS) {
-                for (final Axis axis : axes) {
-                    if (axis.isCellCorner()) {
-                        anchor = PixelInCell.CELL_CORNER;
-                        break;
-                    }
+            for (final Axis axis : axes) {
+                if (axis.isCellCorner()) {
+                    anchor = PixelInCell.CELL_CORNER;
+                    break;
                 }
             }
             geometry = new GridGeometry(getExtent(axes), anchor, gridToCRS, crs);
-        } catch (FactoryException | IllegalGridGeometryException ex) {
+        } catch (FactoryException | TransformException | IllegalGridGeometryException ex) {
             canNotCreate(decoder, "getGridGeometry", Resources.Keys.CanNotCreateGridGeometry_3, ex);
         }
         return geometry;
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheKey.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheKey.java
index c69df17..fae9e03 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheKey.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheKey.java
@@ -18,9 +18,9 @@ package org.apache.sis.internal.netcdf;
 
 import java.util.Set;
 import java.util.Arrays;
+import java.util.EnumSet;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.util.collection.Cache;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.internal.storage.io.ByteWriter;
@@ -39,7 +39,7 @@ import org.apache.sis.math.Vector;
  * The base class if for local cache. The inner class is for the global cache.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
@@ -89,7 +89,7 @@ class GridCacheKey {
      * Returns the localization grid from the local cache if one exists, or {@code null} if none.
      * This method looks only in the local cache. For the global cache, see {@link Global#lock()}.
      */
-    final MathTransform cached(final Decoder decoder) {
+    final GridCacheValue cached(final Decoder decoder) {
         return decoder.localizationGrids.get(this);
     }
 
@@ -101,8 +101,8 @@ class GridCacheKey {
      * @param  grid     the grid to cache.
      * @return the cached grid. Should be the given {@code grid} instance, unless another grid has been cached concurrently.
      */
-    final MathTransform cache(final Decoder decoder, final MathTransform grid) {
-        final MathTransform tr = decoder.localizationGrids.putIfAbsent(this, grid);
+    final GridCacheValue cache(final Decoder decoder, final GridCacheValue grid) {
+        final GridCacheValue tr = decoder.localizationGrids.putIfAbsent(this, grid);
         return (tr != null) ? tr : grid;
     }
 
@@ -118,13 +118,13 @@ class GridCacheKey {
         /**
          * The global cache shared by all netCDF files. All grids are retained by weak references.
          */
-        private static final Cache<GridCacheKey,MathTransform> CACHE = new Cache<>(12, 0, false);
+        private static final Cache<GridCacheKey,GridCacheValue> CACHE = new Cache<>(12, 0, false);
 
         /**
          * The algorithms tried for making the localization grids more linear.
          * May be empty but shall not be null.
          */
-        private final Set<Linearizer> linearizers;
+        private final Set<Linearizer.Type> linearizerTypes;
 
         /**
          * Concatenation of the digests of the two vectors.
@@ -142,7 +142,10 @@ class GridCacheKey {
          */
         Global(final GridCacheKey keyLocal, final Vector vx, final Vector vy, final Set<Linearizer> linearizers) {
             super(keyLocal);
-            this.linearizers = linearizers;
+            linearizerTypes = EnumSet.noneOf(Linearizer.Type.class);
+            for (final Linearizer linearizer : linearizers) {
+                linearizerTypes.add(linearizer.type);
+            }
             final MessageDigest md;
             try {
                 md = MessageDigest.getInstance("MD5");
@@ -180,8 +183,8 @@ class GridCacheKey {
          * This method must be used with a {@code try … finally} block as below:
          *
          * {@preformat java
-         *     MathTransform tr;
-         *     final Cache.Handler<MathTransform> handler = key.lock();
+         *     GridCacheValue tr;
+         *     final Cache.Handler<GridCacheValue> handler = key.lock();
          *     try {
          *         tr = handler.peek();
          *         if (tr == null) {
@@ -192,7 +195,7 @@ class GridCacheKey {
          *     }
          * }
          */
-        final Cache.Handler<MathTransform> lock() {
+        final Cache.Handler<GridCacheValue> lock() {
             return CACHE.lock(this);
         }
 
@@ -201,7 +204,7 @@ class GridCacheKey {
          * The hash code uses a digest of coordinate values given at construction time.
          */
         @Override public int hashCode() {
-            return super.hashCode() + linearizers.hashCode() + Arrays.hashCode(digest);
+            return super.hashCode() + linearizerTypes.hashCode() + Arrays.hashCode(digest);
         }
 
         /**
@@ -213,7 +216,7 @@ class GridCacheKey {
         @Override public boolean equals(final Object other) {
             if (super.equals(other)) {
                 final Global that = (Global) other;
-                if (linearizers.equals(that.linearizers)) {
+                if (linearizerTypes.equals(that.linearizerTypes)) {
                     return Arrays.equals(digest, that.digest);
                 }
             }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheValue.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheValue.java
new file mode 100644
index 0000000..cd3be6d
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheValue.java
@@ -0,0 +1,77 @@
+/*
+ * 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.internal.netcdf;
+
+import java.util.Set;
+import java.util.List;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
+
+
+/**
+ * A value cached in {@link GridCacheKey.Global#CACHE}.
+ * This is used for sharing common localization grids between different netCDF files.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class GridCacheValue {
+    /**
+     * The transform from grid coordinates to geographic or projected coordinates.
+     */
+    final MathTransform transform;
+
+    /**
+     * The target CRS of {@link #transform} if different than the CRS inferred by {@link CRSBuilder}.
+     * This field is non-null if the target CRS has been changed by application of a linearizer.
+     */
+    private CoordinateReferenceSystem targetCRS;
+
+    /**
+     * Creates a new "grid to CRS" together with target CRS.
+     */
+    GridCacheValue(final Set<Linearizer> linearizers, final LocalizationGridBuilder grid,
+                   final MathTransformFactory factory) throws FactoryException
+    {
+        transform = grid.create(factory);
+        grid.linearizer(true).ifPresent((e) -> {
+            final String name = e.getKey();
+            for (final Linearizer linearizer : linearizers) {
+                if (name.equals(linearizer.name())) {
+                    targetCRS = linearizer.getTargetCRS();
+                    break;
+                }
+            }
+        });
+    }
+
+    /**
+     * Adds the target CRS to the given list if that CRS is different than the CRS inferred by {@link CRSBuilder}.
+     * This is an element of the list to provide to {@link CRSBuilder#assemble(Decoder, Grid, List, Matrix)}.
+     */
+    final void getLinearizationTarget(final List<CoordinateReferenceSystem> list) {
+        if (targetCRS != null) {
+            list.add(targetCRS);
+        }
+    }
+}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
index eaa8cc1..5a0f50f 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
@@ -19,153 +19,231 @@ package org.apache.sis.internal.netcdf;
 import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
-import org.opengis.util.FactoryException;
-import org.opengis.parameter.ParameterValueGroup;
+import java.util.List;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.crs.ProjectedCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+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.referencing.operation.matrix.Matrix3;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
-import org.apache.sis.internal.metadata.ReferencingServices;
-import org.apache.sis.internal.system.DefaultFactories;
-import org.apache.sis.internal.util.Constants;
-import org.apache.sis.util.logging.Logging;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.internal.util.Strings;
+import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.storage.DataStoreReferencingException;
 
 
 /**
- * Two-dimensional non-linear transforms to try in attempts to make a localization grid more linear.
+ * A two-dimensional non-linear transform to try in an attempt to make a localization grid more linear.
  * Non-linear transforms are tested in "trials and errors" and the one resulting in best correlation
- * coefficients is selected. This enumeration identifies which linearizers to try for a given file.
- *
- * <p>When a non-linear transform exists in spherical or ellipsoidal variants, we use the spherical
- * formulas instead than ellipsoidal formulas because the spherical ones are faster and more stable
- * (because the inverse transforms are exact, up to rounding errors).  The errors caused by the use
- * of spherical formulas are compensated by the localization grid used after the linearizer.</p>
+ * coefficients is selected.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  *
- * @see org.apache.sis.referencing.operation.builder.LocalizationGridBuilder#addLinearizers(Map, int...)
+ * @see org.apache.sis.referencing.operation.builder.LocalizationGridBuilder#addLinearizers(Map, boolean, int...)
  *
  * @since 1.0
  * @module
  */
-public enum Linearizer {
+public final class Linearizer {
+    /**
+     * The datum to use as one of the predefined constants. The ellipsoid size do not matter
+     * because a linear regression will be applied anyway. However the eccentricity matter.
+     *
+     * <p>When a non-linear transform exists in spherical or ellipsoidal variants, it may be sufficient to use
+     * the spherical formulas instead than ellipsoidal formulas because the spherical ones are faster and more
+     * stable (because the inverse transforms are exact, up to rounding errors). The errors caused by the use
+     * of spherical formulas are compensated by the localization grid used after the linearizer.
+     * Spherical formulas can be requested by setting this field to {@link CommonCRS#SPHERE}.</p>
+     *
+     * @see Convention#defaultHorizontalCRS(boolean)
+     */
+    private final CommonCRS datum;
+
+    /**
+     * The type of projection to create.
+     * Current implementation supports only Universal Transverse Mercator (UTM) projection,
+     * but we nevertheless define this enumeration as a place-holder for more types in the future.
+     */
+    public enum Type {
+        /**
+         * Universal Transverse Mercator projection.
+         */
+        UTM
+    }
+
+    /**
+     * The type of projection to create (Mercator, UTM, <i>etc</i>).
+     */
+    final Type type;
+
     /**
-     * Mercator (Spherical) projection. Inputs are latitude and longitude in any order (axis order will
-     * be detected by inspection of {@link Axis} elements). Outputs are projected coordinates.
+     * The target coordinate reference system after application of the non-linear transform.
      */
-    MERCATOR("Mercator (Spherical)"),
+    private CoordinateReferenceSystem targetCRS;
 
     /**
-     * Satellite ground track.
+     * Creates a new linearizer working on the specified datum.
+     *
+     * @param  datum  the datum to use. Should be consistent with {@link Convention#defaultHorizontalCRS(boolean)}.
+     * @param  type   the type of projection to create (Mercator, UTM, <i>etc</i>).
      */
-    GROUND_TRACK(null);
+    public Linearizer(final CommonCRS datum, final Type type) {
+        this.datum = datum;
+        this.type  = type;
+    }
 
     /**
-     * The map projection method to use for constructing {@link #transform}, or {@code null} if the operation
-     * is not a map projection or has already be constructed.  If non-null, the identified operation requires
-     * (<var>longitude</var>, <var>latitude</var>) axis order.
+     * Returns the name used for identifying this linearizer in {@link LocalizationGridBuilder}.
      */
-    private String projection;
+    final String name() {
+        return type.name();
+    }
 
     /**
-     * The transform to apply, or {@code null} if none or not yet created. This is created by {@link #transform()}
-     * when first needed. The value after initialization may still be {@code null} if initialization failed.
+     * Returns the target CRS computed by {@link #gridToTargetCRS gridToTargetCRS(…)}.
      */
-    private MathTransform transform;
+    final CoordinateReferenceSystem getTargetCRS() {
+        return targetCRS;
+    }
 
     /**
-     * Creates a new linearizer for the given projection method.
+     * Returns a string representation for debugging purposes.
      */
-    private Linearizer(final String projection) {
-        this.projection = projection;
+    @Override
+    public String toString() {
+        return Strings.toString(getClass(), "type", type, "targetCRS", IdentifiedObjects.getName(targetCRS, null));
     }
 
     /**
-     * Returns the hard-coded transform represented by this enumeration that may help to make a localization grid
-     * more linear.
+     * Creates a transform for the given localization grid. The returned transform expects source coordinates in
+     * (latitude, longitude) or (longitude, latitude) order, depending on {@code xdim} and {@code ydim} values.
+     * Target coordinates will be in the order defined by {@link #targetCRS}, which is assigned by this method.
      *
-     * @return the transform, or {@code null} if it can not be built.
+     * @param  grid  the grid on which to add non-linear transform.
+     * @param  xdim  index of longitude dimension in the grid control points.
+     * @param  ydim  index of latitude dimension in the grid control points.
+     * @return a two-dimensional transform expecting source coordinates in (longitude, latitude) order.
+     * @throws TransformException if grid coordinates can not be obtained. Actually this exception
+     *         should never happen because the {@code MathTransform} used is a linear transform.
      */
-    private synchronized MathTransform transform() {
-        final String p = projection;
-        if (p != null) {
-            projection = null;                              // Set to null now in case of failure.
-            final MathTransformFactory factory = DefaultFactories.forClass(MathTransformFactory.class);
-            if (factory != null) try {    // Should never be null, but be tolerant to configuration oddity.
-                /*
-                 * The exact value of sphere radius does not matter because a linear regression will
-                 * be applied anyway. However it matter to define a sphere instead than an ellipsoid
-                 * because the spherical equations are simpler (consequently faster and more stable).
-                 */
-                final ParameterValueGroup pg = factory.getDefaultParameters(p);
-                pg.parameter(Constants.SEMI_MAJOR).setValue(ReferencingServices.AUTHALIC_RADIUS);
-                pg.parameter(Constants.SEMI_MINOR).setValue(ReferencingServices.AUTHALIC_RADIUS);
-                transform = factory.createParameterizedTransform(pg);
-            } catch (FactoryException e) {
-                warning(e);
+    private MathTransform gridToTargetCRS(final LocalizationGridBuilder grid, final int xdim, final int ydim)
+            throws TransformException
+    {
+        MathTransform transform;
+        switch (type) {
+            default: {
+                throw new AssertionError(type);
             }
+            /*
+             * Create a Universal Transverse Mercator (UTM) projection
+             * for the zone containing a point in the middle of the grid.
+             */
+            case UTM: {
+                final Envelope bounds = grid.getSourceEnvelope(false);
+                final double[] median = grid.getControlPoint(
+                        (int) Math.round(bounds.getMedian(0)),
+                        (int) Math.round(bounds.getMedian(1)));
+                ProjectedCRS crs = datum.universal(median[ydim], median[xdim]);
+                transform = crs.getConversionFromBase().getMathTransform();
+                targetCRS = crs;
+                break;
+            }
+        }
+        /*
+         * Above transform expects (latitude, longitude) inputs. If grid coordinates
+         * are in (longitude, latitude) order, we need to swap input coordinates.
+         */
+        if (xdim < ydim) {
+            final Matrix3 m = new Matrix3();
+            m.m00 = m.m11 = 0;
+            m.m01 = m.m10 = 1;
+            transform = MathTransforms.concatenate(MathTransforms.linear(m), transform);
         }
         return transform;
     }
 
     /**
      * Applies non-linear transform candidates to the given localization grid.
+     * This method tries to locate longitude and latitude axes. If those axes are found,
+     * they will be used as input coordinates for the {@link MathTransform} instances
+     * (typically map projections) created by {@link #gridToTargetCRS gridToTargetCRS(…)}.
+     * Those transforms are then {@linkplain LocalizationGridBuilder#addLinearizers given
+     * to the localization grid} for consideration in attempts to make the grid more linear.
      *
-     * @param  factory      the factory to use for creating transforms.
+     * @param  sourceAxes   coordinate system axes in CRS order.
      * @param  linearizers  the linearizers to apply.
      * @param  grid         the grid on which to add non-linear transform candidates.
-     * @param  axes         coordinate system axes in CRS order.
+     * @throws TransformException if grid coordinates can not be obtained. Actually this exception should never
+     *         happen because the {@code MathTransform} used is a linear transform. We propagate this exception
+     *         because it is more convenient to have it handled by the caller together with other exceptions.
      */
-    static void applyTo(final Set<Linearizer> linearizers, final MathTransformFactory factory,
-                        final LocalizationGridBuilder grid, final Axis... axes)
+    static void setCandidatesOnGrid(final Axis[] sourceAxes, final Set<Linearizer> linearizers, final LocalizationGridBuilder grid)
+            throws TransformException
     {
         int xdim = -1, ydim = -1;
-        for (int i=axes.length; --i >= 0;) {
-            switch (axes[i].abbreviation) {
+        for (int i=sourceAxes.length; --i >= 0;) {
+            switch (sourceAxes[i].abbreviation) {
                 case 'λ': xdim = i; break;
                 case 'φ': ydim = i; break;
             }
         }
-        if (xdim >= 0 && ydim >= 0) {
+        if ((xdim | ydim) >= 0) {
             final Map<String,MathTransform> projections = new HashMap<>();
             for (final Linearizer linearizer : linearizers) {
-                MathTransform transform;
-                switch (linearizer) {
-                    default: {
-                        transform = linearizer.transform();
-                        break;
-                    }
-                    /*
-                     * Some special cases require information about the particular grid we have at hand.
-                     * Only one case for now, but more cases may be added here in the future.
-                     */
-                    case GROUND_TRACK: {
-                        int direction = axes[ydim].getMainDirection();  // Fastest varying dimension (in netCDF order) of latitude.
-                        direction ^= 1;                                 // Convert netCDF order to CRS order.
-                        try {
-                            transform = SatelliteGroundTrack.create(factory, grid, xdim, direction);
-                        } catch (FactoryException | TransformException e) {
-                            transform = null;
-                            warning(e);
-                        }
-                        break;
-                    }
-                }
-                if (transform != null) {
-                    projections.put(linearizer.name(), transform);
-                }
+                final MathTransform transform = linearizer.gridToTargetCRS(grid, xdim, ydim);
+                projections.put(linearizer.name(), transform);
             }
-            grid.addLinearizers(projections, xdim, ydim);
+            grid.addLinearizers(projections, false, Math.min(xdim, ydim), Math.max(xdim, ydim));
         }
     }
 
     /**
-     * Reports a warning as originating from {@link Variable#getGridGeometry()}, because it is the caller
-     * (indirectly) for this class. The warning reported to this method should never happen. But if one
-     * happens anyway, do not cause the whole netCDF reader to fail for all files because of this error.
+     * Given CRS components inferred by {@link CRSBuilder}, replaces CRS components in the dimensions
+     * where linearization has been applied. The CRS components to replace are inferred from axis directions.
+     *
+     * @param  components        the components of the compound CRS that {@link CRSBuilder} inferred.
+     * @param  replacements      the {@link #targetCRS} of linearizations.
+     * @param  reorderGridToCRS  an affine transform doing a final step in a "grid to CRS" transform for ordering axes.
+     *         Not used by this method, but modified for taking in account axis order changes caused by replacements.
      */
-    private static void warning(final Exception e) {
-        Logging.unexpectedException(Decoder.LOGGER, Variable.class, "getGridGeometry", e);
+    static void replaceInCompoundCRS(final SingleCRS[] components,
+            final List<CoordinateReferenceSystem> replacements, final Matrix reorderGridToCRS)
+            throws DataStoreReferencingException
+    {
+        Matrix original = null;
+search: for (final CoordinateReferenceSystem targetCRS : replacements) {
+            int firstDimension = 0;
+            for (int i=0; i < components.length; i++) {
+                final SingleCRS sourceCRS = components[i];
+                final int[] r = AxisDirections.indicesOfColinear(sourceCRS.getCoordinateSystem(), targetCRS.getCoordinateSystem());
+                if (r != null) {
+                    for (int j=0; j<r.length; j++) {
+                        if (r[j] != j) {
+                            final int oldRow = r[j] + firstDimension;
+                            final int newRow =   j  + firstDimension;
+                            if (original == null) {
+                                original = reorderGridToCRS.clone();
+                            }
+                            for (int k = original.getNumCol(); --k >= 0;) {
+                                reorderGridToCRS.setElement(newRow, k, original.getElement(oldRow, k));
+                            }
+                        }
+                    }
+                    components[i] = (ProjectedCRS) targetCRS;
+                    continue search;
+                }
+                firstDimension += sourceCRS.getCoordinateSystem().getDimension();
+            }
+            // If a replacement can not be applied, fail CRS construction.
+            // May be relaxed in a future version if we have a use case.
+            throw new DataStoreReferencingException(Resources.format(
+                    Resources.Keys.CanNotInjectComponent_1, IdentifiedObjects.getName(targetCRS, null)));
+        }
     }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
index 4a8bfa8..2cba062 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
@@ -79,6 +79,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CanNotCreateGridGeometry_3 = 12;
 
         /**
+         * Can not inject component “{0}” in the reference system.
+         */
+        public static final short CanNotInjectComponent_1 = 26;
+
+        /**
          * Can not relate dimension “{2}” of variable “{1}” to a coordinate system dimension in netCDF
          * file “{0}”.
          */
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
index 931f669..f3ebbb8 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
@@ -23,6 +23,7 @@ AmbiguousAxisDirection_4          = NetCDF file \u201c{0}\u201d provides an ambi
 CanNotComputeVariablePosition_2   = Can not compute data location for \u201c{1}\u201d variable in the \u201c{0}\u201d netCDF file.
 CanNotCreateCRS_3                 = Can not create the Coordinate Reference System for \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2}
 CanNotCreateGridGeometry_3        = Can not create the grid geometry \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2}
+CanNotInjectComponent_1           = Can not inject component \u201c{0}\u201d in the reference system.
 CanNotRelateVariableDimension_3   = Can not relate dimension \u201c{2}\u201d of variable \u201c{1}\u201d to a coordinate system dimension in netCDF file \u201c{0}\u201d.
 CanNotRender_2                    = Can not render an image for \u201c{0}\u201d. The reason is: {1}
 CanNotSetProjectionParameter_5    = Can not set map projection parameter \u201c{1}\u200b:{2}\u201d = {3} in the \u201c{0}\u201d netCDF file. The reason is: {4}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
index dfb79b6..10add51 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
@@ -28,6 +28,7 @@ AmbiguousAxisDirection_4          = Le fichier netCDF \u00ab\u202f{0}\u202f\u00b
 CanNotComputeVariablePosition_2   = Ne peut pas calculer la position des donn\u00e9es de la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 CanNotCreateCRS_3                 = Ne peut pas cr\u00e9er le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es pour \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {2}
 CanNotCreateGridGeometry_3        = Ne peut pas cr\u00e9er la g\u00e9om\u00e9trie de grille \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {2}
+CanNotInjectComponent_1           = Ne peut pas pas ins\u00e9rer la composante \u00ab\u202f{0}\u202f\u00bb dans le syst\u00e8me de r\u00e9f\u00e9rence.
 CanNotRelateVariableDimension_3   = Ne peut pas relier la dimension \u00ab\u202f{2}\u202f\u00bb de la variable \u00ab\u202f{1}\u202f\u00bb \u00e0 une dimension d\u2019un syst\u00e8me de coordonn\u00e9es du fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 CanNotRender_2                    = Ne peut pas produire une image pour \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {1}
 CanNotSetProjectionParameter_5    = Ne peut pas d\u00e9finir le param\u00e8tre de projection \u00ab\u202f{1}\u200b:{2}\u202f\u00bb = {3} dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {4}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/SatelliteGroundTrack.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/SatelliteGroundTrack.java
deleted file mode 100644
index d6eddf0..0000000
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/SatelliteGroundTrack.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * 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.internal.netcdf;
-
-import org.opengis.util.FactoryException;
-import org.opengis.geometry.DirectPosition;
-import org.opengis.parameter.ParameterDescriptor;
-import org.opengis.parameter.ParameterDescriptorGroup;
-import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.referencing.operation.Matrix;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.MathTransform2D;
-import org.opengis.referencing.operation.MathTransformFactory;
-import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.referencing.operation.transform.AbstractMathTransform2D;
-import org.apache.sis.referencing.operation.transform.ContextualParameters;
-import org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
-import org.apache.sis.referencing.operation.matrix.MatrixSIS;
-import org.apache.sis.referencing.operation.matrix.Matrix2;
-import org.apache.sis.parameter.ParameterBuilder;
-import org.apache.sis.geometry.DirectPosition2D;
-import org.apache.sis.internal.util.DoubleDouble;
-import org.apache.sis.internal.util.Constants;
-import org.apache.sis.math.Vector;
-import org.apache.sis.math.Line;
-
-
-/**
- * An estimation of the position of the satellite for given row and column indices.
- * The calculation done in this class is very rough; the intent is not to give an exact answer,
- * but to convert grid indices to something roughly proportional to latitudes and longitudes
- * in order to make {@link LocalizationGridBuilder} work easier.
- *
- * <p>Current implementation is similar to a <a href="https://en.wikipedia.org/wiki/Sinusoidal_projection">sinusoidal projection</a>
- * in which the central meridian is oblique. That "oblique central meridian" is fitted (by linear regression) to the presumed
- * satellite trajectory. This model may change in any future SIS version.</p>
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-final class SatelliteGroundTrack extends AbstractMathTransform2D {
-    /**
-     * Parameter descriptor for this transform.
-     */
-    private static final ParameterDescriptorGroup PARAMETERS;
-    static {
-        final ParameterBuilder builder = new ParameterBuilder().setRequired(true);
-        final ParameterDescriptor<?>[] grids = new ParameterDescriptor<?>[] {
-            builder.addName(Constants.CENTRAL_MERIDIAN + "_start").create(DirectPosition.class, null),
-            builder.addName(Constants.CENTRAL_MERIDIAN + "_end")  .create(DirectPosition.class, null),
-        };
-        PARAMETERS = builder.addName("Satellite ground track").createGroup(grids);
-    }
-
-    /**
-     * Parameters describing (at least partially) this transform.
-     * They are used for formatting <cite>Well Known Text</cite> (WKT).
-     *
-     * @see #getContextualParameters()
-     */
-    private final ContextualParameters context;
-
-    /**
-     * Terms of the λ = <var>slope</var>⋅φ + λ₀ equation estimating the satellite longitude λ for a latitude φ.
-     */
-    private final double λ0, slope;
-
-    /**
-     * The inverse of this transform.
-     */
-    private final MathTransform2D inverse;
-
-    /**
-     * Creates a new instance of this transform.
-     *
-     * @param grid       localization grid containing longitude and latitude coordinates.
-     * @param lonDim     dimension of the longitude coordinates in the given grid.
-     * @param direction  0 if the ground track is on rows, of 1 if it is on columns.
-     */
-    private SatelliteGroundTrack(final LocalizationGridBuilder grid, final int lonDim, final int direction) throws TransformException {
-        /*
-         * We presume that the row or column in the middle of the localization grid give the
-         * coordinates that are closest to coordinates of the actual satellite ground track.
-         */
-        final int median = (int) grid.getSourceEnvelope(false).getMedian(direction ^ 1);
-        final Vector longitudes, latitudes;
-        if (direction == 0) {
-            longitudes = grid.getRow(lonDim,     median);
-            latitudes  = grid.getRow(lonDim ^ 1, median);
-        } else {
-            longitudes = grid.getColumn(lonDim,     median);
-            latitudes  = grid.getColumn(lonDim ^ 1, median);
-        }
-        final Line line = new Line();
-        line.fit(latitudes, longitudes);
-        λ0      = line.y0();                                // Longitude in degrees.
-        slope   = Math.toRadians(line.slope());             // Take the slope as if all latitudes were given in radians.
-        inverse = new Inverse();
-        context = new ContextualParameters(PARAMETERS, 2, 2);
-        final MatrixSIS normalize = context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
-        normalize.convertAfter(1, DoubleDouble.createDegreesToRadians(), null);
-        setPositionParameter(Constants.CENTRAL_MERIDIAN + "_start", line, latitudes.doubleValue(0));
-        setPositionParameter(Constants.CENTRAL_MERIDIAN + "_end",   line, latitudes.doubleValue(latitudes.size() - 1));
-    }
-
-    /**
-     * Sets the {@link DirectPosition} value of a parameter.
-     */
-    private void setPositionParameter(final String name, final Line line, final double φ) {
-        final DirectPosition2D pos = new DirectPosition2D(φ, line.y(φ));
-        context.parameter(name).setValue(pos);
-    }
-
-    /**
-     * Creates a new instance of this transform, or returns {@code null} if no instance can be created.
-     * The grid is presumed to contains latitude and longitude coordinates in decimal degrees.
-     *
-     * @param factory    the factory to use for creating transforms.
-     * @param grid       localization grid containing longitude and latitude coordinates.
-     * @param lonDim     dimension of the longitude coordinates in the given grid.
-     * @param direction  0 if the ground track is on rows, of 1 if it is on columns.
-     */
-    static MathTransform create(final MathTransformFactory factory, final LocalizationGridBuilder grid,
-            final int lonDim, final int direction) throws TransformException, FactoryException
-    {
-        final SatelliteGroundTrack tr = new SatelliteGroundTrack(grid, lonDim, direction);
-        return tr.context.completeTransform(factory, tr);
-    }
-
-    /**
-     * Returns the parameter values for this math transform.
-     */
-    @Override
-    public ParameterValueGroup getParameterValues() {
-        return context;
-    }
-
-    /**
-     * Returns the parameters used for creating the complete transformation. Those parameters describe a sequence
-     * of <cite>normalize</cite> → {@code this} → <cite>denormalize</cite> transforms.
-     */
-    @Override
-    protected ContextualParameters getContextualParameters() {
-        return context;
-    }
-
-    /**
-     * Converts a single geographic coordinates into something hopefully more proportional to grid indices.
-     * For each coordinate tuple in {@code srcPts}, the first coordinate value is longitude in degrees and
-     * the second value is latitude in <strong>radians</strong>. The conversion from degrees to radians is
-     * done by the concatenated transform.
-     */
-    @Override
-    public Matrix transform(final double[] srcPts, final int srcOff,
-                            final double[] dstPts, final int dstOff,
-                            final boolean derivate) throws TransformException
-    {
-        final double λ    = srcPts[srcOff  ];
-        final double φ    = srcPts[srcOff+1];
-        final double cosφ = Math.cos(φ);
-        final double m    = φ * slope + λ0;                 // Central meridian at the given latitude.
-        final double Δλ   = λ - m;
-        if (dstPts != null) {
-            dstPts[dstOff  ] = Δλ * cosφ + m;               // TODO: use Math.fma with JDK9.
-            dstPts[dstOff+1] = φ;
-        }
-        if (!derivate) {
-            return null;
-        }
-        final Matrix2 d = new Matrix2();
-        d.m00 = cosφ;
-        d.m01 = slope * (1 - cosφ) - Δλ * Math.sin(φ);
-        return d;
-    }
-
-    /**
-     * Returns the inverse of this transform.
-     */
-    @Override
-    public MathTransform2D inverse() {
-        return inverse;
-    }
-
-    /**
-     * The inverse of {@link SatelliteGroundTrack} transform.
-     */
-    private final class Inverse extends AbstractMathTransform2D.Inverse {
-        /**
-         * Creates a new instance of the inverse transform.
-         */
-        Inverse() {
-        }
-
-        /**
-         * Returns the inverse of this transform, which is the enclosing {@link SatelliteGroundTrack} transform.
-         */
-        @Override
-        public MathTransform2D inverse() {
-            return SatelliteGroundTrack.this;
-        }
-
-        /**
-         * Converts grid indices to geographic coordinates (not necessarily in degrees units).
-         * See {@link SatelliteGroundTrack#transform(double[], int, double[], int, boolean)}
-         * for the units of measurement.
-         */
-        @Override
-        public Matrix transform(final double[] srcPts, final int srcOff,
-                                final double[] dstPts, final int dstOff,
-                                final boolean derivate) throws TransformException
-        {
-            final double x    = srcPts[srcOff  ];
-            final double φ    = srcPts[srcOff+1];
-            final double cosφ = Math.cos(φ);
-            final double m    = φ * slope + λ0;                 // Central meridian at the given latitude.
-            final double Δx   = x - m;
-            if (dstPts != null) {
-                dstPts[dstOff  ] = Δx / cosφ + m;
-                dstPts[dstOff+1] = φ;
-            }
-            if (!derivate) {
-                return null;
-            }
-            final Matrix2 d = new Matrix2();
-            d.m00 = 1 / cosφ;
-            d.m01 = (Δx * Math.sin(φ) / cosφ - slope) / cosφ + slope;
-            return d;
-        }
-    }
-}
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/SatelliteGroundTrackTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/SatelliteGroundTrackTest.java
deleted file mode 100644
index fd39239..0000000
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/SatelliteGroundTrackTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.internal.netcdf;
-
-import java.awt.geom.AffineTransform;
-import org.opengis.util.FactoryException;
-import org.opengis.referencing.operation.TransformException;
-import org.opengis.referencing.operation.MathTransformFactory;
-import org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
-import org.apache.sis.referencing.operation.transform.MathTransformTestCase;
-import org.apache.sis.referencing.operation.transform.CoordinateDomain;
-import org.apache.sis.internal.system.DefaultFactories;
-import org.junit.Test;
-
-
-/**
- * Tests {@link SatelliteGroundTrack}. There is no external data that we can use as a reference.
- * Consequently this test merely verifies that {@link SatelliteGroundTrack} is self-consistent.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-public final strictfp class SatelliteGroundTrackTest extends MathTransformTestCase {
-    /**
-     * Size of the grid created for testing purpose.
-     */
-    private static final int WIDTH = 20, HEIGHT = 20;
-
-    /**
-     * Creates a transform for a grid of fixed size in a geographic domain.
-     */
-    private void createTransform() throws TransformException, FactoryException {
-        final LocalizationGridBuilder grid = new LocalizationGridBuilder(WIDTH, HEIGHT);
-        final AffineTransform tr = AffineTransform.getRotateInstance(StrictMath.toRadians(31));
-        tr.translate(-WIDTH / 2, -HEIGHT / 2);
-        final double[] point = new double[2];
-        for (int y=0; y<HEIGHT; y++) {
-            for (int x=0; x<WIDTH; x++) {
-                point[0] = x;
-                point[1] = y;
-                tr.transform(point, 0, point, 0, 1);
-                grid.setControlPoint(x, y, point);
-            }
-        }
-        transform = SatelliteGroundTrack.create(DefaultFactories.forBuildin(MathTransformFactory.class), grid, 1, 1);
-        tolerance = 1E-12;
-        derivativeDeltas = new double[] {0.1, 0.1};
-    }
-
-    /**
-     * Tests self-consistency at random points.
-     *
-     * @throws FactoryException if an error occurred while creating the transform.
-     * @throws TransformException if an error occurred while transforming a point.
-     */
-    @Test
-    public void testConsistency() throws TransformException, FactoryException {
-        createTransform();
-        validate();
-        verifyInDomain(CoordinateDomain.RANGE_10, -979924465940961910L);
-        transform = transform.inverse();
-        validate();
-        verifyInDomain(CoordinateDomain.RANGE_10, -2122465178330330413L);
-    }
-}
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/test/suite/NetcdfTestSuite.java b/storage/sis-netcdf/src/test/java/org/apache/sis/test/suite/NetcdfTestSuite.java
index 64c89c2..b821ba5 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/test/suite/NetcdfTestSuite.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/test/suite/NetcdfTestSuite.java
@@ -35,7 +35,6 @@ import org.junit.BeforeClass;
     org.apache.sis.internal.netcdf.VariableTest.class,
     org.apache.sis.internal.netcdf.AxisTest.class,
     org.apache.sis.internal.netcdf.GridTest.class,
-    org.apache.sis.internal.netcdf.SatelliteGroundTrackTest.class,
     org.apache.sis.internal.netcdf.impl.ChannelDecoderTest.class,
     org.apache.sis.internal.netcdf.impl.VariableInfoTest.class,
     org.apache.sis.internal.netcdf.impl.GridInfoTest.class,


Mime
View raw message