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

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

commit f17de4bf788c413ef76cd8cb2474bb6b7ca4219c
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Mar 9 23:52:51 2019 +0100

    First draft of the use of linearizers in LinearTransformBuilder.
    https://issues.apache.org/jira/browse/SIS-446
---
 .../operation/builder/LinearTransformBuilder.java  | 467 +++++++++++++++++----
 .../operation/builder/LocalizationGridBuilder.java |   5 +
 .../operation/builder/ProjectedTransformTry.java   | 324 ++++++++++++++
 .../operation/builder/TransformBuilder.java        |   6 +
 .../operation/transform/AbstractMathTransform.java |   2 +-
 .../builder/LinearTransformBuilderTest.java        |  37 +-
 .../operation/builder/NonLinearTransform.java      |  51 +++
 .../java/org/apache/sis/internal/util/Strings.java |  26 +-
 .../src/main/java/org/apache/sis/math/Plane.java   |   4 +
 .../main/java/org/apache/sis/util/ArraysExt.java   |   8 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 ide-project/NetBeans/nbproject/genfiles.properties |   2 +-
 ide-project/NetBeans/nbproject/project.xml         |   2 +
 15 files changed, 852 insertions(+), 89 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 4138d69..6aa38a4 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
@@ -17,8 +17,15 @@
 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.ArrayList;
+import java.util.ArrayDeque;
+import java.util.Collections;
 import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.text.NumberFormat;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import org.opengis.util.FactoryException;
@@ -27,6 +34,7 @@ import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.geometry.coordinate.Position;
 import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.io.TableAppender;
