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 <[email protected]>
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,

Reply via email to