You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2019/04/13 16:07:35 UTC

[sis] branch geoapi-4.0 updated: Refactor the way we use ParameterDescriptorGroup, ParameterValueGroup and "grid to target" transform in GridDatumShift in such a way that we can keep GridDatumShiftFile internal to NADCON and NTv2 transformations. This avoid the confusion declaration of ResidualGrid as a subtype of DatumShiftGridFile. Also moved the DatumShiftGrid.normalizedToGridX/Y methods as package-private methods in DatumShiftTransform; those methods should never have been public, since their relationship with `coordi [...]

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 69e3bec  Refactor the way we use ParameterDescriptorGroup, ParameterValueGroup and "grid to target" transform in GridDatumShift in such a way that we can keep GridDatumShiftFile internal to NADCON and NTv2 transformations. This avoid the confusion declaration of ResidualGrid as a subtype of DatumShiftGridFile. Also moved the DatumShiftGrid.normalizedToGridX/Y methods as package-private methods in DatumShiftTransform; those methods should never have been public, since their relati [...]
69e3bec is described below

commit 69e3bec57ac878fa8ebb39f838281a976e6834a5
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Apr 13 18:03:23 2019 +0200

    Refactor the way we use ParameterDescriptorGroup, ParameterValueGroup and "grid to target" transform in GridDatumShift in such a way that we can keep GridDatumShiftFile internal to NADCON and NTv2 transformations. This avoid the confusion declaration of ResidualGrid as a subtype of DatumShiftGridFile.
    Also moved the DatumShiftGrid.normalizedToGridX/Y methods as package-private methods in DatumShiftTransform; those methods should never have been public, since their relationship with `coordinateToGrid` conversion can be confusing.
---
 .../provider/DatumShiftGridCompressed.java         |   4 +-
 .../referencing/provider/DatumShiftGridFile.java   | 126 ++++--------
 .../referencing/provider/DatumShiftGridLoader.java |   4 +-
 .../provider/FranceGeocentricInterpolation.java    |   2 +-
 .../sis/internal/referencing/provider/NADCON.java  |   4 +-
 .../sis/internal/referencing/provider/NTv2.java    |   2 +-
 .../sis/referencing/datum/DatumShiftGrid.java      | 212 ++++++++++-----------
 .../operation/builder/ResidualGrid.java            | 141 +++++++++-----
 .../operation/transform/DatumShiftTransform.java   | 103 +++++++++-
 .../transform/InterpolatedGeocentricTransform.java |  15 +-
 .../transform/InterpolatedMolodenskyTransform.java |  15 +-
 .../operation/transform/InterpolatedTransform.java |  30 +--
 .../operation/transform/MolodenskyFormula.java     |   4 +-
 .../operation/builder/ResidualGridTest.java        |   2 +-
 .../operation/transform/QuadraticShiftGrid.java    |  20 ++
 .../java/org/apache/sis/internal/util/Strings.java |  28 ++-
 16 files changed, 408 insertions(+), 304 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridCompressed.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridCompressed.java
index 97cef2e..4eee2d8 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridCompressed.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridCompressed.java
@@ -199,7 +199,7 @@ final class DatumShiftGridCompressed<C extends Quantity<C>, T extends Quantity<T
         final int p00 = nx*iy + ix;
         final int p10 = nx + p00;
         final int n   = data.length;
-        boolean derivative = (vector.length >= n + 4);
+        boolean derivative = (vector.length >= n + INTERPOLATED_DIMENSIONS*INTERPOLATED_DIMENSIONS);
         for (int dim = 0; dim < n; dim++) {
             double dx;
             final short[] values = data[dim];
@@ -218,7 +218,7 @@ final class DatumShiftGridCompressed<C extends Quantity<C>, T extends Quantity<T
                     dx++;
                 } else {
                     dy++;
-                    i += 2;
+                    i += INTERPOLATED_DIMENSIONS;
                     derivative = false;
                 }
                 vector[i  ] = dx;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java
index 8953a92..ebc74ee 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java
@@ -24,33 +24,21 @@ import javax.measure.Quantity;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.GeneralParameterDescriptor;
-import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.util.collection.Cache;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.referencing.datum.DatumShiftGrid;
-import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
-import org.apache.sis.internal.util.Strings;
 
 
 /**
  * A datum shift grid loaded from a file.
  * The filename is usually a parameter defined in the EPSG database.
  * This class should not be in public API because it requires implementation to expose internal mechanic:
- *
- * <ul>
- *   <li>Subclasses need to give an access to their internal data (not a copy) through the {@link #getData()}
- *       and {@link #setData(Object[])} methods. We use that for managing the cache, reducing memory usage by
- *       sharing data and for {@link #equals(Object)} and {@link #hashCode()} implementations.</li>
- *   <li>{@link #descriptor}, {@link #gridToTarget()} and {@link #setGridParameters(Parameters)} are convenience
- *       members for {@link org.apache.sis.referencing.operation.transform.InterpolatedTransform} constructor.
- *       What they do are closely related to how {@code InterpolatedTransform} works, and trying to document that
- *       in a public API would probably be too distracting for the users.</li>
- * </ul>
- *
- * The main concrete subclass is {@link DatumShiftGridFile.Float}.
+ * Subclasses need to give an access to their internal data (not a copy) through the {@link #getData()}
+ * and {@link #setData(Object[])} methods. We use that for managing the cache, reducing memory usage by
+ * sharing data and for {@link #equals(Object)} and {@link #hashCode()} implementations.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
@@ -64,7 +52,7 @@ import org.apache.sis.internal.util.Strings;
  * @since 0.7
  * @module
  */
-public abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>> extends DatumShiftGrid<C,T> {
+abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>> extends DatumShiftGrid<C,T> {
     /**
      * Serial number for inter-operability with different versions.
      */
@@ -88,18 +76,18 @@ public abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quanti
     /**
      * The parameter descriptor of the provider that created this grid.
      */
-    public final ParameterDescriptorGroup descriptor;
+    private final ParameterDescriptorGroup descriptor;
 
     /**
      * The files from which the grid has been loaded. This is not used directly by this class
      * (except for {@link #equals(Object)} and {@link #hashCode()}), but can be used by math
-     * transform for setting the parameter values. Never empty but may be null if the grid is
-     * computed instead than loaded from file(s).
+     * transform for setting the parameter values. Shall never be null and never empty.
      */
     private final Path[] files;
 
     /**
      * Number of grid cells along the <var>x</var> axis.
+     * This is <code>{@linkplain #getGridSize()}[0]</code> as a field for performance reasons.
      */
     protected final int nx;
 
@@ -112,48 +100,23 @@ public abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quanti
      *
      * @see #getCellPrecision()
      */
-    protected double accuracy;
+    double accuracy;
 
     /**
      * Creates a new datum shift grid for the given grid geometry.
      * The actual offset values need to be provided by subclasses.
      *
-     * @param  coordinateUnit    the unit of measurement of input values, before conversion to grid indices by {@code coordinateToGrid}.
-     * @param  translationUnit   the unit of measurement of output values.
-     * @param  isCellValueRatio  {@code true} if results of {@link #interpolateInCell interpolateInCell(…)} are divided by grid cell size.
-     * @param  coordinateToGrid  conversion from the "real world" coordinates to grid indices including fractional parts.
-     * @param  nx                number of cells along the <var>x</var> axis in the grid.
-     * @param  ny                number of cells along the <var>y</var> axis in the grid.
-     * @param  descriptor        the parameter descriptor of the provider that created this grid.
-     * @param  files             the file(s) from which the grid has been loaded.
-     *
-     * @since 0.8
-     */
-    protected DatumShiftGridFile(final Unit<C> coordinateUnit,
-                                 final Unit<T> translationUnit,
-                                 final boolean isCellValueRatio,
-                                 final LinearTransform coordinateToGrid,
-                                 final int nx, final int ny,
-                                 final ParameterDescriptorGroup descriptor,
-                                 final Path... files)
-    {
-        super(coordinateUnit, coordinateToGrid, new int[] {nx, ny}, isCellValueRatio, translationUnit);
-        this.descriptor = descriptor;
-        this.files      = (files.length != 0) ? files : null;
-        this.nx         = nx;
-        this.accuracy   = Double.NaN;
-    }
-
-    /**
-     * Creates a new datum shift grid for the given grid geometry.
-     * The actual offset values need to be provided by subclasses.
-     *
-     * @param x0  longitude in degrees of the center of the cell at grid index (0,0).
-     * @param y0  latitude in degrees of the center of the cell at grid index (0,0).
-     * @param Δx  increment in <var>x</var> value between cells at index <var>gridX</var> and <var>gridX</var> + 1.
-     * @param Δy  increment in <var>y</var> value between cells at index <var>gridY</var> and <var>gridY</var> + 1.
-     * @param nx  number of cells along the <var>x</var> axis in the grid.
-     * @param ny  number of cells along the <var>y</var> axis in the grid.
+     * @param coordinateUnit    the unit of measurement of input values, before conversion to grid indices by {@code coordinateToGrid}.
+     * @param translationUnit   the unit of measurement of output values.
+     * @param isCellValueRatio  {@code true} if results of {@link #interpolateInCell interpolateInCell(…)} are divided by grid cell size.
+     * @param x0                longitude in degrees of the center of the cell at grid index (0,0).
+     * @param y0                latitude in degrees of the center of the cell at grid index (0,0).
+     * @param Δx                increment in <var>x</var> value between cells at index <var>gridX</var> and <var>gridX</var> + 1.
+     * @param Δy                increment in <var>y</var> value between cells at index <var>gridY</var> and <var>gridY</var> + 1.
+     * @param nx                number of cells along the <var>x</var> axis in the grid.
+     * @param ny                number of cells along the <var>y</var> axis in the grid.
+     * @param descriptor        the parameter descriptor of the provider that created this grid.
+     * @param files             the file(s) from which the grid has been loaded. This array is not cloned.
      */
     DatumShiftGridFile(final Unit<C> coordinateUnit,
                        final Unit<T> translationUnit,
@@ -164,8 +127,12 @@ public abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quanti
                        final ParameterDescriptorGroup descriptor,
                        final Path... files) throws NoninvertibleTransformException
     {
-        this(coordinateUnit, translationUnit, isCellValueRatio,
-                new AffineTransform2D(Δx, 0, 0, Δy, x0, y0).inverse(), nx, ny, descriptor, files);
+        super(coordinateUnit, new AffineTransform2D(Δx, 0, 0, Δy, x0, y0).inverse(),
+              new int[] {nx, ny}, isCellValueRatio, translationUnit);
+        this.descriptor = descriptor;
+        this.files      = files;
+        this.nx         = nx;
+        this.accuracy   = Double.NaN;
     }
 
     /**
@@ -248,17 +215,13 @@ public abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quanti
     }
 
     /**
-     * Returns the transform from grid coordinates to "real world" coordinates after the datum shift has been applied,
-     * or {@code null} for the default. This is usually the inverse of the transform from "real world" coordinates to
-     * grid coordinates before datum shift, since NADCON and NTv2 transformations have source and target coordinates
-     * in the same coordinate system (with axis units in degrees). But this method may be overridden by subclasses that
-     * use {@code DatumShiftGridFile} for other kind of transformations.
+     * Returns the descriptor specified at construction time.
      *
-     * @return the transformation from grid coordinates to "real world" coordinates after datum shift,
-     *         or {@code null} for the default (namely the inverse of the "source to grid" transformation).
+     * @return a description of the values in this grid.
      */
-    public Matrix gridToTarget() {
-        return null;
+    @Override
+    public final ParameterDescriptorGroup getParameterDescriptors() {
+        return descriptor;
     }
 
     /**
@@ -267,16 +230,15 @@ public abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quanti
      *
      * @param  parameters  the parameter group where to set the values.
      */
-    public void setGridParameters(final Parameters parameters) {
-        if (files != null) {
-            int i = 0;
-            for (final GeneralParameterDescriptor gd : descriptor.descriptors()) {
-                if (gd instanceof ParameterDescriptor<?>) {
-                    final ParameterDescriptor<?> d = (ParameterDescriptor<?>) gd;
-                    if (Path.class.isAssignableFrom(d.getValueClass())) {
-                        parameters.getOrCreate(d).setValue(files[i]);
-                        if (++i == files.length) break;
-                    }
+    @Override
+    public final void getParameterValues(final Parameters parameters) {
+        int i = 0;
+        for (final GeneralParameterDescriptor gd : descriptor.descriptors()) {
+            if (gd instanceof ParameterDescriptor<?>) {
+                final ParameterDescriptor<?> d = (ParameterDescriptor<?>) gd;
+                if (Path.class.isAssignableFrom(d.getValueClass())) {
+                    if (i >= files.length) break;                               // Safety in case of invalid parameters.
+                    parameters.getOrCreate(d).setValue(files[i++]);
                 }
             }
         }
@@ -311,16 +273,6 @@ public abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quanti
         return super.hashCode() + Arrays.hashCode(files);
     }
 
-    /**
-     * Returns a string representation of this grid.
-     *
-     * @return a string representation for debugging purpose.
-     */
-    @Override
-    public String toString() {
-        return Strings.toString(getClass(), "file", (files != null) ? files[0] : null);
-    }
-
 
 
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
index 7166426..f701d51 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
@@ -65,7 +65,7 @@ class DatumShiftGridLoader {
     static final double SECOND_PRECISION = 1E-4;
 
     /**
-     * The file to load, used only if we have errors to report.
+     * The file to load, used for parameter declaration and if we have errors to report.
      */
     final Path file;
 
@@ -84,7 +84,7 @@ class DatumShiftGridLoader {
      *
      * @param  channel  where to read data from.
      * @param  buffer   the buffer to use.
-     * @param  file     path to the longitude or latitude difference file. Used only for error reporting.
+     * @param  file     path to the longitude or latitude difference file. Used for parameter declaration and error reporting.
      */
     DatumShiftGridLoader(final ReadableByteChannel channel, final ByteBuffer buffer, final Path file) throws IOException {
         this.file    = file;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
index 347c78e..32a0734 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
@@ -350,7 +350,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
      * Unconditionally loads the grid for the given file without in-memory compression.
      *
      * @param  in    reader of the RGF93 datum shift file.
-     * @param  file  path to the file being read, used only for error reporting.
+     * @param  file  path to the file being read, used for parameter declaration and error reporting.
      * @throws IOException if an I/O error occurred.
      * @throws NumberFormatException if a number can not be parsed.
      * @throws NoSuchElementException if a data line is missing a value.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
index 860e45c..3503b1c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
@@ -262,7 +262,7 @@ public final class NADCON extends AbstractProvider {
          * @param channel  where to read data from.
          * @param buffer   the buffer to use. That buffer must use little endian byte order
          *                 and have a capacity divisible by the size of the {@code float} type.
-         * @param file     path to the longitude or latitude difference file. Used only for error reporting.
+         * @param file     path to the longitude or latitude difference file. Used for parameter declaration and error reporting.
          */
         Loader(final ReadableByteChannel channel, final ByteBuffer buffer, final Path file)
                 throws IOException, FactoryException
@@ -376,7 +376,7 @@ public final class NADCON extends AbstractProvider {
          *
          * @param fb               a {@code FloatBuffer} view over the full {@link #buffer} range.
          * @param latitudeShifts   the previously loaded latitude shifts, or {@code null} if not yet loaded.
-         * @param longitudeShifts  the file for the longitude grid, or {@code null} if identical to {@link #file}.
+         * @param longitudeShifts  the file for the longitude grid.
          */
         final void readGrid(final FloatBuffer fb, final Loader latitudeShifts, final Path longitudeShifts)
                 throws IOException, FactoryException, NoninvertibleTransformException
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
index f6bdf37..ba3c018 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
@@ -247,7 +247,7 @@ public final class NTv2 extends AbstractProvider {
          * This constructor parses the header immediately, but does not read any grid.
          *
          * @param  channel  where to read data from.
-         * @param  file     path to the longitude and latitude difference file. Used only for error reporting.
+         * @param  file     path to the longitude and latitude difference file. Used for parameter declaration and error reporting.
          * @throws FactoryException if a data record can not be parsed.
          */
         Loader(final ReadableByteChannel channel, final Path file) throws IOException, FactoryException {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
index 1c3cdd3..a34d548 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
@@ -18,18 +18,18 @@ package org.apache.sis.referencing.datum;
 
 import java.util.Arrays;
 import java.util.Objects;
-import java.io.IOException;
 import java.io.Serializable;
-import java.io.ObjectInputStream;
 import javax.measure.Unit;
 import javax.measure.Quantity;
 import org.opengis.geometry.Envelope;
+import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.parameter.Parameters;
 import org.apache.sis.internal.util.DoubleDouble;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.util.resources.Errors;
@@ -41,7 +41,7 @@ import org.apache.sis.measure.Units;
  * Small but non-constant translations to apply on coordinates for datum shifts or other transformation process.
  * The main purpose of this class is to encapsulate the data provided by <cite>datum shift grid files</cite>
  * like NTv2, NADCON or RGF93. But this class could also be used for other kind of transformations,
- * provided that the shifts are <strong>small</strong> (otherwise algorithms may not converge).
+ * provided that the shifts are relatively small (otherwise algorithms may not converge).
  *
  * <p>{@linkplain DefaultGeodeticDatum Geodetic datum} changes can be implemented by translations in geographic
  * or geocentric coordinates. Translations given by {@code DatumShiftGrid} instances are often, but not always,
@@ -101,13 +101,14 @@ import org.apache.sis.measure.Units;
  * <div class="section">Number of dimensions</div>
  * Input coordinates and translation vectors can have any number of dimensions. However in the current implementation,
  * only the two first dimensions are used for interpolating the translation vectors. This restriction appears in the
- * following method signatures:
+ * following field and method signatures:
  *
  * <ul>
- *   <li>{@link #interpolateInCell(double, double, double[])}
- *       where the two first {@code double} values are (<var>x</var>,<var>y</var>) grid indices.</li>
+ *   <li>{@link #INTERPOLATED_DIMENSIONS}.</li>
  *   <li>{@link #getCellValue(int, int, int)}
  *       where the two last {@code int} values are (<var>x</var>,<var>y</var>) grid indices.</li>
+ *   <li>{@link #interpolateInCell(double, double, double[])}
+ *       where the two first {@code double} values are (<var>x</var>,<var>y</var>) grid indices.</li>
  *   <li>{@link #derivativeInCell(double, double)}
  *       where the values are (<var>x</var>,<var>y</var>) grid indices.</li>
  * </ul>
@@ -141,10 +142,22 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
     private static final long serialVersionUID = 8405276545243175808L;
 
     /**
-     * Number of dimensions in which interpolations are applied. The grid may have more dimensions,
-     * but only this number of dimensions will be used in interpolations.
+     * Number of source dimensions in which interpolations are applied. The grids may have more dimensions,
+     * but only this number of dimensions will be used in interpolations. The value of this field is set to
+     * {@value}. That value is hard-coded not only in this field, but also in signature of various methods
+     * expecting a two-dimensional (<var>x</var>, <var>y</var>) position:
+     * <code>{@linkplain #getCellValue(int, int, int) getCellValue}(…, x, y)</code>,
+     * <code>{@linkplain #interpolateInCell(double, double, double[]) interpolateInCell}(x, y, …)</code>,
+     * <code>{@linkplain #derivativeInCell(double, double) derivativeInCell}(x, y)</code>.
+     *
+     * <div class="note"><b>Future evolution:</b>
+     * if this class is generalized to more source dimensions in a future Apache SIS version, then this field
+     * may be deprecated or its value changed. That change would be accompanied by new methods with different
+     * signature. This field can be used as a way to detect that such change occurred.</div>
+     *
+     * @since 1.0
      */
-    private static final int INTERPOLATED_DIMENSIONS = 2;
+    protected static final int INTERPOLATED_DIMENSIONS = 2;
 
     /**
      * The unit of measurements of input values, before conversion to grid indices by {@link #coordinateToGrid}.
@@ -186,18 +199,6 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
     private final int[] gridSize;
 
     /**
-     * Conversion from (λ,φ) coordinates in radians to grid indices (x,y).
-     *
-     * <ul>
-     *   <li>x  =  (λ - λ₀) ⋅ {@code scaleX}  =  λ ⋅ {@code scaleX} + x₀</li>
-     *   <li>y  =  (φ - φ₀) ⋅ {@code scaleY}  =  φ ⋅ {@code scaleY} + y₀</li>
-     * </ul>
-     *
-     * Those factors are extracted from the {@link #coordinateToGrid} transform for performance purposes.
-     */
-    private transient double scaleX, scaleY, x0, y0;
-
-    /**
      * Creates a new datum shift grid for the given size and units.
      * The actual cell values need to be provided by subclasses.
      *
@@ -233,49 +234,6 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
                         : Errors.Keys.IllegalArgumentValue_2, Strings.toIndexed("gridSize", i), n));
             }
         }
-        computeConversionFactors();
-    }
-
-    /**
-     * Computes the conversion factors needed by {@link #interpolateInCell(double, double, double[])}.
-     * This method takes only the {@value #INTERPOLATED_DIMENSIONS} first dimensions. If a conversion
-     * factor can not be computed, then it is set to NaN.
-     */
-    @SuppressWarnings("fallthrough")
-    private void computeConversionFactors() {
-        scaleX = Double.NaN;
-        scaleY = Double.NaN;
-        x0     = Double.NaN;
-        y0     = Double.NaN;
-        final double toStandardUnit = Units.toStandardUnit(coordinateUnit);
-        if (!Double.isNaN(toStandardUnit)) {
-            final Matrix m = coordinateToGrid.getMatrix();
-            if (Matrices.isAffine(m)) {
-                final int n = m.getNumCol() - 1;
-                switch (m.getNumRow()) {
-                    default: y0 = m.getElement(1,n); scaleY = diagonal(m, 1, n) / toStandardUnit;   // Fall through
-                    case 1:  x0 = m.getElement(0,n); scaleX = diagonal(m, 0, n) / toStandardUnit;
-                    case 0:  break;
-                }
-            }
-        }
-    }
-
-    /**
-     * Returns the value on the diagonal of the given matrix, provided that all other non-translation terms are 0.
-     *
-     * @param  m  the matrix from which to get the scale factor on a row.
-     * @param  j  the row for which to get the scale factor.
-     * @param  n  index of the last column.
-     * @return the scale factor on the diagonal, or NaN.
-     */
-    private static double diagonal(final Matrix m, final int j, int n) {
-        while (--n >= 0) {
-            if (j != n && m.getElement(j, n) != 0) {
-                return Double.NaN;
-            }
-        }
-        return m.getElement(j, j);
     }
 
     /**
@@ -290,15 +248,15 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
         isCellValueRatio = other.isCellValueRatio;
         translationUnit  = other.translationUnit;
         gridSize         = other.gridSize;
-        scaleX           = other.scaleX;
-        scaleY           = other.scaleY;
-        x0               = other.x0;
-        y0               = other.y0;
     }
 
     /**
      * Returns the number of cells along each axis in the grid.
-     * The length of this array is equal to {@code coordinateToGrid} target dimensions.
+     * The length of this array is the number of grid dimensions, which is typically {@value #INTERPOLATED_DIMENSIONS}.
+     * The grid dimensions shall be equal to {@link #getCoordinateToGrid() coordinateToGrid} target dimensions.
+     *
+     * <div class="note"><b>Note:</b> the number of grid dimensions is not necessarily equal to the
+     * {@linkplain #getTranslationDimensions() number of dimension of the translation vectors}.</div>
      *
      * @return the number of cells along each axis in the grid.
      */
@@ -339,10 +297,11 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
     }
 
     /**
-     * Conversion from the "real world" coordinates to grid indices including fractional parts.
-     * The input points given to the {@code MathTransform} shall be in the unit of measurement
-     * given by {@link #getCoordinateUnit()}.
-     * The output points are grid indices with integer values in the center of grid cells.
+     * Returns the conversion from the source coordinates (in "real world" units) to grid indices.
+     * The input coordinates given to the {@link LinearTransform} shall be in the unit of measurement
+     * given by {@link #getCoordinateUnit()}. The output coordinates are grid indices as real numbers
+     * (i.e. can have a fractional part). Integer grid indices are located in the center of grid cells,
+     * i.e. the transform uses {@link org.opengis.referencing.datum.PixelInCell#CELL_CENTER} convention.
      *
      * <p>This transform is usually two-dimensional, in which case conversions from (<var>x</var>,<var>y</var>)
      * coordinates to ({@code gridX}, {@code gridY}) indices can be done with the following formulas:</p>
@@ -375,30 +334,6 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
     }
 
     /**
-     * Converts the given normalized <var>x</var> coordinate to grid index.
-     * "Normalized coordinates" are coordinates in the unit of measurement given by {@link Unit#getSystemUnit()}.
-     * For angular coordinates, this is radians. For linear coordinates, this is metres.
-     *
-     * @param  x  the "real world" coordinate (often longitude in radians) of the point for which to get the translation.
-     * @return the grid index for the given coordinate. May be out of bounds.
-     */
-    public final double normalizedToGridX(final double x) {
-        return x * scaleX + x0;
-    }
-
-    /**
-     * Converts the given normalized <var>x</var> coordinate to grid index.
-     * "Normalized coordinates" are coordinates in the unit of measurement given by {@link Unit#getSystemUnit()}.
-     * For angular coordinates, this is radians. For linear coordinates, this is metres.
-     *
-     * @param  y  the "real world" coordinate (often latitude in radians) of the point for which to get the translation.
-     * @return the grid index for the given coordinate. May be out of bounds.
-     */
-    public final double normalizedToGridY(final double y) {
-        return y * scaleY + y0;
-    }
-
-    /**
      * Returns the number of dimensions of the translation vectors interpolated by this datum shift grid.
      * This number of dimensions is not necessarily equals to the number of source or target dimensions
      * of the "{@linkplain #getCoordinateToGrid() coordinate to grid}" transform.
@@ -688,7 +623,78 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
     }
 
     /**
+     * Returns a description of the values in this grid. Grid values may be given directly as matrices or tensors,
+     * or indirectly as name of file from which data were loaded. If grid values are given directly, then:
+     *
+     * <ul>
+     *   <li>The number of {@linkplain #getGridSize() grid} dimensions determines the parameter type:
+     *       one-dimensional grids are represented by {@link org.apache.sis.math.Vector} instances,
+     *       two-dimensional grids are represented by {@link Matrix} instances,
+     *       and grids with more than {@value #INTERPOLATED_DIMENSIONS} are represented by tensors.</li>
+     *   <li>The {@linkplain #getTranslationDimensions() number of dimensions of translation vectors}
+     *       determines how many matrix or tensor parameters appear.</li>
+     * </ul>
+     *
+     * <div class="note"><b>Example 1:</b>
+     * if this {@code DatumShiftGrid} instance has been created for performing NADCON datum shifts,
+     * then this method returns a group named "NADCON" with two parameters:
+     * <ul>
+     *   <li>A parameter of type {@link java.nio.file.Path} named “Latitude difference file”.</li>
+     *   <li>A parameter of type {@link java.nio.file.Path} named “Longitude difference file”.</li>
+     * </ul></div>
+     *
+     * <div class="note"><b>Example 2:</b>
+     * if this {@code DatumShiftGrid} instance has been created by
+     * {@link org.apache.sis.referencing.operation.builder.LocalizationGridBuilder},
+     * then this method returns a group named "Localization grid" with four parameters:
+     * <ul>
+     *   <li>A parameter of type {@link Integer} named “num_row” for the number of rows in each matrix.</li>
+     *   <li>A parameter of type {@link Integer} named “num_col” for the number of columns in each matrix.</li>
+     *   <li>A parameter of type {@link Matrix} named “grid_x”.</li>
+     *   <li>A parameter of type {@link Matrix} named “grid_y”.</li>
+     * </ul></div>
+     *
+     * @return a description of the values in this grid.
+     *
+     * @since 1.0
+     */
+    public abstract ParameterDescriptorGroup getParameterDescriptors();
+
+    /**
+     * Gets the parameter values for the grids and stores them in the provided {@code parameters} group.
+     * The given {@code parameters} must have the descriptor returned by {@link #getParameterDescriptors()}.
+     * The matrices, tensors or file names are stored in the given {@code parameters} instance.
+     *
+     * <div class="note"><b>Implementation note:</b>
+     * this method is invoked by {@link org.apache.sis.referencing.operation.transform.InterpolatedTransform}
+     * and other transforms for initializing the values of their parameter group.</div>
+     *
+     * @param  parameters  the parameter group where to set the values.
+     *
+     * @since 1.0
+     */
+    public abstract void getParameterValues(Parameters parameters);
+
+    /**
+     * Returns a string representation of this {@code DatumShiftGrid}. The default implementation
+     * formats the {@linkplain #getParameterValues(Parameters) parameter values}.
+     *
+     * @return a string representation of the grid parameters.
+     *
+     * @since 1.0
+     */
+    @Override
+    public String toString() {
+        final Parameters p = Parameters.castOrWrap(getParameterDescriptors().createValue());
+        getParameterValues(p);
+        return p.toString();
+    }
+
+    /**
      * Returns {@code true} if the given object is a grid containing the same data than this grid.
+     * Default implementation compares only the properties known to this abstract class like
+     * {@linkplain #getGridSize() grid size}, {@linkplain #getCoordinateUnit() coordinate unit}, <i>etc.</i>
+     * Subclasses need to override for adding comparison of the actual values.
      *
      * @param  other  the other object to compare with this datum shift grid.
      * @return {@code true} if the given object is non-null, of the same class than this {@code DatumShiftGrid}
@@ -718,16 +724,4 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
     public int hashCode() {
         return Objects.hashCode(coordinateToGrid) + 37 * Arrays.hashCode(gridSize);
     }
-
-    /**
-     * Invoked after deserialization. This method computes the transient fields.
-     *
-     * @param  in  the input stream from which to deserialize the datum shift grid.
-     * @throws IOException if an I/O error occurred while reading or if the stream contains invalid data.
-     * @throws ClassNotFoundException if the class serialized on the stream is not on the classpath.
-     */
-    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
-        in.defaultReadObject();
-        computeConversionFactors();
-    }
 }
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 4e27f04..4cf8672 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
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.referencing.operation.builder;
 
+import java.util.Arrays;
 import java.util.function.Function;
 import javax.measure.quantity.Dimensionless;
 import org.opengis.parameter.ParameterDescriptor;
@@ -23,10 +24,10 @@ import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.parameter.ParameterBuilder;
-import org.apache.sis.referencing.operation.matrix.Matrix3;
+import org.apache.sis.referencing.datum.DatumShiftGrid;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.ContextualParameters;
-import org.apache.sis.internal.referencing.provider.DatumShiftGridFile;
 import org.apache.sis.internal.referencing.WKTUtilities;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.Numerics;
@@ -45,27 +46,29 @@ import org.apache.sis.measure.Units;
  * @since   0.8
  * @module
  */
-final class ResidualGrid extends DatumShiftGridFile<Dimensionless,Dimensionless> {
+final class ResidualGrid extends DatumShiftGrid<Dimensionless,Dimensionless> {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = 1445697681304159019L;
+    private static final long serialVersionUID = -713276314000661839L;
 
     /**
      * Number of source dimensions of the residual grid.
+     *
+     * @see #INTERPOLATED_DIMENSIONS
      */
     static final int SOURCE_DIMENSION = 2;
 
     /**
      * The parameter descriptors for the "Localization grid" operation.
-     * Current implementation is fixed to 2 dimensions. This is not a committed set of parameters and they
-     * may change in any future SIS version. We define them mostly for {@code toString()} implementation.
+     * Current implementation is fixed to {@value #SOURCE_DIMENSION} dimensions.
+     *
+     * @see #getParameterDescriptors()
      */
     private static final ParameterDescriptorGroup PARAMETERS;
     static {
         final ParameterBuilder builder = new ParameterBuilder().setRequired(true);
-        @SuppressWarnings("rawtypes")
-        final ParameterDescriptor<?>[] grids = new ParameterDescriptor[] {
+        final ParameterDescriptor<?>[] grids = new ParameterDescriptor<?>[] {
             builder.addName(Constants.NUM_ROW).createBounded(Integer.class, 2, null, null),
             builder.addName(Constants.NUM_COL).createBounded(Integer.class, 2, null, null),
             builder.addName("grid_x").create(Matrix.class, null),
@@ -80,13 +83,17 @@ final class ResidualGrid extends DatumShiftGridFile<Dimensionless,Dimensionless>
      * This method sets the matrix parameters using views over the {@link #offsets} array.
      */
     @Override
-    public void setGridParameters(final Parameters parameters) {
-        super.setGridParameters(parameters);
-        final Matrix denormalization;
+    public void getParameterValues(final Parameters parameters) {
+        final Matrix denormalization = gridToTarget.getMatrix();
         if (parameters instanceof ContextualParameters) {
-            denormalization = ((ContextualParameters) parameters).getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
-        } else {
-            denormalization = new Matrix3();            // Identity.
+            /*
+             * The denormalization matrix computed by InterpolatedTransform is the inverse of the normalization matrix.
+             * This inverse is not suitable for the transform created by LocalizationGridBuilder; we need to replace it
+             * by the linear regression. We do not want to define a public API in `DatumShiftGrid` for that purpose yet
+             * because it would complexify that class (we would have to define API contract, etc.).
+             */
+            MatrixSIS m = ((ContextualParameters) parameters).getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
+            m.setMatrix(denormalization);
         }
         final int[] size = getGridSize();
         parameters.parameter(Constants.NUM_ROW).setValue(size[1]);
@@ -96,17 +103,32 @@ final class ResidualGrid extends DatumShiftGridFile<Dimensionless,Dimensionless>
     }
 
     /**
+     * Number of grid cells along the <var>x</var> axis.
+     * This is <code>{@linkplain #getGridSize()}[0]</code> as a field for performance reasons.
+     */
+    private final int nx;
+
+    /**
      * The residual data, as translations to apply on the result of affine transform.
      * In this flat array, index of target dimension varies fastest, then column index, then row index.
      */
     private final double[] offsets;
 
     /**
-     * Conversion from grid coordinates to the final "real world" coordinates.
+     * Conversion from translated coordinates (after the datum shift has been applied) to "real world" coordinates.
+     * If we were doing NADCON or NTv2 transformations with {@link #isCellValueRatio()} = {@code true} (source and
+     * target coordinates in the same coordinate system with axis units in degrees), that conversion would be the
+     * inverse of {@link #getCoordinateToGrid()}. But in this {@code ResidualGrid} case, we need to override with
+     * the linear regression computed by {@link LocalizationGridBuilder}.
+     */
+    final LinearTransform gridToTarget;
+
+    /**
+     * The best translation accuracy that we can expect from this file.
      *
-     * @see #gridToTarget()
+     * @see #getCellPrecision()
      */
-    private final LinearTransform gridToTarget;
+    private final double accuracy;
 
     /**
      * Creates a new residual grid.
@@ -119,46 +141,29 @@ final class ResidualGrid extends DatumShiftGridFile<Dimensionless,Dimensionless>
     ResidualGrid(final LinearTransform sourceToGrid, final LinearTransform gridToTarget,
             final int nx, final int ny, final double[] residuals, final double precision)
     {
-        super(Units.UNITY, Units.UNITY, true, sourceToGrid, nx, ny, PARAMETERS);
+        super(Units.UNITY, sourceToGrid, new int[] {nx, ny}, true, Units.UNITY);
         this.gridToTarget = gridToTarget;
         this.offsets      = residuals;
         this.accuracy     = precision;
+        this.nx           = nx;
     }
 
     /**
-     * Creates a new datum shift grid with the same grid geometry than the given grid
-     * but a reference to a different data array.
-     */
-    private ResidualGrid(final ResidualGrid other, final double[] data) {
-        super(other);
-        gridToTarget = other.gridToTarget;
-        accuracy     = other.accuracy;
-        offsets      = data;
-    }
-
-    /**
-     * Returns a new grid with the same geometry than this grid but different data array.
-     */
-    @Override
-    protected DatumShiftGridFile<Dimensionless, Dimensionless> setData(final Object[] other) {
-        return new ResidualGrid(this, (double[]) other[0]);
-    }
-
-    /**
-     * Returns reference to the data array. This method is for cache management, {@link #equals(Object)}
-     * and {@link #hashCode()} implementations only and should not be invoked in other context.
-     */
-    @Override
-    protected Object[] getData() {
-        return new Object[] {offsets};
-    }
-
-    /**
-     * Returns the transform from grid coordinates to "real world" coordinates after the datum shift has been applied.
+     * Returns a description of the values in this grid. Grid values may be given as matrices or tensors.
+     * Current implementation provides values in the form of {@link Matrix} objects on the assumption
+     * that the number of {@linkplain #getGridSize() grid} dimensions is {@value #SOURCE_DIMENSION}.
+     *
+     * <div class="note"><b>Note:</b>
+     * the number of {@linkplain #getGridSize() grid} dimensions determines the parameter type: if that number
+     * is greater than {@value #SOURCE_DIMENSION}, then parameters would need to be represented by tensors instead
+     * than matrices. By contrast, the {@linkplain #getTranslationDimensions() number of dimensions of translation
+     * vectors} only determines how many matrix or tensor parameters appear.</div>
+     *
+     * @return a description of the values in this grid.
      */
     @Override
-    public Matrix gridToTarget() {
-        return gridToTarget.getMatrix();
+    public ParameterDescriptorGroup getParameterDescriptors() {
+        return PARAMETERS;
     }
 
     /**
@@ -189,8 +194,19 @@ final class ResidualGrid extends DatumShiftGridFile<Dimensionless,Dimensionless>
     }
 
     /**
-     * View over one dimension of the offset vectors. This is used for populating the {@link ParameterDescriptorGroup}
+     * View over one target dimension of the localization grid. Used for populating the {@link ParameterDescriptorGroup}
      * that describes the {@code MathTransform}. Those parameters are themselves used for formatting Well Known Text.
+     * Current implementation can be used only when the number of grid dimensions is {@value #INTERPOLATED_DIMENSIONS}.
+     * If a grid has more dimensions, then tensors would need to be used instead than matrices.
+     *
+     * <p>This implementation can not be moved to the {@link DatumShiftGrid} parent class because this class assumes
+     * that the translation vectors are added to the source coordinates. This is not always true; for example France
+     * Geocentric interpolations add the translation to coordinates converted to geocentric coordinates.</p>
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.0
+     * @since   1.0
+     * @module
      */
     private final class Data extends FormattableObject implements Matrix, Function<int[],Number> {
         /** Coefficients from the denormalization matrix for the row corresponding to this dimension. */
@@ -274,4 +290,29 @@ final class ResidualGrid extends DatumShiftGridFile<Dimensionless,Dimensionless>
             return "Matrix";
         }
     }
+
+    /**
+     * Returns {@code true} if the given object is a grid containing the same data than this grid.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (other == this) {                        // Optimization for a common case.
+            return true;
+        }
+        if (super.equals(other)) {
+            final ResidualGrid that = (ResidualGrid) other;
+            return Numerics.equals(accuracy, that.accuracy) &&
+                    gridToTarget.equals(that.gridToTarget) &&
+                    Arrays.equals(offsets, that.offsets);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value for this datum shift grid.
+     */
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Arrays.hashCode(offsets) + 37 * gridToTarget.hashCode();
+    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
index 3acb776..74cfb4e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
@@ -18,16 +18,21 @@ package org.apache.sis.referencing.operation.transform;
 
 import java.util.Objects;
 import java.io.Serializable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
 import javax.measure.UnitConverter;
 import org.opengis.referencing.datum.Ellipsoid;
+import org.opengis.referencing.operation.Matrix;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.geometry.MismatchedDimensionException;
 import org.apache.sis.referencing.datum.DatumShiftGrid;
+import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.internal.referencing.provider.Molodensky;
+import org.apache.sis.measure.Units;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.Debug;
@@ -68,7 +73,7 @@ import org.apache.sis.util.Debug;
  * SIS handles those datum shifts with the {@link InterpolatedTransform} subclass.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.0
  *
  * @see DatumShiftGrid
  *
@@ -96,6 +101,19 @@ public abstract class DatumShiftTransform extends AbstractMathTransform implemen
     final DatumShiftGrid<?,?> grid;
 
     /**
+     * Conversion from (λ,φ) coordinates in radians to grid indices (x,y).
+     *
+     * <ul>
+     *   <li>x  =  (λ - λ₀) ⋅ {@code scaleX}  =  λ ⋅ {@code scaleX} + x₀</li>
+     *   <li>y  =  (φ - φ₀) ⋅ {@code scaleY}  =  φ ⋅ {@code scaleY} + y₀</li>
+     * </ul>
+     *
+     * Those factors are extracted from the {@link DatumShiftGrid#getCoordinateToGrid()}
+     * transform for performance reasons.
+     */
+    private transient double scaleX, scaleY, x0, y0;
+
+    /**
      * Creates a datum shift transform for direct interpolations in a grid.
      * It is caller responsibility to initialize the {@link #context} parameters.
      *
@@ -106,6 +124,7 @@ public abstract class DatumShiftTransform extends AbstractMathTransform implemen
         final int size = grid.getTranslationDimensions() + 1;
         context = new ContextualParameters(descriptor, size, size);
         this.grid = grid;
+        computeConversionFactors();
     }
 
     /**
@@ -122,6 +141,64 @@ public abstract class DatumShiftTransform extends AbstractMathTransform implemen
     {
         context = new ContextualParameters(descriptor, isSource3D ? 4 : 3, isTarget3D ? 4 : 3);
         this.grid = grid;
+        computeConversionFactors();
+    }
+
+    /**
+     * Invoked after deserialization. This method computes the transient fields.
+     *
+     * @param  in  the input stream from which to deserialize the datum shift grid.
+     * @throws IOException if an I/O error occurred while reading or if the stream contains invalid data.
+     * @throws ClassNotFoundException if the class serialized on the stream is not on the classpath.
+     */
+    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
+        in.defaultReadObject();
+        computeConversionFactors();
+    }
+
+    /**
+     * Computes the conversion factors needed for calls to {@link DatumShiftGrid#interpolateInCell(double, double, double[])}.
+     * This method takes only the {@value DatumShiftGrid#INTERPOLATED_DIMENSIONS} first dimensions. If a conversion factor can
+     * not be computed, then it is set to NaN.
+     */
+    @SuppressWarnings("fallthrough")
+    private void computeConversionFactors() {
+        scaleX = Double.NaN;
+        scaleY = Double.NaN;
+        x0     = Double.NaN;
+        y0     = Double.NaN;
+        if (grid != null) {
+            final LinearTransform coordinateToGrid = grid.getCoordinateToGrid();
+            final double toStandardUnit = Units.toStandardUnit(grid.getCoordinateUnit());
+            if (!Double.isNaN(toStandardUnit)) {
+                final Matrix m = coordinateToGrid.getMatrix();
+                if (Matrices.isAffine(m)) {
+                    final int n = m.getNumCol() - 1;
+                    switch (m.getNumRow()) {
+                        default: y0 = m.getElement(1,n); scaleY = diagonal(m, 1, n) / toStandardUnit;   // Fall through
+                        case 1:  x0 = m.getElement(0,n); scaleX = diagonal(m, 0, n) / toStandardUnit;
+                        case 0:  break;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the value on the diagonal of the given matrix, provided that all other non-translation terms are 0.
+     *
+     * @param  m  the matrix from which to get the scale factor on a row.
+     * @param  j  the row for which to get the scale factor.
+     * @param  n  index of the last column.
+     * @return the scale factor on the diagonal, or NaN.
+     */
+    private static double diagonal(final Matrix m, final int j, int n) {
+        while (--n >= 0) {
+            if (j != n && m.getElement(j, n) != 0) {
+                return Double.NaN;
+            }
+        }
+        return m.getElement(j, j);
     }
 
     /**
@@ -221,6 +298,30 @@ public abstract class DatumShiftTransform extends AbstractMathTransform implemen
     }
 
     /**
+     * Converts the given normalized <var>x</var> coordinate to grid index.
+     * "Normalized coordinates" are coordinates in the unit of measurement given by {@link Unit#getSystemUnit()}.
+     * For angular coordinates, this is radians. For linear coordinates, this is metres.
+     *
+     * @param  x  the "real world" coordinate (often longitude in radians) of the point for which to get the translation.
+     * @return the grid index for the given coordinate. May be out of bounds.
+     */
+    final double normalizedToGridX(final double x) {
+        return x * scaleX + x0;
+    }
+
+    /**
+     * Converts the given normalized <var>x</var> coordinate to grid index.
+     * "Normalized coordinates" are coordinates in the unit of measurement given by {@link Unit#getSystemUnit()}.
+     * For angular coordinates, this is radians. For linear coordinates, this is metres.
+     *
+     * @param  y  the "real world" coordinate (often latitude in radians) of the point for which to get the translation.
+     * @return the grid index for the given coordinate. May be out of bounds.
+     */
+    final double normalizedToGridY(final double y) {
+        return y * scaleY + y0;
+    }
+
+    /**
      * {@inheritDoc}
      *
      * @return {@inheritDoc}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedGeocentricTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedGeocentricTransform.java
index 6962dbe..170bc57 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedGeocentricTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedGeocentricTransform.java
@@ -28,7 +28,6 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.internal.referencing.provider.DatumShiftGridFile;
 import org.apache.sis.internal.referencing.provider.FranceGeocentricInterpolation;
 import org.apache.sis.internal.referencing.provider.Molodensky;
 import org.apache.sis.internal.util.Constants;
@@ -79,7 +78,7 @@ import org.apache.sis.util.ArgumentChecks;
  *
  * @author  Simon Reynard (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.0
  *
  * @see InterpolatedMolodenskyTransform
  *
@@ -235,9 +234,7 @@ public class InterpolatedGeocentricTransform extends DatumShiftTransform {
         semiMinor = source.getSemiMinorAxis();
         setContextParameters(semiMajor, semiMinor, unit, target);
         context.getOrCreate(Molodensky.DIMENSION).setValue(isSource3D ? 3 : 2);
-        if (grid instanceof DatumShiftGridFile<?,?>) {
-            ((DatumShiftGridFile<?,?>) grid).setGridParameters(context);
-        }
+        grid.getParameterValues(context);
         /*
          * The above setContextParameters(…) method converted the axis lengths of target ellipsoid in the same units
          * than source ellipsoid. Opportunistically fetch that value, so we don't have to convert the values ourselves.
@@ -375,8 +372,8 @@ public class InterpolatedGeocentricTransform extends DatumShiftTransform {
          * The translation that we got is in metres, which we convert into normalized units.
          */
         final double[] vector = new double[3];
-        grid.interpolateInCell(grid.normalizedToGridX(srcPts[srcOff]),              // In radians
-                               grid.normalizedToGridY(srcPts[srcOff+1]), vector);
+        grid.interpolateInCell(normalizedToGridX(srcPts[srcOff]),              // In radians
+                               normalizedToGridY(srcPts[srcOff+1]), vector);
         final double tX = vector[0] / semiMajor;
         final double tY = vector[1] / semiMajor;
         final double tZ = vector[2] / semiMajor;
@@ -522,8 +519,8 @@ public class InterpolatedGeocentricTransform extends DatumShiftTransform {
              * geocentric interpolation at that location and get the (λ,φ) again. In theory, we just
              * iterate until we got the desired precision. But in practice a single interation is enough.
              */
-            grid.interpolateInCell(grid.normalizedToGridX(vector[0]),
-                                   grid.normalizedToGridY(vector[1]), vector);
+            grid.interpolateInCell(normalizedToGridX(vector[0]),
+                                   normalizedToGridY(vector[1]), vector);
             vector[0] = (x - vector[0] / semiMajor) * scale;
             vector[1] = (y - vector[1] / semiMajor) * scale;
             vector[2] = (z - vector[2] / semiMajor) * scale;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedMolodenskyTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedMolodenskyTransform.java
index 65e3b26..d731805 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedMolodenskyTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedMolodenskyTransform.java
@@ -28,7 +28,6 @@ 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.internal.referencing.provider.DatumShiftGridFile;
 import org.apache.sis.internal.referencing.provider.FranceGeocentricInterpolation;
 import org.apache.sis.internal.referencing.provider.Molodensky;
 import org.apache.sis.internal.util.Constants;
@@ -61,7 +60,7 @@ import org.apache.sis.util.Debug;
  * ({@linkplain #tX}, {@linkplain #tY}, {@linkplain #tZ}) parameters of a Molodensky transformation.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see InterpolatedGeocentricTransform
  *
@@ -223,9 +222,7 @@ public class InterpolatedMolodenskyTransform extends MolodenskyFormula {
             pg.getOrCreate(Molodensky.AXIS_LENGTH_DIFFERENCE).setValue(Δa, unit);
             pg.getOrCreate(Molodensky.FLATTENING_DIFFERENCE) .setValue(Δf, Units.UNITY);
         }
-        if (grid instanceof DatumShiftGridFile<?,?>) {
-            ((DatumShiftGridFile<?,?>) grid).setGridParameters(pg);
-        }
+        grid.getParameterValues(pg);
     }
 
     /**
@@ -244,8 +241,8 @@ public class InterpolatedMolodenskyTransform extends MolodenskyFormula {
         final double[] vector = new double[3];
         final double λ = srcPts[srcOff];
         final double φ = srcPts[srcOff+1];
-        grid.interpolateInCell(grid.normalizedToGridX(λ),
-                               grid.normalizedToGridY(φ), vector);
+        grid.interpolateInCell(normalizedToGridX(λ),
+                               normalizedToGridY(φ), vector);
         return transform(λ, φ, isSource3D ? srcPts[srcOff+2] : 0,
                 dstPts, dstOff, vector[0], vector[1], vector[2], null, derivate);
     }
@@ -292,8 +289,8 @@ public class InterpolatedMolodenskyTransform extends MolodenskyFormula {
         while (--numPts >= 0) {
             final double λ = srcPts[srcOff  ];
             final double φ = srcPts[srcOff+1];
-            grid.interpolateInCell(grid.normalizedToGridX(λ),
-                                   grid.normalizedToGridY(φ), offset);
+            grid.interpolateInCell(normalizedToGridX(λ),
+                                   normalizedToGridY(φ), offset);
             transform(λ, φ, isSource3D ? srcPts[srcOff+2] : 0,
                       dstPts, dstOff, offset[0], offset[1], offset[2], null, false);
             srcOff += srcInc;
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 8fc5bcc..62ccf8b 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
@@ -37,8 +37,6 @@ import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.internal.referencing.Formulas;
 import org.apache.sis.internal.referencing.DirectPositionView;
-import org.apache.sis.internal.referencing.provider.NTv2;
-import org.apache.sis.internal.referencing.provider.DatumShiftGridFile;
 
 
 /**
@@ -132,14 +130,11 @@ public class InterpolatedTransform extends DatumShiftTransform {
      * @see #createGeodeticTransformation(MathTransformFactory, DatumShiftGrid)
      */
     @SuppressWarnings( {"OverridableMethodCallDuringObjectConstruction", "fallthrough"})
-    protected <T extends Quantity<T>> InterpolatedTransform(final DatumShiftGrid<T,T> grid)
-            throws NoninvertibleMatrixException
-    {
+    protected <T extends Quantity<T>> InterpolatedTransform(final DatumShiftGrid<T,T> grid) throws NoninvertibleMatrixException {
         /*
          * Create the contextual parameters using the descriptor of the provider that created the datum shift grid.
-         * If that provider is unknown, default (for now) to NTv2. This default may change in any future SIS version.
          */
-        super((grid instanceof DatumShiftGridFile<?,?>) ? ((DatumShiftGridFile<?,?>) grid).descriptor : NTv2.PARAMETERS, grid);
+        super(grid.getParameterDescriptors(), grid);
         if (!grid.isCellValueRatio()) {
             throw new IllegalArgumentException(Resources.format(
                     Resources.Keys.IllegalParameterValue_2, "isCellValueRatio", Boolean.FALSE));
@@ -181,26 +176,15 @@ public class InterpolatedTransform extends DatumShiftTransform {
         /*
          * Denormalization is the inverse of all above conversions in the usual case (NADCON and NTv2) where the
          * source coordinate system is the same than the target coordinate system, for example with axis unit in
-         * degrees. However we also use this InterpolatedTransform implementation for other operation, like the
+         * degrees. However we also use this InterpolatedTransform implementation for other operations, like the
          * one created by LocalizationGridBuilder. Those later operations may require a different denormalization
-         * matrix.
+         * matrix. Consequently the call to `getParameterValues(…)` may overwrite the denormalization matrix as
+         * a non-documented side effect.
          */
-        Matrix denormalize = null;
-        if (grid instanceof DatumShiftGridFile<?,?>) {
-            denormalize = ((DatumShiftGridFile<?,?>) grid).gridToTarget();
-        }
-        if (denormalize == null) {
-            denormalize = normalize.inverse();                      // Normal NACDON and NTv2 case.
-        }
+        Matrix denormalize = normalize.inverse();                   // Normal NACDON and NTv2 case.
         context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION).setMatrix(denormalize);
+        grid.getParameterValues(context);       // May overwrite `denormalize` (see above comment).
         inverse = createInverse();
-        /*
-         * Parameters completed last because some DatumShiftGridFile subclasses (e.g. ResidualGrid) needs the
-         * (de)normalization matrices.
-         */
-        if (grid instanceof DatumShiftGridFile<?,?>) {
-            ((DatumShiftGridFile<?,?>) grid).setGridParameters(context);
-        }
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MolodenskyFormula.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MolodenskyFormula.java
index 6386fc3..c5497f4 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MolodenskyFormula.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MolodenskyFormula.java
@@ -47,7 +47,7 @@ import static java.lang.Math.*;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.0
  * @since   0.7
  * @module
  */
@@ -367,7 +367,7 @@ abstract class MolodenskyFormula extends DatumShiftTransform {
             /*
              * Following is executed only in InterpolatedMolodenskyTransform case.
              */
-            grid.interpolateInCell(grid.normalizedToGridX(λt), grid.normalizedToGridY(φt), offset);
+            grid.interpolateInCell(normalizedToGridX(λt), normalizedToGridY(φt), offset);
             tX = -offset[0];
             tY = -offset[1];
             tZ = -offset[2];
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 9030ac0..a5ba367 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
@@ -61,7 +61,7 @@ public final strictfp class ResidualGridTest extends TestCase {
     public void verifyGlobalProperties() {
         assertEquals("translationDimensions", 2,  grid.getTranslationDimensions());
         assertTrue("coordinateToGrid.isIdentity", grid.getCoordinateToGrid().isIdentity());
-        assertTrue("gridToTarget.isIdentity",     grid.gridToTarget().isIdentity());
+        assertTrue("gridToTarget.isIdentity",     grid.gridToTarget.isIdentity());
     }
 
     /**
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/QuadraticShiftGrid.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/QuadraticShiftGrid.java
index b7ff5d4..6eb20df 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/QuadraticShiftGrid.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/QuadraticShiftGrid.java
@@ -16,10 +16,14 @@
  */
 package org.apache.sis.referencing.operation.transform;
 
+import java.util.Collections;
 import java.awt.geom.AffineTransform;
 import javax.measure.quantity.Dimensionless;
+import org.opengis.parameter.ParameterDescriptorGroup;
 import org.apache.sis.referencing.datum.DatumShiftGrid;
 import org.apache.sis.measure.Units;
+import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
+import org.apache.sis.parameter.Parameters;
 
 
 /**
@@ -147,4 +151,20 @@ final strictfp class QuadraticShiftGrid extends DatumShiftGrid<Dimensionless,Dim
     public double getCellPrecision() {
         return PRECISION;
     }
+
+    /**
+     * Returns a dummy parameter descriptor for this test.
+     */
+    @Override
+    public ParameterDescriptorGroup getParameterDescriptors() {
+        return new DefaultParameterDescriptorGroup(
+                Collections.singletonMap(DefaultParameterDescriptorGroup.NAME_KEY, "Test grid"), 0, 1);
+    }
+
+    /**
+     * No parameter to set for this test.
+     */
+    @Override
+    public void getParameterValues(Parameters parameters) {
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
index abee72c..ed5d200 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.internal.util;
 
+import java.lang.reflect.Array;
 import java.util.Formatter;
 import java.util.FormattableFlags;
 import org.apache.sis.util.Static;
@@ -226,7 +227,7 @@ public final class Strings extends Static {
      * @return a string representation of an instance of the given class having the given properties.
      */
     public static String toString(final Class<?> classe, final Object... properties) {
-        final StringBuffer buffer = new StringBuffer(32).append(Classes.getShortName(classe)).append('[');
+        final StringBuilder buffer = new StringBuilder(32).append(Classes.getShortName(classe)).append('[');
         boolean isNext = false;
         for (int i=0; i<properties.length; i++) {
             final Object value = properties[++i];
@@ -238,10 +239,17 @@ public final class Strings extends Static {
                 if (name != null) {
                     buffer.append(name).append('=');
                 }
-                final boolean isText = (value instanceof CharSequence);
-                if (isText) buffer.append('“');
-                buffer.append(value);
-                if (isText) buffer.append('”');
+                if (value.getClass().isArray()) {
+                    final int n = Array.getLength(value);
+                    if (n != 1) buffer.append('{');
+                    for (int j=0; j<n; j++) {
+                        if (j != 0) buffer.append(", ");
+                        append(Array.get(value, j), buffer);
+                    }
+                    if (n != 1) buffer.append('}');
+                } else {
+                    append(value, buffer);
+                }
                 isNext = true;
             }
         }
@@ -249,6 +257,16 @@ public final class Strings extends Static {
     }
 
     /**
+     * Appends the given value in the given buffer, using quotes if the value is a character sequence.
+     */
+    private static void append(final Object value, final StringBuilder buffer) {
+        final boolean isText = (value instanceof CharSequence);
+        if (isText) buffer.append('“');
+        buffer.append(value);
+        if (isText) buffer.append('”');
+    }
+
+    /**
      * Formats the given character sequence to the given formatter. This method takes in account
      * the {@link FormattableFlags#UPPERCASE} and {@link FormattableFlags#LEFT_JUSTIFY} flags.
      *