@@ -60,9 +68,23 @@ import org.apache.sis.util.Classes;
  * Otherwise a builder created by the {@link #LinearTransformBuilder()} 
constructor will be able to handle
  * randomly distributed coordinates.
  *
- * <p>The transform coefficients are determined using a <cite>least 
squares</cite> estimation method,
+ * <p>Builders can be used only once;
+ * points can not be added or modified after {@link 
#create(MathTransformFactory)} has been invoked.
+ * The transform coefficients are determined using a <cite>least 
squares</cite> estimation method,
  * with the assumption that source positions are exact and all the uncertainty 
is in the target positions.</p>
  *
+ * <div class="section">Linearizers</div>
+ * Consider the following situation (commonly found with {@linkplain 
org.apache.sis.storage.netcdf netCDF files}):
+ * the <i>sources</i> coordinates are pixel indices and the <i>targets</i> are 
(longitude, latitude) coordinates,
+ * but we suspect that the <i>sources to targets</i> transform is some 
undetermined map projection, maybe Mercator.
+ * A linear approximation between those coordinates will give poor results; 
the results would be much better if all
+ * (longitude, latitude) coordinates were converted to the right projection 
first. However that map projection may
+ * not be known, but we can try to guess it by trials-and-errors using a set 
of plausible projections.
+ * That set can be specified by {@link #addLinearizers(Map, int...)}.
+ * If the {@link #create(MathTransformFactory)} method finds that one of the 
specified projections seems a good fit,
+ * it will automatically convert all target coordinates to that projection.
+ * That selected projection is given by {@link #linearizer()}.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  *
@@ -103,6 +125,12 @@ public class LinearTransformBuilder extends 
TransformBuilder {
      * This layout allows to create only a few (typically two) large arrays 
instead of a multitude of small arrays.
      * Example: {x[], y[], z[]}.
      * This is {@code null} if not yet specified.
+     *
+     * <div class="note"><b>Implementation note:</b>
+     * we could use a flat array with (x₀, y₀), (x₁, y₁), (x₂, y₂), 
<i>etc.</i> coordinate tuples instead.
+     * Such flat array would be more convenient for some coordinate 
conversions with {@link MathTransform}.
+     * But using array of arrays is more convenient for other calculations 
working on one dimension at time,
+     * make data more local for CPU, and also allows handling of more 
points.</div>
      */
     private double[][] targets;
 
@@ -122,8 +150,25 @@ public class LinearTransformBuilder extends 
TransformBuilder {
     private int numPoints;
 
     /**
+     * If the user suspects that the transform may be linear when the target 
is another space than the space of
+     * {@link #targets} coordinates, projections toward spaces to try. The 
{@link #create(MathTransformFactory)}
+     * method will try to apply those transforms on {@link #targets} and check 
if they produce better fits.
+     *
+     * @see #addLinearizers(Map, int[])
+     * @see #linearizer()
+     */
+    private List<ProjectedTransformTry> linearizers;
+
+    /**
+     * If one of the {@linkplain #linearizers} have been applied, that 
linearizer. Otherwise {@code null}.
+     *
+     * @see #linearizer()
+     */
+    private transient ProjectedTransformTry appliedLinearizer;
+
+    /**
      * The transform created by the last call to {@link 
#create(MathTransformFactory)}.
-     * This is reset to {@code null} when coordinates are modified.
+     * A non-null value means that this builder became unmodifiable.
      */
     private transient LinearTransform transform;
 
@@ -131,7 +176,24 @@ public class LinearTransformBuilder extends 
TransformBuilder {
      * An estimation of the Pearson correlation coefficient for each target 
dimension.
      * This is {@code null} if not yet computed.
      */
-    private transient double[] correlation;
+    private transient double[] correlations;
+
+    /**
+     * Creates a temporary builder with all source fields from the given 
builder and no target arrays.
+     * 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)}
+     * internal usage.
+     *
+     * @param original  the builder from which to take array references of 
source values.
+     */
+    private LinearTransformBuilder(final LinearTransformBuilder original) {
+        gridSize   = original.gridSize;
+        sources    = original.sources;
+        gridLength = original.gridLength;
+        numPoints  = original.numPoints;
+    }
 
     /**
      * Creates a new linear transform builder for randomly distributed 
positions.
@@ -326,6 +388,14 @@ search: for (int j=numPoints; --j >= 0;) {
     }
 
     /**
+     * Returns the error message to be given to {@link IllegalStateException}
+     * when this builder can not be modified anymore.
+     */
+    private static String unmodifiable() {
+        return Errors.format(Errors.Keys.UnmodifiableObject_1, 
LinearTransformBuilder.class);
+    }
+
+    /**
      * Returns the number of dimensions in the source grid, or -1 if this 
builder is not backed by a grid.
      * Contrarily to the other {@code get*Dimensions()} methods, this method 
does not throw exception.
      *
@@ -391,12 +461,14 @@ search: for (int j=numPoints; --j >= 0;) {
             }
             return envelope;
         } else {
-            return envelope(sources);
+            return envelope(sources, numPoints);
         }
     }
 
     /**
      * Returns the envelope of target points. 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.
      *
      * @return the envelope of target points.
      * @throws IllegalStateException if the target points are not yet known.
@@ -404,22 +476,24 @@ search: for (int j=numPoints; --j >= 0;) {
      * @since 1.0
      */
     public Envelope getTargetEnvelope() {
-        return envelope(targets);
+        return envelope(targets, (gridLength != 0) ? gridLength : numPoints);
     }
 
     /**
      * Implementation of {@link #getSourceEnvelope()} and {@link 
#getTargetEnvelope()}.
      */
-    private static Envelope envelope(final double[][] points) {
+    private static Envelope envelope(final double[][] points, final int 
numPoints) {
         if (points == null) {
             throw new IllegalStateException(noData());
         }
         final int dim = points.length;
         final GeneralEnvelope envelope = new GeneralEnvelope(dim);
         for (int i=0; i<dim; i++) {
+            final double[] values = points[i];
             double lower = Double.POSITIVE_INFINITY;
             double upper = Double.NEGATIVE_INFINITY;
-            for (final double value : points[i]) {
+            for (int j=0; j<numPoints; j++) {
+                final double value = values[j];
                 if (value < lower) lower = value;
                 if (value > upper) upper = value;
             }
@@ -447,7 +521,7 @@ search: for (int j=numPoints; --j >= 0;) {
      *
      * <p>All source positions shall have the same number of dimensions (the 
<cite>source dimension</cite>),
      * and all target positions shall have the same number of dimensions (the 
<cite>target dimension</cite>).
-     * However the source dimension does not need to be the same the target 
dimension.
+     * However the source dimension does not need to be the same than the 
target dimension.
      * Apache SIS currently supports only one- or two-dimensional source 
positions,
      * together with arbitrary target dimension.</p>
      *
@@ -459,6 +533,7 @@ search: for (int j=numPoints; --j >= 0;) {
      *
      * @param  sourceToTarget  a map of source positions to target positions.
      *         Source positions are assumed precise and target positions are 
assumed uncertain.
+     * @throws IllegalStateException if {@link #create(MathTransformFactory) 
create(…)} has already been invoked.
      * @throws IllegalArgumentException if the given positions contain NaN or 
infinite coordinate values.
      * @throws IllegalArgumentException if this builder has been {@linkplain 
#LinearTransformBuilder(int...)
      *         created for a grid} but some source coordinates are not indices 
in that grid.
@@ -469,14 +544,15 @@ search: for (int j=numPoints; --j >= 0;) {
     public void setControlPoints(final Map<? extends Position, ? extends 
Position> sourceToTarget)
             throws MismatchedDimensionException
     {
+        if (transform != null) {
+            throw new IllegalStateException(unmodifiable());
+        }
         ArgumentChecks.ensureNonNull("sourceToTarget", sourceToTarget);
-        transform   = null;
-        correlation = null;
-        sources     = null;
-        targets     = null;
-        numPoints   = 0;
-        int srcDim  = 0;
-        int tgtDim  = 0;
+        sources    = null;
+        targets    = null;
+        numPoints  = 0;
+        int srcDim = 0;
+        int tgtDim = 0;
         for (final Map.Entry<? extends Position, ? extends Position> entry : 
sourceToTarget.entrySet()) {
             final DirectPosition src = position(entry.getKey());   if (src == 
null) continue;
             final DirectPosition tgt = position(entry.getValue()); if (tgt == 
null) continue;
@@ -543,6 +619,10 @@ search: for (int j=numPoints; --j >= 0;) {
      * The map is unmodifiable and is guaranteed to contain only non-null keys 
and values.
      * The map is a view: changes in this builder are immediately reflected in 
the returned map.
      *
+     * <p>If {@link #linearizer()} returns a non-empty value,
+     * then the values in the returned map are projected using that linearizer.
+     * This may happen only after {@link #create(MathTransformFactory) 
create(…)} has been invoked.</p>
+     *
      * @return all control points in this builder.
      *
      * @since 1.0
@@ -788,6 +868,7 @@ search:         for (int j=domain(); --j >= 0;) {
      *                 then for every index <var>i</var> the {@code source[i]} 
value shall be in the [0 … {@code gridSize[i]}-1] range inclusive.
      *                 If this builder has been created with the {@link 
#LinearTransformBuilder()} constructor, then no constraint apply.
      * @param  target  the target coordinates, assumed uncertain.
+     * @throws IllegalStateException if {@link #create(MathTransformFactory) 
create(…)} has already been invoked.
      * @throws IllegalArgumentException if this builder has been {@linkplain 
#LinearTransformBuilder(int...) created for a grid}
      *         but some source coordinates are out of index range, or if 
{@code target} contains NaN of infinite numbers.
      * @throws MismatchedDimensionException if the source or target position 
does not have the expected number of dimensions.
@@ -795,6 +876,9 @@ search:         for (int j=domain(); --j >= 0;) {
      * @since 0.8
      */
     public void setControlPoint(final int[] source, final double[] target) {
+        if (transform != null) {
+            throw new IllegalStateException(unmodifiable());
+        }
         ArgumentChecks.ensureNonNull("source", source);
         ArgumentChecks.ensureNonNull("target", target);
         verifySourceDimension(source.length);
@@ -809,6 +893,9 @@ search:         for (int j=domain(); --j >= 0;) {
             if (targets == null) {
                 allocate(tgtDim);
             }
+            if (Double.isNaN(targets[0][index])) {
+                numPoints++;
+            }
         } else {
             /*
              * Case of randomly distributed points. Algorithm used below is 
inefficient, but Javadoc
@@ -836,10 +923,11 @@ search:         for (int j=domain(); --j >= 0;) {
         for (int i=0; i<tgtDim; i++) {
             isValid &= Double.isFinite(targets[i][index] = target[i]);
         }
-        transform   = null;
-        correlation = null;
         if (!isValid) {
-            if (gridSize == null) numPoints--;
+            numPoints--;
+            for (int i=0; i<tgtDim; i++) {
+                targets[i][index] = Double.NaN;
+            }
             throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IllegalMapping_2,
                                                source, new 
DirectPositionView.Double(target)));
         }
@@ -850,6 +938,9 @@ search:         for (int j=domain(); --j >= 0;) {
      * This method can be used for retrieving points set by previous calls to
      * {@link #setControlPoint(int[], double[])} or {@link 
#setControlPoints(Map)}.
      *
+     * <p>If {@link #linearizer()} returns a non-empty value, then the 
returned values are projected using that linearizer.
+     * This may happen only if this method is invoked after {@link 
#create(MathTransformFactory) create(…)}.</p>
+     *
      * <div class="note"><b>Performance note:</b>
      * current implementation is efficient for builders {@linkplain 
#LinearTransformBuilder(int...) created for a grid}
      * but inefficient for builders {@linkplain #LinearTransformBuilder() 
created for randomly distributed points}.</div>
@@ -900,9 +991,13 @@ search:         for (int j=domain(); --j >= 0;) {
      * <i>etc.</i>. Coordinates are stored in row-major order (column index 
varies faster, followed by row index).
      *
      * @param  coordinates coordinates in each target dimensions, stored in 
row-major order.
+     * @throws IllegalStateException if {@link #create(MathTransformFactory) 
create(…)} has already been invoked.
      */
-    final void setControlPoints(final Vector... coordinates) {
+    final void setControlPoints(final Vector[] coordinates) {
         assert gridSize != null;
+        if (transform != null) {
+            throw new IllegalStateException(unmodifiable());
+        }
         final int tgtDim = coordinates.length;
         final double[][] result = new double[tgtDim][];
         for (int i=0; i<tgtDim; i++) {
@@ -917,9 +1012,8 @@ search:         for (int j=domain(); --j >= 0;) {
             }
             throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnexpectedArrayLength_2, 
gridLength, size));
         }
-        targets     = result;
-        transform   = null;
-        correlation = null;
+        targets = result;
+        numPoints = gridLength;
     }
 
     /**
@@ -955,8 +1049,12 @@ search:         for (int j=domain(); --j >= 0;) {
      *                    Value can be from 0 inclusive to {@link 
#getSourceDimensions()} exclusive.
      *                    The recommended direction is the direction of most 
stable values, typically 1 (rows) for longitudes.
      * @param  period     that wraparound range (typically 360° for 
longitudes).
+     * @throws IllegalStateException if {@link #create(MathTransformFactory) 
create(…)} has already been invoked.
      */
     final void resolveWraparoundAxis(final int dimension, final int direction, 
final double period) {
+        if (transform != null) {
+            throw new IllegalStateException(unmodifiable());
+        }
         final double[] coordinates = targets[dimension];
         int stride = 1;
         for (int i=0; i<direction; i++) {
@@ -1014,8 +1112,56 @@ 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 
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 analysis or 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>
+     *
+     * @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.
+     *                      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 #linearizer()
+     * @see #correlation()
+     *
+     * @since 1.0
+     */
+    public void addLinearizers(final Map<String,MathTransform> projections, 
int... dimensions) {
+        if (transform != null) {
+            throw new IllegalStateException(unmodifiable());
+        }
+        final int tgtDim = getTargetDimensions();
+        if (dimensions == null || dimensions.length == 0) {
+            dimensions = ArraysExt.sequence(0, tgtDim);
+        }
+        if (linearizers == null) {
+            linearizers = new ArrayList<>();
+        }
+        for (final Map.Entry<String,MathTransform> entry : 
projections.entrySet()) {
+            ProjectedTransformTry t = new 
ProjectedTransformTry(entry.getKey(), entry.getValue(), dimensions, tgtDim);
+            if (!t.projection.isIdentity()) {
+                linearizers.add(t);
+            }
+        }
+    }
+
+    /**
      * 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
+     * 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.
      *
      * @param  factory  the factory to use for creating the transform, or 
{@code null} for the default factory.
      *                  The {@link 
MathTransformFactory#createAffineTransform(Matrix)} method of that factory
@@ -1027,68 +1173,129 @@ search:         for (int j=domain(); --j >= 0;) {
      * @since 0.8
      */
     @Override
-    @SuppressWarnings("serial")
     public LinearTransform create(final MathTransformFactory factory) throws 
FactoryException {
         if (transform == null) {
-            final double[][] sources = this.sources;                    // 
Protect from changes.
-            final double[][] targets = this.targets;
-            if (targets == null) {
-                throw new InvalidGeodeticParameterException(noData());
-            }
-            final int sourceDim = (sources != null) ? sources.length : 
gridSize.length;
-            final int targetDim = targets.length;
-            correlation = new double[targetDim];
-            final MatrixSIS matrix = Matrices.create(targetDim + 1, sourceDim 
+ 1,  ExtendedPrecisionMatrix.ZERO);
-            matrix.setElement(targetDim, sourceDim, 1);
-            for (int j=0; j < targetDim; j++) {
-                final double c;
-                switch (sourceDim) {
-                    case 1: {
-                        final int row = j;
-                        final Line line = new Line() {
-                            @Override public void setEquation(final Number 
slope, final Number y0) {
-                                super.setEquation(slope, y0);
-                                matrix.setNumber(row, 0, slope);    // 
Preserve the extended precision (double-double).
-                                matrix.setNumber(row, 1, y0);
-                            }
-                        };
-                        if (sources != null) {
-                            c = line.fit(vector(sources[0]), 
vector(targets[j]));
+            MatrixSIS matrix = fit();
+            if (linearizers != null) {
+                /*
+                 * 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.
+                 */
+                double     bestCorrelation   = average(correlations);
+                double[]   bestCorrelations  = null;
+                MatrixSIS  bestTransform     = null;
+                double[][] transformedArrays = null;
+                /*
+                 * 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.
+                 */
+                final ProjectedTransformTry[] alternatives = 
linearizers.toArray(new ProjectedTransformTry[linearizers.size()]);
+                linearizers.add(new ProjectedTransformTry((float) 
bestCorrelation));
+                /*
+                 * '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 MatrixSIS altTransform    = tmp.fit();
+                        final double[]  altCorrelations = 
alt.replace(correlations, tmp.correlations);
+                        final double    altCorrelation  = 
average(altCorrelations);
+                        alt.correlation = (float) altCorrelation;
+                        if (altCorrelation > bestCorrelation) {
+                            ProjectedTransformTry.recycle(transformedArrays, 
pool);
+                            transformedArrays = tmp.targets;
+                            bestCorrelation   = altCorrelation;
+                            bestCorrelations  = altCorrelations;
+                            bestTransform     = alt.replace(matrix, 
altTransform);
+                            appliedLinearizer = alt;
                         } else {
-                            c = line.fit(Vector.createSequence(0, 1, 
gridSize[0]),
-                                         Vector.create(targets[j]));
+                            ProjectedTransformTry.recycle(tmp.targets, pool);
                         }
-                        break;
                     }
-                    case 2: {
-                        final int row = j;
-                        final Plane plan = new Plane() {
-                            @Override public void setEquation(final Number sx, 
final Number sy, final Number z0) {
-                                super.setEquation(sx, sy, z0);
-                                matrix.setNumber(row, 0, sx);       // 
Preserve the extended precision (double-double).
-                                matrix.setNumber(row, 1, sy);
-                                matrix.setNumber(row, 2, z0);
-                            }
-                        };
-                        if (sources != null) {
-                            c = plan.fit(vector(sources[0]), 
vector(sources[1]), vector(targets[j]));
-                        } else try {
-                            c = plan.fit(gridSize[0], gridSize[1], 
Vector.create(targets[j]));
-                        } catch (IllegalArgumentException e) {
-                            // This may happen if the z vector still contain 
some "NaN" values.
-                            throw new 
InvalidGeodeticParameterException(noData(), e);
+                }
+                if (bestTransform != null) {
+                    matrix       = bestTransform;
+                    targets      = transformedArrays;
+                    correlations = bestCorrelations;
+                }
+            }
+            transform = (LinearTransform) 
nonNull(factory).createAffineTransform(matrix);
+        }
+        return transform;
+    }
+
+    /**
+     * Computes the matrix of the linear approximation. This is the 
implementation of {@link #create(MathTransformFactory)}
+     * without the step creating the {@link LinearTransform} from a matrix. 
The {@link #correlations} field is set as a side
+     * effect of this method call.
+     */
+    @SuppressWarnings("serial")
+    private MatrixSIS fit() throws FactoryException {
+        final double[][] sources = this.sources;                    // Protect 
from changes.
+        final double[][] targets = this.targets;
+        if (targets == null) {
+            throw new InvalidGeodeticParameterException(noData());
+        }
+        final int sourceDim = (sources != null) ? sources.length : 
gridSize.length;
+        final int targetDim = targets.length;
+        correlations = new double[targetDim];
+        final MatrixSIS matrix = Matrices.create(targetDim + 1, sourceDim + 1, 
 ExtendedPrecisionMatrix.ZERO);
+        matrix.setElement(targetDim, sourceDim, 1);
+        for (int j=0; j < targetDim; j++) {
+            final double c;
+            switch (sourceDim) {
+                case 1: {
+                    final int row = j;
+                    final Line line = new Line() {
+                        @Override public void setEquation(final Number slope, 
final Number y0) {
+                            super.setEquation(slope, y0);
+                            matrix.setNumber(row, 0, slope);    // Preserve 
the extended precision (double-double).
+                            matrix.setNumber(row, 1, y0);
                         }
-                        break;
+                    };
+                    if (sources != null) {
+                        c = line.fit(vector(sources[0]), vector(targets[j]));
+                    } else {
+                        c = line.fit(Vector.createSequence(0, 1, gridSize[0]),
+                                     Vector.create(targets[j]));
                     }
-                    default: {
-                        throw new 
FactoryException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, 
sourceDim));
+                    break;
+                }
+                case 2: {
+                    final int row = j;
+                    final Plane plan = new Plane() {
+                        @Override public void setEquation(final Number sx, 
final Number sy, final Number z0) {
+                            super.setEquation(sx, sy, z0);
+                            matrix.setNumber(row, 0, sx);       // Preserve 
the extended precision (double-double).
+                            matrix.setNumber(row, 1, sy);
+                            matrix.setNumber(row, 2, z0);
+                        }
+                    };
+                    if (sources != null) {
+                        c = plan.fit(vector(sources[0]), vector(sources[1]), 
vector(targets[j]));
+                    } else try {
+                        c = plan.fit(gridSize[0], gridSize[1], 
Vector.create(targets[j]));
+                    } catch (IllegalArgumentException e) {
+                        // This may happen if the z vector still contain some 
"NaN" values.
+                        throw new InvalidGeodeticParameterException(noData(), 
e);
                     }
+                    break;
+                }
+                default: {
+                    throw new 
FactoryException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, 
sourceDim));
                 }
-                correlation[j] = c;
             }
-            transform = (LinearTransform) 
nonNull(factory).createAffineTransform(matrix);
+            correlations[j] = c;
         }
-        return transform;
+        return matrix;
     }
 
     /**
@@ -1102,43 +1309,137 @@ search:         for (int j=domain(); --j >= 0;) {
     }
 
     /**
-     * Returns the correlation coefficients of the last transform created by 
{@link #create create(…)},
-     * or {@code null} if none. If non-null, the array length is equals to the 
number of target
-     * dimensions.
+     * Returns a global estimation of correlation by computing the average of 
absolute values.
+     * We don't use {@link 
org.apache.sis.math.MathFunctions#magnitude(double...)} because it
+     * would result in values greater than 1.
+     */
+    private static double average(final double[] correlations) {
+        double sum = 0;
+        for (int i=0; i<correlations.length; i++) {
+            sum += Math.abs(correlations[i]);
+        }
+        return sum / correlations.length;
+    }
+
+    /**
+     * If all target coordinates have been projected to another space, returns 
the projection applied.
+     * This method returns a non-empty value only if all the following 
conditions are met:
      *
-     * @return estimation of correlation coefficients for each target 
dimension, or {@code null}.
+     * <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.
+     *
+     * @return the projection applied on target coordinates before to compute 
a linear transform.
+     *
+     * @since 1.0
+     */
+    public Optional<MathTransform> linearizer() {
+        return (appliedLinearizer != null) ? 
Optional.of(appliedLinearizer.projection) : Optional.empty();
+    }
+
+    /**
+     * Returns the Pearson correlation coefficients of the transform created 
by {@link #create create(…)}.
+     * The closer those coefficients are to +1 or -1, the better the fit.
+     * This method returns {@code null} if {@code create(…)} has not yet been 
invoked.
+     * If non-null, the array length is equal to the number of target 
dimensions.
+     *
+     * @return estimation of Pearson correlation coefficients for each target 
dimension,
+     *         or {@code null} if {@code create(…)} has not been invoked yet.
      */
     public double[] correlation() {
-        return (correlation != null) ? correlation.clone() : null;
+        return (correlations != null) ? correlations.clone() : null;
     }
 
     /**
      * Returns a string representation of this builder for debugging purpose.
+     * Current implementation shows the following information:
+     *
+     * <ul>
+     *   <li>Number of points.</li>
+     *   <li>Linearizers and their correlation coefficients (if 
available).</li>
+     *   <li>The linear transform (if already computed).</li>
+     * </ul>
+     *
+     * The string representation may change in any future version.
      *
      * @return a string representation of this builder.
      */
     @Override
     public String toString() {
-        final StringBuilder buffer = new 
StringBuilder(Classes.getShortClassName(this)).append('[');
-        if (sources != null) {
-            buffer.append(sources[0].length).append(" points");
+        final StringBuilder buffer = new 
StringBuilder(Classes.getShortClassName(this))
+                .append('[').append(numPoints).append(" points");
+        if (gridSize != null) {
+            String separator = " on ";
+            for (final int size : gridSize) {
+                buffer.append(separator).append(size);
+                separator = " × ";
+            }
+            buffer.append(" grid");
         }
         buffer.append(']');
+        final String lineSeparator = System.lineSeparator();
+        /*
+         * Example (from LinearTransformBuilderTest):
+         * ┌────────────┬─────────────┐
+         * │ Conversion │ Correlation │
+         * ├────────────┼─────────────┤
+         * │ x³ y²      │ 1.000000    │
+         * │ x² y³      │ 0.997437    │
+         * │ Identité   │ 0.995969    │
+         * └────────────┴─────────────┘
+         */
+        if (linearizers != null) {
+            buffer.append(':').append(lineSeparator);
+            Collections.sort(linearizers);
+            NumberFormat nf = null;
+            final TableAppender table = new TableAppender(buffer, " │ ");
+            table.appendHorizontalSeparator();
+            
table.append(Vocabulary.format(Vocabulary.Keys.Conversion)).nextColumn();
+            
table.append(Vocabulary.format(Vocabulary.Keys.Correlation)).nextLine();
+            table.appendHorizontalSeparator();
+            for (final ProjectedTransformTry alt : linearizers) {
+                nf = alt.summarize(table, nf);
+            }
+            table.appendHorizontalSeparator();
+            try {
+                table.flush();
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);      // Should never happen 
since we wrote into a StringBuilder.
+            }
+        }
+        /*
+         * Example:
+         * Result:
+         * ┌               ┐                 ┌        ┐
+         * │ 2.0  0    3.0 │   Correlation = │ 0.9967 │
+         * │ 0    1.0  1.0 │                 │ 0.9950 │
+         * │ 0    0    1   │                 └        ┘
+         * └               ┘
+         */
         if (transform != null) {
-            final String lineSeparator = System.lineSeparator();
+            if (linearizers != null) {
+                buffer.append(Vocabulary.format(Vocabulary.Keys.Result));
+            }
             buffer.append(':').append(lineSeparator);
             final TableAppender table = new TableAppender(buffer, " ");
             table.setMultiLinesCells(true);
             
table.append(Matrices.toString(transform.getMatrix())).nextColumn();
             table.append(lineSeparator).append("  ")
                  
.append(Vocabulary.format(Vocabulary.Keys.Correlation)).append(" 
=").nextColumn();
-            table.append(Matrices.create(correlation.length, 1, 
correlation).toString());
+            table.append(Matrices.create(correlations.length, 1, 
correlations).toString());
             try {
                 table.flush();
             } catch (IOException e) {
                 throw new UncheckedIOException(e);      // Should never happen 
since we wrote into a StringBuilder.
             }
         }
+        Strings.insertLineInLeftMargin(buffer, lineSeparator);
         return buffer.toString();
     }
 }
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 b3fb15a..a035ec9 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
@@ -61,6 +61,9 @@ import static 
org.apache.sis.referencing.operation.builder.ResidualGrid.SOURCE_D
  *   <li>Create a {@link InterpolatedTransform} with the above shift grid.</li>
  * </ol>
  *
+ * Builders can be used only once;
+ * points can not be added or modified after {@link 
#create(MathTransformFactory)} has been invoked.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  *
@@ -351,6 +354,7 @@ public class LocalizationGridBuilder extends 
TransformBuilder {
      * <i>etc.</i>. Coordinates are stored in row-major order (column index 
varies faster, followed by row index).
      *
      * @param  coordinates coordinates in each target dimensions, stored in 
row-major order.
+     * @throws IllegalStateException if {@link #create(MathTransformFactory) 
create(…)} has already been invoked.
      *
      * @since 1.0
      */
@@ -369,6 +373,7 @@ public class LocalizationGridBuilder extends 
TransformBuilder {
      * @param  gridX   the column index in the grid where to store the given 
target position.
      * @param  gridY   the row index in the grid where to store the given 
target position.
      * @param  target  the target coordinates, assumed uncertain.
+     * @throws IllegalStateException if {@link #create(MathTransformFactory) 
create(…)} has already been invoked.
      * @throws IllegalArgumentException if the {@code x} or {@code y} 
coordinate value is out of grid range.
      * @throws MismatchedDimensionException if the target position does not 
have the expected number of dimensions.
      */
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
new file mode 100644
index 0000000..5065b8f
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ProjectedTransformTry.java
@@ -0,0 +1,324 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.operation.builder;
+
+import java.util.Queue;
+import java.util.Arrays;
+import java.text.NumberFormat;
+import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.internal.referencing.Resources;
+import org.apache.sis.io.TableAppender;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.resources.Vocabulary;
+
+
+/**
+ * Information about an attempt to transform coordinates to some projection 
before to compute a linear approximation.
+ * This class contains only the projection to be attempted and a summary of 
the result. We do not keep new coordinates
+ * in order to avoid consuming too much memory when many attempts are made; 
{@link LinearTransformBuilder} needs only
+ * to keep the best attempt.
+ *
+ * <div class="note">
+ * <p><b>Purpose:</b> localization grids in netCDF files contain 
(<var>longitude</var>, <var>latitude</var>) values for all pixels.
+ * {@link LocalizationGridBuilder} first computes a linear (affine) 
approximation of a localization grid, then stores the residuals.
+ * This approach works well when the residuals are small. However if the 
localization grid is non-linear, then the affine transform
+ * is a poor approximation of that grid and the residuals are high. High 
residuals make inverse transforms hard to compute, which
+ * sometime cause a {@link TransformException} with <cite>"no 
convergence"</cite> error message.</p>
+ *
+ * <p>In practice, localization grids in netCDF files are often used for 
storing the results of a map projection, e.g. Mercator.
+ * This class allows {@link LocalizationGridBuilder} to try to transform the 
grid using a given list of map projections and see
+ * if one of those projections results in a grid closer to an affine 
transform. In other words, we use this class for trying to
+ * guess what the projection may be. It is okay if the guess is not a perfect 
match; if the residuals become smalls enough,
+ * it will resolve the "no convergence" errors.</p>
+ * </div>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> 
{
+    /**
+     * Number of points in the temporary buffer used for transforming data.
+     * The buffer length will be this capacity multiplied by the number of 
dimensions.
+     *
+     * @see 
org.apache.sis.referencing.operation.transform.AbstractMathTransform#MAXIMUM_BUFFER_SIZE
+     */
+    private static final int BUFFER_CAPACITY = 512;
+
+    /**
+     * A name by witch this projection attempt is identified.
+     */
+    private 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).
+     */
+    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 
{@code projection}
+     * map dimensions 2 and 1 of {@link LinearTransformBuilder#targets} 
respectively. The length
+     * of this array shall be equal to the number of {@link #projection} 
source dimensions.
+     */
+    private final int[] dimensions;
+
+    /**
+     * A global correlation factor, stored for information purpose only.
+     */
+    float correlation;
+
+    /**
+     * If an error occurred during coordinate operations, the error. Otherwise 
{@code null}.
+     */
+    private TransformException error;
+
+    /**
+     * 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;
+        dimensions  = null;
+        correlation = corr;
+    }
+
+    /**
+     * 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 projection         conversion from non-linear grid to something 
that may be more linear.
+     * @param dimensions         maps {@code projection} dimensions to {@link 
LinearTransformBuilder} target dimensions.
+     * @param expectedDimension  number of {@link LinearTransformBuilder} 
target dimensions.
+     */
+    ProjectedTransformTry(final String name, final MathTransform projection, 
final int[] dimensions, int expectedDimension) {
+        this.name       = name;
+        this.projection = projection;
+        this.dimensions = dimensions;
+        int side = 0;                           // 0 = problem with source 
dimensions, 1 = problem with target dimensions.
+        int actual = projection.getSourceDimensions();
+        if (actual <= expectedDimension) {
+            expectedDimension = dimensions.length;
+            if (actual == expectedDimension) {
+                actual = projection.getTargetDimensions();
+                if (actual == expectedDimension) {
+                    return;
+                }
+                side = 1;
+            }
+        }
+        throw new MismatchedDimensionException(Resources.format(
+                Resources.Keys.MismatchedTransformDimension_3, side, 
expectedDimension, actual));
+    }
+
+    /**
+     * 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 #dimensions} will be read; the 
other arrays will be ignored.
+     * The coordinate operation result will be stored in arrays of size {@code 
[numDimensions][numPoints]}
+     * where {@code numDimensions} is the length of the {@link #dimensions} 
array. Indices are as below,
+     * with 0 ≦ <var>d</var> ≦ {@code numDimensions}:
+     *
+     * <ol>
+     *   <li>{@code results[d]}    contains the coordinates in dimension 
<var>d</var>.</li>
+     *   <li>{@code results[d][i]} is a coordinate of the point at index 
<var>i</var>.</li>
+     * </ol>
+     *
+     * 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.
+     *
+     * @param  coordinates  the {@link LinearTransformBuilder#targets} arrays 
of coordinates to transform.
+     * @param  numPoints    number of points to transform: {@code numPoints} ≦ 
{@code coordinates[i].length}.
+     * @param  pool         pre-allocated arrays of length {@code numPoints} 
that can be recycled.
+     * @return results of coordinate operations (see method javadoc), or 
{@code null} if an error occurred.
+     */
+    final double[][] transform(final double[][] coordinates, final int 
numPoints, final Queue<double[]> pool) {
+        final int numDimensions = dimensions.length;
+        final double[][] results = new double[numDimensions][];
+        for (int i=0; i<numDimensions; i++) {
+            if ((results[i] = pool.poll()) == null) {
+                results[i] = new double[numPoints];
+            }
+        }
+        /*
+         * Allocate the destination arrays for coordinates to transform as 
(x₀,y₀), (x₁,y₁), (x₂,y₂)… tuples.
+         * In the particular case of one-dimensional transforms (not 
necessarily one-dimensional coordinates)
+         * we can transform arrays directly without the need for a temporary 
buffer.
+         */
+        try {
+            if (numDimensions == 1) {
+                projection.transform(coordinates[dimensions[0]], 0, 
results[0], 0, numPoints);
+            } else {
+                final int bufferCapacity = Math.min(numPoints, 
BUFFER_CAPACITY);                 // In number of points.
+                final double[] buffer = new double[bufferCapacity * 
numDimensions];
+                int dataOffset = 0;
+                while (dataOffset < numPoints) {
+                    final int start = dataOffset;
+                    final int stop = Math.min(start + bufferCapacity, 
numPoints);
+                    /*
+                     * Copies coordinates in a single interleaved array before 
to transform them.
+                     * Coordinates start at index 0 and the number of valid 
points is stop - start.
+                     */
+                    for (int d=0; d<numDimensions; d++) {
+                        final double[] data = coordinates[dimensions[d]];
+                        dataOffset = start;
+                        int dst = d;
+                        do {
+                            buffer[dst] = data[dataOffset];
+                            dst += numDimensions;
+                        } while (++dataOffset < stop);
+                    }
+                    /*
+                     * Transform coordinates and save the result.
+                     */
+                    projection.transform(buffer, 0, buffer, 0, stop - start);
+                    for (int d=0; d<numDimensions; d++) {
+                        @SuppressWarnings("MismatchedReadAndWriteOfArray")
+                        final double[] data = results[d];
+                        dataOffset = start;
+                        int dst = d;
+                        do {
+                            data[dataOffset] = buffer[dst];
+                            dst += numDimensions;
+                        } while (++dataOffset < stop);
+                    }
+                }
+            }
+        } catch (TransformException e) {
+            error = e;
+            recycle(results, pool);         // Make arrays available for other 
transforms.
+            return null;
+        }
+        return results;
+    }
+
+    /**
+     * Makes the given arrays available for reuse by other transforms.
+     */
+    static void recycle(final double[][] arrays, final Queue<double[]> pool) {
+        if (arrays != null) {
+            pool.addAll(Arrays.asList(arrays));
+        }
+    }
+
+    /**
+     * Replaces old correlation values by new values in a copy of the given 
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.isSequence(0, 
dimensions)) {
+            return newValues;
+        }
+        correlations = correlations.clone();
+        for (int j=0; j<dimensions.length; j++) {
+            correlations[dimensions[j]] = newValues[j];
+        }
+        return correlations;
+    }
+
+    /**
+     * Replaces old transform coefficients by new values in a copy of the 
given 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) {
+        /*
+         * 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.isSequence(0, dimensions)) {
+            return newValues;
+        }
+        transform = transform.clone();
+        for (int j=0; j<dimensions.length; j++) {
+            final int d = dimensions[j];
+            for (int i=transform.getNumRow(); --i >= 0;) {
+                transform.setNumber(d, i, newValues.getNumber(j, i));
+            }
+        }
+        return transform;
+    }
+
+    /**
+     * Order by the inverse of correlation coefficients. Highest coefficients 
(best correlations)
+     * are first, lower coefficients are next, {@link Float#NaN} values are 
last.
+     */
+    @Override
+    public int compareTo(final ProjectedTransformTry other) {
+        return Float.compare(-correlation, -other.correlation);
+    }
+
+    /**
+     * Formats a summary of this projection attempt. This method formats the 
following columns:
+     *
+     * <ol>
+     *   <li>The projection name.</li>
+     *   <li>The corelation coefficient, or the error message if an error 
occurred.</li>
+     * </ol>
+     *
+     * @param  table  the table where to write a row.
+     * @param  nf     format to use for writing coefficients, or {@code null} 
if not yet created.
+     * @return format used for writing coefficients, or {@code null}.
+     */
+    final NumberFormat summarize(final TableAppender table, NumberFormat nf) {
+        if (name == null) {
+            name = Vocabulary.format(projection == null ? 
Vocabulary.Keys.Identity : Vocabulary.Keys.Unnamed);
+        }
+        table.append(name).nextColumn();
+        String message = "";
+        if (error != null) {
+            message = error.getMessage();
+            if (message == null) {
+                message = error.getClass().getSimpleName();
+            }
+        } else if (correlation > 0) {
+            if (nf == null) {
+                nf = NumberFormat.getInstance();
+                nf.setMinimumFractionDigits(6);         // Math.ulp(1f) ≈ 
1.2E-7
+                nf.setMaximumFractionDigits(6);
+            }
+            message = nf.format(correlation);
+        }
+        table.append(message).nextLine();
+        return nf;
+    }
+
+    /**
+     * Returns a string representation of this projection attempt for 
debugging purpose.
+     */
+    @Override
+    public String toString() {
+        final TableAppender buffer = new TableAppender("  ");
+        summarize(buffer, null);
+        return buffer.toString();
+    }
+}
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/TransformBuilder.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/TransformBuilder.java
index ef621d7..3a60ed1 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/TransformBuilder.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/TransformBuilder.java
@@ -28,6 +28,9 @@ import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactor
  * The transform may be a linear approximation the minimize the errors in a 
<cite>least square</cite> sense,
  * or a more accurate transform using a localization grid.
  *
+ * <p>Builders can be used only once;
+ * points can not be added or modified after {@link 
#create(MathTransformFactory)} has been invoked.</p>
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 0.8
  * @since   0.8
@@ -42,6 +45,9 @@ public abstract class TransformBuilder {
 
     /**
      * Creates a transform from the source points to the target points.
+     * Invoking this method puts the builder in an unmodifiable state.
+     * Invoking this method more than once returns the same transform
+     * (the transform is not recomputed).
      *
      * @param  factory  the factory to use for creating the transform, or 
{@code null} for the default factory.
      * @return the transform from source to target points.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
index 3ac113d..5664883 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
@@ -581,7 +581,7 @@ public abstract class AbstractMathTransform extends 
FormattableObject
             } catch (TransformException exception) {
                 /*
                  * If an exception occurred but the transform nevertheless 
declares having been
-                 * able to process all coordinate points (setting to NaN those 
that can't be
+                 * able to process all coordinate points (setting to NaN those 
that can not be
                  * transformed), we will keep the first exception (to be 
propagated at the end
                  * of this method) and continue. Otherwise we will stop 
immediately.
                  */
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 1deb313..582eaa1 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
@@ -19,9 +19,12 @@ package org.apache.sis.referencing.operation.builder;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Random;
+import java.util.Collections;
 import java.awt.geom.AffineTransform;
 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.geometry.DirectPosition1D;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.test.DependsOnMethod;
@@ -30,7 +33,6 @@ import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
 import static org.apache.sis.test.Assert.*;
-import org.opengis.geometry.DirectPosition;
 
 
 /**
@@ -406,4 +408,37 @@ public final strictfp class LinearTransformBuilderTest 
extends TestCase {
         assertTrue (actual.containsValue(t00));
         assertMapEquals(expected, actual);
     }
+
+    /**
+     * Tests the effect of {@link LinearTransformBuilder#addLinearizers(Map, 
int...)}.
+     *
+     * @throws FactoryException if the transform can not be created.
+     */
+    @Test
+    public void testLinearizers() throws FactoryException {
+        final int width  = 3;
+        final int height = 4;
+        final LinearTransformBuilder builder = new 
LinearTransformBuilder(width, height);
+        for (int y=0; y<height; y++) {
+            final int[]    source = new int[2];
+            final double[] target = new double[2];
+            for (int x=0; x<width; x++) {
+                source[0] = x;
+                source[1] = y;
+                target[0] = StrictMath.cbrt(3 + x*2);
+                target[1] = StrictMath.sqrt(1 + y);
+                builder.setControlPoint(source, target);
+            }
+        }
+        final NonLinearTransform x2y3 = new NonLinearTransform();
+        final NonLinearTransform x3y2 = new NonLinearTransform();
+        builder.addLinearizers(Collections.singletonMap("x² y³", x2y3));
+        builder.addLinearizers(Collections.singletonMap("x³ y²", x3y2), 1, 0);
+        final Matrix m = builder.create(null).getMatrix();
+        assertSame("linearizer", x3y2, builder.linearizer().get());
+        assertMatrixEquals("linear",
+                new Matrix3(2, 0, 3,
+                            0, 1, 1,
+                            0, 0, 1), m, 1E-15);
+    }
 }
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/NonLinearTransform.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/NonLinearTransform.java
new file mode 100644
index 0000000..4dea19b
--- /dev/null
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/NonLinearTransform.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.operation.builder;
+
+import org.opengis.referencing.operation.Matrix;
+import org.apache.sis.referencing.operation.transform.AbstractMathTransform2D;
+
+
+/**
+ * A two-dimensional non-linear transform for {@link 
LinearTransformBuilderTest} purpose.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final strictfp class NonLinearTransform extends AbstractMathTransform2D {
+    /**
+     * Creates a new instance of this class.
+     */
+    NonLinearTransform() {
+    }
+
+    /**
+     * Applies an arbitrary non-linear transform.
+     */
+    @Override
+    public Matrix transform(final double[] srcPts, int srcOff,
+                            final double[] dstPts, int dstOff, boolean 
derivate)
+    {
+        final double x = srcPts[srcOff++];
+        final double y = srcPts[srcOff  ];
+        dstPts[dstOff++] = x * x;
+        dstPts[dstOff  ] = y * y * y;
+        return null;
+    }
+}
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 117dcb0..19b4cef 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
@@ -184,10 +184,31 @@ public final class Strings extends Static {
     }
 
     /**
+     * Inserts a continuation character after each line separator except the 
last one.
+     * The intent is to show that a block of lines are part of the same 
element.
+     * The characters are the same than {@link 
org.apache.sis.util.logging.MonolineFormatter}.
+     *
+     * @param buffer         the buffer where to insert a continuation 
character in the left margin.
+     * @param lineSeparator  the line separator.
+     */
+    public static void insertLineInLeftMargin(final StringBuilder buffer, 
final String lineSeparator) {
+        char c = '╹';
+        int i = CharSequences.skipTrailingWhitespaces(buffer, 0, 
buffer.length());
+        while ((i = buffer.lastIndexOf(lineSeparator, i - 1)) >= 0) {
+            buffer.insert(i + lineSeparator.length(), c);
+            c = '┃';
+        }
+    }
+
+    /**
      * Returns a string representation of an instance of the given class 
having the given properties.
      * This is a convenience method for implementation of {@link 
Object#toString()} methods that are
      * used mostly for debugging purpose.
      *
+     * <p>The content is specified by (<var>key</var>=<var>value</var>) pairs. 
If a value is {@code null},
+     * the whole entry is omitted. If a key is {@code null}, the value is 
written without the {@code "key="}
+     * part. The later happens typically when the first value is the object 
name.</p>
+     *
      * @param  classe      the class to format.
      * @param  properties  the (<var>key</var>=<var>value</var>) pairs.
      * @return a string representation of an instance of the given class 
having the given properties.
@@ -201,7 +222,10 @@ public final class Strings extends Static {
                 if (isNext) {
                     buffer.append(", ");
                 }
-                buffer.append(properties[i-1]).append('=');
+                final Object name = properties[i-1];
+                if (name != null) {
+                    buffer.append(name).append('=');
+                }
                 final boolean isText = (value instanceof CharSequence);
                 if (isText) buffer.append('“');
                 buffer.append(value);
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/Plane.java 
b/core/sis-utility/src/main/java/org/apache/sis/math/Plane.java
index 1dd05ae..e8e6b2e 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/Plane.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/Plane.java
@@ -242,6 +242,7 @@ public class Plane implements Cloneable, Serializable {
      * @param  y  vector of <var>y</var> coordinates.
      * @param  z  vector of <var>z</var> values.
      * @return an estimation of the Pearson correlation coefficient.
+     *         The closer this coefficient is to +1 or -1, the better the fit.
      * @throws IllegalArgumentException if <var>x</var>, <var>y</var> and 
<var>z</var> do not have the same length.
      */
     public double fit(final double[] x, final double[] y, final double[] z) {
@@ -263,6 +264,7 @@ public class Plane implements Cloneable, Serializable {
      * @param  y  vector of <var>y</var> coordinates.
      * @param  z  vector of <var>z</var> values.
      * @return an estimation of the Pearson correlation coefficient.
+     *         The closer this coefficient is to +1 or -1, the better the fit.
      * @throws IllegalArgumentException if <var>x</var>, <var>y</var> and 
<var>z</var> do not have the same length.
      *
      * @since 0.8
@@ -311,6 +313,7 @@ public class Plane implements Cloneable, Serializable {
      * @param  ny  number of rows.
      * @param  z   values of a matrix of {@code nx} columns by {@code ny} rows 
organized in a row-major fashion.
      * @return an estimation of the Pearson correlation coefficient.
+     *         The closer this coefficient is to +1 or -1, the better the fit.
      * @throws IllegalArgumentException if <var>z</var> does not have the 
expected length or if a <var>z</var>
      *         value is {@link Double#NaN}.
      *
@@ -340,6 +343,7 @@ public class Plane implements Cloneable, Serializable {
      *
      * @param  points  the three-dimensional points.
      * @return an estimation of the Pearson correlation coefficient.
+     *         The closer this coefficient is to +1 or -1, the better the fit.
      * @throws MismatchedDimensionException if a point is not 
three-dimensional.
      */
     public double fit(final Iterable<? extends DirectPosition> points) {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
index a6dd7a7..26079ae 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
@@ -1259,7 +1259,7 @@ public final class ArraysExt extends Static {
     }
 
     /**
-     * Returns a finite arithmetic progression of the given length. Each value 
is increased by 1.
+     * Returns a finite arithmetic progression of the given length and common 
difference of 1.
      * For example {@code sequence(-1, 4)} returns {@code {-1, 0, 1, 2}}.
      *
      * <div class="note"><b>Purpose:</b>
@@ -1302,7 +1302,8 @@ public final class ArraysExt extends Static {
     }
 
     /**
-     * Returns {@code true} if the given array is a finite arithmetic 
progression starting at the given value.
+     * Returns {@code true} if the given array is a finite arithmetic 
progression starting at the given value
+     * and having a common difference of 1.
      * More specifically:
      *
      * <ul>
@@ -1318,6 +1319,9 @@ public final class ArraysExt extends Static {
      * {@code isSequence(1, array)} returns {@code true} if the given array is 
{@code {1, 2, 3, 4}}
      * but {@code false} if the array is {@code {1, 2, 4}} (missing 3).</div>
      *
+     * This method is useful when {@code array} is an argument specified to 
another method, and determining that the
+     * argument values are {@code start}, {@code start}+1, {@code start}+2, 
<i>etc.</i> allows some optimizations.
+     *
      * @param  start  first value expected in the given {@code array}.
      * @param  array  the array to test, or {@code null}.
      * @return {@code true} if the given array is non-null and equal to
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index 5f20a54..c3f14cf 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -682,6 +682,11 @@ public final class Vocabulary extends 
IndexedResourceBundle {
         public static final short Resolution = 153;
 
         /**
+         * Result
+         */
+        public static final short Result = 164;
+
+        /**
          * Root
          */
         public static final short Root = 90;
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index b3ff71a..e5c62c0 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -139,6 +139,7 @@ Remarks                 = Remarks
 RemoteConfiguration     = Remote configuration
 RepresentativeValue     = Representative value
 Resolution              = Resolution
+Result                  = Result
 Root                    = Root
 RootMeanSquare          = Root Mean Square
 SampleDimensions        = Sample dimensions
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index 5c4f7d5..30d2755 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -146,6 +146,7 @@ Remarks                 = Remarques
 RemoteConfiguration     = Configuration distante
 RepresentativeValue     = Valeur repr\u00e9sentative
 Resolution              = R\u00e9solution
+Result                  = R\u00e9sultat
 Root                    = Racine
 RootMeanSquare          = Moyenne quadratique
 SampleDimensions        = Dimensions d\u2019\u00e9chantillonnage
diff --git a/ide-project/NetBeans/nbproject/genfiles.properties 
b/ide-project/NetBeans/nbproject/genfiles.properties
index ec9b777..35088b0 100644
--- a/ide-project/NetBeans/nbproject/genfiles.properties
+++ b/ide-project/NetBeans/nbproject/genfiles.properties
@@ -3,6 +3,6 @@
 build.xml.data.CRC32=58e6b21c
 build.xml.script.CRC32=462eaba0
 [email protected]
-nbproject/build-impl.xml.data.CRC32=80e7865b
+nbproject/build-impl.xml.data.CRC32=6673fb19
 nbproject/build-impl.xml.script.CRC32=a7689f96
 nbproject/[email protected]
diff --git a/ide-project/NetBeans/nbproject/project.xml 
b/ide-project/NetBeans/nbproject/project.xml
index 3d5b601..e24aedb 100644
--- a/ide-project/NetBeans/nbproject/project.xml
+++ b/ide-project/NetBeans/nbproject/project.xml
@@ -99,6 +99,8 @@
             <word>initially</word>
             <word>javadoc</word>
             <word>kilometre</word>
+            <word>linearizer</word>
+            <word>linearizers</word>
             <word>loggings</word>
             <word>maintainance</word>
             <word>marshallable</word>

Reply via email to