sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 03/03: Allow LocalizationGridBuilder to use the "linearizers" functionality added in LinearTransformBuilder.
Date Sun, 10 Mar 2019 22:08:11 GMT
This is an automated email from the ASF dual-hosted git repository.

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

commit fd7b9d70643b945d21fc5764d08745612da54ef1
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sun Mar 10 22:36:57 2019 +0100

    Allow LocalizationGridBuilder to use the "linearizers" functionality added in LinearTransformBuilder.
---
 .../operation/builder/LinearTransformBuilder.java  | 151 ++++++----
 .../operation/builder/LocalizationGridBuilder.java | 321 ++++++++++++++++-----
 .../operation/builder/ProjectedTransformTry.java   |  35 ++-
 .../operation/builder/TransformBuilder.java        |   2 +-
 .../java/org/apache/sis/internal/util/Strings.java |   7 +
 .../org/apache/sis/util/resources/Vocabulary.java  |  15 +
 .../sis/util/resources/Vocabulary.properties       |   3 +
 .../sis/util/resources/Vocabulary_fr.properties    |   3 +
 ide-project/NetBeans/nbproject/genfiles.properties |   2 +-
 ide-project/NetBeans/nbproject/project.xml         |   1 +
 10 files changed, 395 insertions(+), 145 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 8f8586a..c1492e2 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
@@ -25,6 +25,7 @@ import java.util.ArrayDeque;
 import java.util.Collections;
 import java.util.NoSuchElementException;
 import java.util.Optional;
+import java.util.Locale;
 import java.text.NumberFormat;
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -68,7 +69,7 @@ import org.apache.sis.util.Classes;
  * Otherwise a builder created by the {@link #LinearTransformBuilder()} constructor will be able to handle
  * randomly distributed coordinates.
  *
- * <p>Builders can be used only once;
+ * <p>Builders are not thread-safe. 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>
@@ -353,10 +354,26 @@ search: for (int j=numPoints; --j >= 0;) {
     }
 
     /**
+     * Returns {@code true} if {@link #create(MathTransformFactory)} has not yet been invoked.
+     */
+    final boolean isModifiable() {
+        return transform == null;
+    }
+
+    /**
+     * Throws {@link IllegalStateException} if this builder can not be modified anymore.
+     */
+    private void ensureModifiable() throws IllegalStateException {
+        if (transform != null) {
+            throw new IllegalStateException(Errors.format(Errors.Keys.UnmodifiableObject_1, LinearTransformBuilder.class));
+        }
+    }
+
+    /**
      * Verifies that the given number of dimensions is equal to the expected value.
      * No verification are done if the source point is the first point of randomly distributed points.
      */
-    private void verifySourceDimension(final int actual) {
+    private void verifySourceDimension(final int actual) throws MismatchedDimensionException {
         final int expected;
         if (gridSize != null) {
             expected = gridSize.length;
@@ -388,14 +405,6 @@ 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.
      *
@@ -544,9 +553,7 @@ 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());
-        }
+        ensureModifiable();
         ArgumentChecks.ensureNonNull("sourceToTarget", sourceToTarget);
         sources    = null;
         targets    = null;
@@ -876,9 +883,7 @@ 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());
-        }
+        ensureModifiable();
         ArgumentChecks.ensureNonNull("source", source);
         ArgumentChecks.ensureNonNull("target", target);
         verifySourceDimension(source.length);
@@ -994,10 +999,8 @@ search:         for (int j=domain(); --j >= 0;) {
      * @throws IllegalStateException if {@link #create(MathTransformFactory) create(…)} has already been invoked.
      */
     final void setControlPoints(final Vector[] coordinates) {
+        // ensureModifiable() invoked by LocalizationGridBuilder; it does not need to be invoked again here.
         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++) {
@@ -1052,9 +1055,7 @@ search:         for (int j=domain(); --j >= 0;) {
      * @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());
-        }
+        // ensureModifiable() invoked by LocalizationGridBuilder; it does not need to be invoked again here.
         final double[] coordinates = targets[dimension];
         int stride = 1;
         for (int i=0; i<direction; i++) {
@@ -1121,7 +1122,7 @@ search:         for (int j=domain(); --j >= 0;) {
      *
      * <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.
+     * identifiers used in {@link #toString()} for debugging purpose.
      * The {@code dimensions} argument specifies which target dimensions to project and can be null or omitted
      * if the projections shall be applied on all target coordinates. It is possible to invoke this method many
      * times with different {@code dimensions} argument values.</p>
@@ -1138,9 +1139,7 @@ search:         for (int j=domain(); --j >= 0;) {
      * @since 1.0
      */
     public void addLinearizers(final Map<String,MathTransform> projections, int... dimensions) {
-        if (transform != null) {
-            throw new IllegalStateException(unmodifiable());
-        }
+        ensureModifiable();
         final int tgtDim = getTargetDimensions();
         if (dimensions == null || dimensions.length == 0) {
             dimensions = ArraysExt.range(0, tgtDim);
@@ -1157,12 +1156,24 @@ search:         for (int j=domain(); --j >= 0;) {
     }
 
     /**
+     * Sets the linearizers to a copy of those of the given builder.
+     */
+    final void setLinearizers(final LinearTransformBuilder other) {
+        if (other.linearizers != null) {
+            linearizers = new ArrayList<>(other.linearizers);
+            linearizers.replaceAll(ProjectedTransformTry::new);
+        }
+    }
+
+    /**
      * 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.
      *
+     * <p>If this method is invoked more than once, the previously created transform instance is returned.</p>
+     *
      * @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
      *                  shall return {@link LinearTransform} instances.
@@ -1184,7 +1195,8 @@ search:         for (int j=domain(); --j >= 0;) {
                  * 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);
+                final double sqrtLength      = Math.sqrt(correlations.length);
+                double     bestCorrelation   = rms(correlations, sqrtLength);
                 double[]   bestCorrelations  = null;
                 MatrixSIS  bestTransform     = null;
                 double[][] transformedArrays = null;
@@ -1207,7 +1219,7 @@ search:         for (int j=domain(); --j >= 0;) {
                     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);
+                        final double    altCorrelation  = rms(altCorrelations, sqrtLength);
                         alt.correlation = (float) altCorrelation;
                         if (altCorrelation > bestCorrelation) {
                             ProjectedTransformTry.recycle(transformedArrays, pool);
@@ -1309,16 +1321,10 @@ search:         for (int j=domain(); --j >= 0;) {
     }
 
     /**
-     * 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.
+     * Returns a global estimation of correlation by computing the root mean square of values.
      */
-    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;
+    private static double rms(final double[] correlations, final double sqrtLength) {
+        return org.apache.sis.math.MathFunctions.magnitude(correlations) / sqrtLength;
     }
 
     /**
@@ -1344,6 +1350,13 @@ search:         for (int j=domain(); --j >= 0;) {
     }
 
     /**
+     * Returns the identifier of the linearizer, or {@code null} if none.
+     */
+    final String linearizerID() {
+        return (appliedLinearizer != null) ? appliedLinearizer.name() : null;
+    }
+
+    /**
      * 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.
@@ -1372,8 +1385,31 @@ search:         for (int j=domain(); --j >= 0;) {
      */
     @Override
     public String toString() {
-        final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this))
-                .append('[').append(numPoints).append(" points");
+        final StringBuilder buffer = new StringBuilder(400);
+        final String lineSeparator;
+        try {
+            lineSeparator = appendTo(buffer, getClass(), null, Vocabulary.Keys.Result);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        Strings.insertLineInLeftMargin(buffer, lineSeparator);
+        return buffer.toString();
+    }
+
+    /**
+     * Appends a string representation of this builder into the given buffer.
+     *
+     * @param  buffer     where to append the string representation.
+     * @param  caller     the class name to report.
+     * @param  locale     the locale for formatting messages and some numbers, or {@code null} for the default.
+     * @param  resultKey  either {@code Vocabulary.Keys.Result} or {@code Vocabulary.Keys.LinearTransformation}.
+     * @return the line separator, for convenience of callers who wants to append more content.
+     * @throws IOException should never happen because we write in a {@link StringBuilder}.
+     */
+    final String appendTo(final StringBuilder buffer, final Class<?> caller, final Locale locale, final short resultKey) throws IOException {
+        final String lineSeparator = System.lineSeparator();
+        final Vocabulary vocabulary = Vocabulary.getResources(locale);
+        buffer.append(Classes.getShortName(caller)).append('[').append(numPoints).append(" points");
         if (gridSize != null) {
             String separator = " on ";
             for (final int size : gridSize) {
@@ -1382,8 +1418,7 @@ search:         for (int j=domain(); --j >= 0;) {
             }
             buffer.append(" grid");
         }
-        buffer.append(']');
-        final String lineSeparator = System.lineSeparator();
+        buffer.append(']').append(lineSeparator);
         /*
          * Example (from LinearTransformBuilderTest):
          * ┌────────────┬─────────────┐
@@ -1395,23 +1430,21 @@ search:         for (int j=domain(); --j >= 0;) {
          * └────────────┴─────────────┘
          */
         if (linearizers != null) {
-            buffer.append(':').append(lineSeparator);
+            buffer.append(Strings.CONTINUATION_ITEM);
+            vocabulary.appendLabel(Vocabulary.Keys.Preprocessing, buffer);
+            buffer.append(lineSeparator);
             Collections.sort(linearizers);
             NumberFormat nf = null;
             final TableAppender table = new TableAppender(buffer, " │ ");
             table.appendHorizontalSeparator();
-            table.append(Vocabulary.format(Vocabulary.Keys.Conversion)).nextColumn();
-            table.append(Vocabulary.format(Vocabulary.Keys.Correlation)).nextLine();
+            table.append(vocabulary.getString(Vocabulary.Keys.Conversion)).nextColumn();
+            table.append(vocabulary.getString(Vocabulary.Keys.Correlation)).nextLine();
             table.appendHorizontalSeparator();
             for (final ProjectedTransformTry alt : linearizers) {
-                nf = alt.summarize(table, nf);
+                nf = alt.summarize(table, nf, locale);
             }
             table.appendHorizontalSeparator();
-            try {
-                table.flush();
-            } catch (IOException e) {
-                throw new UncheckedIOException(e);      // Should never happen since we wrote into a StringBuilder.
-            }
+            table.flush();
         }
         /*
          * Example:
@@ -1423,23 +1456,17 @@ search:         for (int j=domain(); --j >= 0;) {
          * └               ┘
          */
         if (transform != null) {
-            if (linearizers != null) {
-                buffer.append(Vocabulary.format(Vocabulary.Keys.Result));
-            }
-            buffer.append(':').append(lineSeparator);
+            buffer.append(Strings.CONTINUATION_ITEM);
+            vocabulary.appendLabel(resultKey, buffer);
+            buffer.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();
+                 .append(vocabulary.getString(Vocabulary.Keys.Correlation)).append(" =").nextColumn();
             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.
-            }
+            table.flush();
         }
-        Strings.insertLineInLeftMargin(buffer, lineSeparator);
-        return buffer.toString();
+        return lineSeparator;
     }
 }
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 a035ec9..aafea53 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
@@ -16,6 +16,11 @@
  */
 package org.apache.sis.referencing.operation.builder;
 
+import java.util.Map;
+import java.util.Locale;
+import java.util.Optional;
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.MismatchedDimensionException;
@@ -24,6 +29,7 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 import org.apache.sis.referencing.operation.transform.InterpolatedTransform;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -33,10 +39,14 @@ import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.internal.util.Strings;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.math.Statistics;
+import org.apache.sis.math.StatisticsFormat;
 import org.apache.sis.math.Vector;
 
 import static org.apache.sis.referencing.operation.builder.ResidualGrid.SOURCE_DIMENSION;
@@ -45,7 +55,7 @@ import static org.apache.sis.referencing.operation.builder.ResidualGrid.SOURCE_D
 /**
  * Creates an "almost linear" transform mapping the given source points to the given target points.
  * The transform is backed by a <cite>grid of localization</cite>, a two-dimensional array of coordinate points.
- * Grid size is {@code width} × {@code height} and input coordinates are (<var>i</var>,<var>j</var>) index in the grid,
+ * Grid size is {@code width} × {@code height} and input coordinates are (<var>i</var>,<var>j</var>) indices in the grid,
  * where <var>i</var> must be in the [0…{@code width}-1] range and <var>j</var> in the [0…{@code height}-1] range inclusive.
  * Output coordinates are the values stored in the grid of localization at the specified index.
  * After a {@code LocalizationGridBuilder} instance has been fully populated (i.e. real world coordinates have been
@@ -54,16 +64,25 @@ import static org.apache.sis.referencing.operation.builder.ResidualGrid.SOURCE_D
  * then an instance of {@link LinearTransform} is returned.
  * Otherwise, a transform backed by the localization grid is returned.
  *
- * <p>This builder performs two steps:</p>
+ * <p>This builder performs the following steps:</p>
  * <ol>
  *   <li>Compute a linear approximation of the transformation using {@link LinearTransformBuilder}.</li>
  *   <li>Compute {@link DatumShiftGrid} with the residuals.</li>
  *   <li>Create a {@link InterpolatedTransform} with the above shift grid.</li>
+ *   <li>If a {@linkplain LinearTransformBuilder#linearizer() linearizer has been applied},
+ *       concatenate the inverse transform of that linearizer.</li>
  * </ol>
  *
- * Builders can be used only once;
+ * Builders are not thread-safe. Builders can be used only once;
  * points can not be added or modified after {@link #create(MathTransformFactory)} has been invoked.
  *
+ * <div class="section">Linearizers</div>
+ * If the localization grid is not close enough to a linear transform, {@link InterpolatedTransform} may not converge.
+ * To improve the speed and reliability of the transform, a non-linear step can be {@linkplain #addLinearizers specified}.
+ * Many candidates can be specified in case the exact form of that non-linear step is unknown;
+ * {@code LocalizationGridBuilder} will select the non-linear step that provides the best improvement, if any.
+ * See the <cite>Linearizers</cite> section in {@link LinearTransformBuilder} for more discussion.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  *
@@ -115,6 +134,11 @@ public class LocalizationGridBuilder extends TransformBuilder {
     static final double DEFAULT_PRECISION = 1E-7;
 
     /**
+     * The transform created by {@link #create(MathTransformFactory)}.
+     */
+    private MathTransform transform;
+
+    /**
      * Creates a new, initially empty, builder for a localization grid of the given size.
      *
      * @param width   the number of columns in the grid of target positions.
@@ -197,6 +221,7 @@ public class LocalizationGridBuilder extends TransformBuilder {
                     } catch (NoninvertibleTransformException e) {
                         throw (ArithmeticException) new ArithmeticException(e.getLocalizedMessage()).initCause(e);
                     }
+                    linear.setLinearizers(localizations);
                     return;
                 }
             }
@@ -254,6 +279,15 @@ public class LocalizationGridBuilder extends TransformBuilder {
     }
 
     /**
+     * Throws {@link IllegalStateException} if this builder can not be modified anymore.
+     */
+    private void ensureModifiable() throws IllegalStateException {
+        if (!linear.isModifiable()) {
+            throw new IllegalStateException(Errors.format(Errors.Keys.UnmodifiableObject_1, LocalizationGridBuilder.class));
+        }
+    }
+
+    /**
      * Sets the desired precision of <em>inverse</em> transformations, in units of source coordinates.
      * If a conversion from "real world" to grid coordinates {@linkplain #setSourceToGrid has been specified},
      * then the given precision is in "real world" units. Otherwise the precision is in units of grid cells
@@ -265,11 +299,13 @@ public class LocalizationGridBuilder extends TransformBuilder {
      * forward and inverse transformations still limited by the accuracy of given control points and the grid resolution.
      * </div>
      *
-     * @param precision  desired precision of the results of inverse transformations.
+     * @param  precision  desired precision of the results of inverse transformations.
+     * @throws IllegalStateException if {@link #create(MathTransformFactory) create(…)} has already been invoked.
      *
      * @see DatumShiftGrid#getCellPrecision()
      */
     public void setDesiredPrecision(final double precision) {
+        ensureModifiable();
         ArgumentChecks.ensureStrictlyPositive("precision", precision);
         this.precision = precision;
     }
@@ -315,11 +351,13 @@ public class LocalizationGridBuilder extends TransformBuilder {
      * If a {@linkplain #setDesiredPrecision(double) desired precision} has been specified before this method call,
      * it is caller's responsibility to convert that value to new source units if needed.
      *
-     * @param sourceToGrid  conversion from the "real world" source coordinates to grid indices including fractional parts.
+     * @param  sourceToGrid  conversion from the "real world" source coordinates to grid indices including fractional parts.
+     * @throws IllegalStateException if {@link #create(MathTransformFactory) create(…)} has already been invoked.
      *
      * @see DatumShiftGrid#getCoordinateToGrid()
      */
     public void setSourceToGrid(final LinearTransform sourceToGrid) {
+        ensureModifiable();
         ArgumentChecks.ensureNonNull("sourceToGrid", sourceToGrid);
         int isTarget = 0;
         int dim = sourceToGrid.getSourceDimensions();
@@ -359,6 +397,7 @@ public class LocalizationGridBuilder extends TransformBuilder {
      * @since 1.0
      */
     public void setControlPoints(final Vector... coordinates) {
+        ensureModifiable();
         ArgumentChecks.ensureNonNull("coordinates", coordinates);
         linear.setControlPoints(coordinates);
     }
@@ -378,6 +417,7 @@ public class LocalizationGridBuilder extends TransformBuilder {
      * @throws MismatchedDimensionException if the target position does not have the expected number of dimensions.
      */
     public void setControlPoint(final int gridX, final int gridY, final double... target) {
+        ensureModifiable();
         tmp[0] = gridX;
         tmp[1] = gridY;
         linear.setControlPoint(tmp, target);
@@ -385,6 +425,8 @@ public class LocalizationGridBuilder extends TransformBuilder {
 
     /**
      * Returns a single target coordinate for the given source coordinate, or {@code null} if none.
+     * If {@linkplain #addLinearizers linearizers} have been specified and {@link #create create(…)}
+     * has already been invoked, then the control points may be projected using one of the linearizers.
      *
      * @param  gridX  the column index in the grid where to read the target position.
      * @param  gridY  the row index in the grid where to read the target position.
@@ -415,7 +457,7 @@ public class LocalizationGridBuilder extends TransformBuilder {
      *   <li>transformed by the inverse of {@linkplain #getSourceToGrid() source to grid} transform.</li>
      * </ol>
      *
-     * @param  fullArea whether the the envelope shall encompass the full cell surfaces instead than only their centers.
+     * @param  fullArea  whether the the envelope shall encompass the full cell surfaces instead than only their centers.
      * @return the envelope of grid points, from lower corner to upper corner.
      * @throws IllegalStateException if the grid points are not yet known.
      * @throws TransformException if the envelope can not be calculated.
@@ -474,10 +516,12 @@ public class LocalizationGridBuilder extends TransformBuilder {
      * @param  direction  the direction to walk through: 0 for columns or 1 for rows.
      *                    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.
      *
      * @since 1.0
      */
     public void resolveWraparoundAxis(final int dimension, final int direction, final double period) {
+        ensureModifiable();
         ArgumentChecks.ensureBetween("dimension", 0, linear.getTargetDimensions() - 1, dimension);
         ArgumentChecks.ensureBetween("direction", 0, linear.getSourceDimensions() - 1, direction);
         ArgumentChecks.ensureStrictlyPositive("period", period);
@@ -485,10 +529,42 @@ public class LocalizationGridBuilder extends TransformBuilder {
     }
 
     /**
+     * Adds transforms to potentially apply on target coordinates before to compute the transform.
+     * This method can be invoked if the departure from a linear transform is too large, resulting
+     * in {@link InterpolatedTransform} to fail with "no convergence error" messages.
+     * If linearizers have been specified, then the {@link #create(MathTransformFactory)} method
+     * will try to apply each transform on target coordinates and check which one results in the
+     * best correlation coefficients. It may be none.
+     *
+     * <p>The linearizers are specified as {@link MathTransform}s from current target coordinates
+     * to other spaces where <cite>sources to new targets</cite> transforms may be more linear.
+     * The keys in the map are arbitrary identifiers used in {@link #toString()} for debugging purpose.
+     * The {@code dimensions} argument specifies which target dimensions to project and can be null or omitted
+     * if the projections shall be applied on all target coordinates. It is possible to invoke this method many
+     * times with different {@code dimensions} argument values.</p>
+     *
+     * @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 LinearTransformBuilder#addLinearizers(Map, int...)
+     *
+     * @since 1.0
+     */
+    public void addLinearizers(final Map<String,MathTransform> projections, int... dimensions) {
+        ensureModifiable();
+        linear.addLinearizers(projections, dimensions);
+    }
+
+    /**
      * Creates a transform from the source points to the target points.
      * This method assumes that source points are precise and all uncertainty is in the target points.
      * If this transform is close enough to an affine transform, then an instance of {@link LinearTransform} is returned.
      *
+     * <p>If this method is invoked more than once, the previously created transform instance is returned.</p>
+     *
      * @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
      *                  shall return {@link LinearTransform} instances.
@@ -498,72 +574,91 @@ public class LocalizationGridBuilder extends TransformBuilder {
      */
     @Override
     public MathTransform create(final MathTransformFactory factory) throws FactoryException {
-        final LinearTransform gridToCoord = linear.create(factory);
-        /*
-         * Make a first check about whether the result of above LinearTransformBuilder.create() call
-         * can be considered a good fit. If true, then we may return the linear transform directly.
-         */
-        boolean isExact  = true;
-        boolean isLinear = true;
-        for (final double c : linear.correlation()) {
-            isExact &= (c == 1);
-            if (!(c >= 0.9999)) {                           // Empirical threshold (may need to be revisited).
-                isLinear = false;
-                break;
-            }
-        }
-        if (isExact) {
-            return MathTransforms.concatenate(sourceToGrid, gridToCoord);
-        }
-        final int      width    = linear.gridSize(0);
-        final int      height   = linear.gridSize(1);
-        final double[] residual = new double[SOURCE_DIMENSION * linear.gridLength];
-        final double[] grid     = new double[SOURCE_DIMENSION * width];
-        double gridPrecision    = precision;
-        try {
+        if (transform == null) {
+            MathTransform step;
+            final LinearTransform gridToCoord = linear.create(factory);
             /*
-             * If the user specified a precision, we need to convert it from source units to grid units.
-             * We convert each dimension separately, then retain the largest magnitude of vector results.
+             * Make a first check about whether the result of above LinearTransformBuilder.create() call
+             * can be considered a good fit. If true, then we may return the linear transform directly.
              */
-            if (gridPrecision > 0 && !sourceToGrid.isIdentity()) {
-                final double[] vector = new double[sourceToGrid.getSourceDimensions()];
-                final double[] offset = new double[sourceToGrid.getTargetDimensions()];
-                double converted = 0;
-                for (int i=0; i<vector.length; i++) {
-                    vector[i] = precision;
-                    sourceToGrid.deltaTransform(vector, 0, offset, 0, 1);
-                    final double length = MathFunctions.magnitude(offset);
-                    if (length > converted) converted = length;
-                    vector[i] = 0;
+            boolean isExact  = true;
+            boolean isLinear = true;
+            for (final double c : linear.correlation()) {
+                isExact &= (c == 1);
+                if (!(c >= 0.9999)) {                           // Empirical threshold (may need to be revisited).
+                    isLinear = false;
+                    break;
+                }
+            }
+            if (isExact) {
+                step = MathTransforms.concatenate(sourceToGrid, gridToCoord);
+            } else {
+                final int      width    = linear.gridSize(0);
+                final int      height   = linear.gridSize(1);
+                final double[] residual = new double[SOURCE_DIMENSION * linear.gridLength];
+                final double[] grid     = new double[SOURCE_DIMENSION * width];
+                double gridPrecision    = precision;
+                try {
+                    /*
+                     * If the user specified a precision, we need to convert it from source units to grid units.
+                     * We convert each dimension separately, then retain the largest magnitude of vector results.
+                     */
+                    if (gridPrecision > 0 && !sourceToGrid.isIdentity()) {
+                        final double[] vector = new double[sourceToGrid.getSourceDimensions()];
+                        final double[] offset = new double[sourceToGrid.getTargetDimensions()];
+                        double converted = 0;
+                        for (int i=0; i<vector.length; i++) {
+                            vector[i] = precision;
+                            sourceToGrid.deltaTransform(vector, 0, offset, 0, 1);
+                            final double length = MathFunctions.magnitude(offset);
+                            if (length > converted) converted = length;
+                            vector[i] = 0;
+                        }
+                        gridPrecision = converted;
+                    }
+                    /*
+                     * Compute the residuals, i.e. the differences between the coordinates that we get by a linear
+                     * transformation and the coordinates that we want to get. If at least one residual is greater
+                     * than the desired precision,  then the returned MathTransform will need to apply corrections
+                     * after linear transforms. Those corrections will be done by InterpolatedTransform.
+                     */
+                    final MathTransform coordToGrid = gridToCoord.inverse();
+                    for (int k=0,y=0; y<height; y++) {
+                        tmp[0] = 0;
+                        tmp[1] = y;
+                        linear.getControlRow(tmp, grid);                                    // Expected positions.
+                        coordToGrid.transform(grid, 0, residual, k, width);                 // As grid coordinate.
+                        for (int x=0; x<width; x++) {
+                            isLinear &= (residual[k++] -= x) <= gridPrecision;
+                            isLinear &= (residual[k++] -= y) <= gridPrecision;
+                        }
+                    }
+                } catch (TransformException e) {
+                    throw new FactoryException(e);                                          // Should never happen.
+                }
+                if (isLinear) {
+                    step = MathTransforms.concatenate(sourceToGrid, gridToCoord);
+                } else {
+                    step = InterpolatedTransform.createGeodeticTransformation(nonNull(factory),
+                            new ResidualGrid(sourceToGrid, gridToCoord, width, height, residual,
+                            (gridPrecision > 0) ? gridPrecision : DEFAULT_PRECISION));
                 }
-                gridPrecision = converted;
             }
             /*
-             * Compute the residuals, i.e. the differences between the coordinates that we get by a linear
-             * transformation and the coordinates that we want to get. If at least one residual is greater
-             * than the desired precision,  then the returned MathTransform will need to apply corrections
-             * after linear transforms. Those corrections will be done by InterpolatedTransform.
+             * At this point we finished to compute the transformation to target coordinates.
+             * If those target coordinates have been modified in order to make that step more
+             * linear, apply the inverse transformation after the step.
              */
-            final MathTransform coordToGrid = gridToCoord.inverse();
-            for (int k=0,y=0; y<height; y++) {
-                tmp[0] = 0;
-                tmp[1] = y;
-                linear.getControlRow(tmp, grid);                                    // Expected positions.
-                coordToGrid.transform(grid, 0, residual, k, width);                 // As grid coordinate.
-                for (int x=0; x<width; x++) {
-                    isLinear &= (residual[k++] -= x) <= gridPrecision;
-                    isLinear &= (residual[k++] -= y) <= gridPrecision;
-                }
+            final Optional<MathTransform> linearizer = linear.linearizer();
+            if (linearizer.isPresent()) try {
+                step = factory.createConcatenatedTransform(step, linearizer.get().inverse());
+            } catch (NoninvertibleTransformException e) {
+                throw new InvalidGeodeticParameterException(Resources.format(
+                        Resources.Keys.NonInvertibleOperation_1, linear.linearizerID()), e);
             }
-        } catch (TransformException e) {
-            throw new FactoryException(e);                                          // Should never happen.
+            transform = step;
         }
-        if (isLinear) {
-            return MathTransforms.concatenate(sourceToGrid, gridToCoord);
-        }
-        return InterpolatedTransform.createGeodeticTransformation(nonNull(factory),
-                new ResidualGrid(sourceToGrid, gridToCoord, width, height, residual,
-                (gridPrecision > 0) ? gridPrecision : DEFAULT_PRECISION));
+        return transform;
     }
 
     /**
@@ -572,28 +667,106 @@ public class LocalizationGridBuilder extends TransformBuilder {
      * but not necessarily.
      *
      * @param  mt  the transform to test.
-     * @return statistics of difference between computed values and expected value.
+     * @return statistics of difference between computed values and expected values for each target dimension.
      * @throws TransformException if an error occurred while transforming a coordinate.
      *
      * @since 1.0
      */
-    public Statistics error(final MathTransform mt) throws TransformException {
-        final Statistics s = new Statistics(null);
+    public Statistics[] error(final MathTransform mt) throws TransformException {
+        final int           tgtDim = mt.getTargetDimensions();
+        final double[]      point  = new double[Math.max(tgtDim, SOURCE_DIMENSION)];
+        final Statistics[]  stats  = new Statistics[tgtDim];
+        final StringBuilder buffer = new StringBuilder(Vocabulary.format(Vocabulary.Keys.Error)).append(' ');
+        final int           spos   = buffer.length();
+        for (int i=0; i<tgtDim; i++) {
+            buffer.setLength(spos);
+            if (tgtDim < 3) {
+                buffer.append((char) ('x' + i));
+            } else {
+                buffer.append(i + 1);
+            }
+            stats[i] = new Statistics(buffer.toString());
+        }
+        /*
+         * If a linearizer has been applied, all target coordinates in this builder have been projected using
+         * that transform. We will need to apply the inverse transform in order to get back the original values.
+         */
+        final Optional<MathTransform> linearizer = linear.linearizer();
+        final MathTransform complete = linearizer.isPresent() ? linearizer.get().inverse() : null;
         final int width  = linear.gridSize(0);
         final int height = linear.gridSize(1);
-        final int tgtDim = Math.max(mt.getTargetDimensions(), SOURCE_DIMENSION);
-        final double[] t = new double[tgtDim];
         for (int y=0; y<height; y++) {
             for (int x=0; x<width; x++) {
-                t[0] = tmp[0] = x;
-                t[1] = tmp[1] = y;
-                mt.transform(t, 0, t, 0, 1);
+                point[0] = tmp[0] = x;
+                point[1] = tmp[1] = y;
+                mt.transform(point, 0, point, 0, 1);
                 final double[] expected = linear.getControlPoint(tmp);
+                if (complete != null) {
+                    complete.transform(expected, 0, expected, 0, 1);
+                }
                 for (int i=0; i<tgtDim; i++) {
-                    s.accept(t[i] - expected[i]);
+                    stats[i].accept(point[i] - expected[i]);
+                }
+            }
+        }
+        return stats;
+    }
+
+    /**
+     * 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 component of the transform.</li>
+     *   <li>Error statistics.</li>
+     * </ul>
+     *
+     * The string representation may change in any future version.
+     *
+     * @return a string representation of this builder.
+     *
+     * @since 1.0
+     */
+    @Override
+    public String toString() {
+        return toString(null);
+    }
+
+    /**
+     * Returns a string representation of this builder in the given locale.
+     * The string representation is for debugging purpose and may change in any future version.
+     *
+     * @param  locale  the locale for formatting messages and some numbers, or {@code null} for the default.
+     * @return a string representation of this builder.
+     *
+     * @since 1.0
+     */
+    public String toString(final Locale locale) {
+        final StringBuilder buffer = new StringBuilder(400);
+        String lineSeparator = null;
+        try {
+            lineSeparator = linear.appendTo(buffer, getClass(), locale, Vocabulary.Keys.LinearTransformation);
+            if (transform != null) {
+                buffer.append(Strings.CONTINUATION_ITEM);
+                final Vocabulary vocabulary = Vocabulary.getResources(locale);
+                vocabulary.appendLabel(Vocabulary.Keys.Result, buffer);
+                buffer.append(lineSeparator);
+                final StatisticsFormat sf;
+                if (locale != null) {
+                    sf = StatisticsFormat.getInstance(locale);
+                } else {
+                    sf = StatisticsFormat.getInstance();
                 }
+                sf.format(error(transform), buffer);
             }
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        } catch (TransformException e) {
+            // Ignore - we will not report error statistics.
         }
-        return s;
+        Strings.insertLineInLeftMargin(buffer, lineSeparator);
+        return buffer.toString();
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ProjectedTransformTry.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ProjectedTransformTry.java
index 1f740e5..92f01ba 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ProjectedTransformTry.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/ProjectedTransformTry.java
@@ -18,6 +18,7 @@ package org.apache.sis.referencing.operation.builder;
 
 import java.util.Queue;
 import java.util.Arrays;
+import java.util.Locale;
 import java.text.NumberFormat;
 import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.referencing.operation.MathTransform;
@@ -26,6 +27,7 @@ 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.Exceptions;
 import org.apache.sis.util.resources.Vocabulary;
 
 
@@ -93,6 +95,15 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
     private TransformException error;
 
     /**
+     * Creates a new instance initialized to a copy of the given instance but without result.
+     */
+    ProjectedTransformTry(final ProjectedTransformTry other) {
+        name       = other.name;
+        projection = other.projection;
+        dimensions = other.dimensions;
+    }
+
+    /**
      * 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.
      */
@@ -132,6 +143,14 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
     }
 
     /**
+     * Returns the name of this object, or {@code null} if unspecified.
+     * This is used only for formatting error messages.
+     */
+    final String name() {
+        return name;
+    }
+
+    /**
      * 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.
@@ -285,24 +304,26 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
      *   <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.
+     * @param  table   the table where to write a row.
+     * @param  nf      format to use for writing coefficients, or {@code null} if not yet created.
+     * @param  locale  the locale to use if a number format must be created.
      * @return format used for writing coefficients, or {@code null}.
      */
-    final NumberFormat summarize(final TableAppender table, NumberFormat nf) {
+    final NumberFormat summarize(final TableAppender table, NumberFormat nf, final Locale locale) {
         if (name == null) {
-            name = Vocabulary.format(projection == null ? Vocabulary.Keys.Identity : Vocabulary.Keys.Unnamed);
+            final short key = (projection == null) ? Vocabulary.Keys.Identity : Vocabulary.Keys.Unnamed;
+            name = Vocabulary.getResources(locale).getString(key);
         }
         table.append(name).nextColumn();
         String message = "";
         if (error != null) {
-            message = error.getMessage();
+            message = Exceptions.getLocalizedMessage(error, locale);
             if (message == null) {
                 message = error.getClass().getSimpleName();
             }
         } else if (correlation > 0) {
             if (nf == null) {
-                nf = NumberFormat.getInstance();
+                nf = (locale != null) ? NumberFormat.getInstance(locale) : NumberFormat.getInstance();
                 nf.setMinimumFractionDigits(6);         // Math.ulp(1f) ≈ 1.2E-7
                 nf.setMaximumFractionDigits(6);
             }
@@ -318,7 +339,7 @@ final class ProjectedTransformTry implements Comparable<ProjectedTransformTry> {
     @Override
     public String toString() {
         final TableAppender buffer = new TableAppender("  ");
-        summarize(buffer, null);
+        summarize(buffer, null, 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 3a60ed1..86b6ecc 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,7 +28,7 @@ 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;
+ * <p>Builders are not thread-safe. 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)
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 ed87ddb..abee72c 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
@@ -36,10 +36,17 @@ import org.apache.sis.util.CharSequences;
 public final class Strings extends Static {
     /**
      * The character to write at the beginning of lines that are continuation of a single log record.
+     * This constant is defined here only for a little bit more uniform {@code toString()} in SIS.
      */
     public static final char CONTINUATION_MARK = '┃', CONTINUATION_END = '╹';
 
     /**
+     * Characters for a new item in a block illustrated by {@link #CONTINUATION_MARK}.
+     * This constant is defined here only for a little bit more uniform {@code toString()} in SIS.
+     */
+    public static final String CONTINUATION_ITEM = "▶ ";
+
+    /**
      * Do not allow instantiation of this class.
      */
     private Strings() {
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 c3f14cf..dedc838 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
@@ -347,6 +347,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Envelope = 151;
 
         /**
+         * Error
+         */
+        public static final short Error = 167;
+
+        /**
          * Exit
          */
         public static final short Exit = 143;
@@ -467,6 +472,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Libraries = 60;
 
         /**
+         * Linear transformation
+         */
+        public static final short LinearTransformation = 165;
+
+        /**
          * Local configuration
          */
         public static final short LocalConfiguration = 61;
@@ -652,6 +662,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Plugins = 120;
 
         /**
+         * Preprocessing
+         */
+        public static final short Preprocessing = 166;
+
+        /**
          * “{0}”
          */
         public static final short Quoted_1 = 87;
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 e5c62c0..d4e8c81 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
@@ -72,6 +72,7 @@ EllipsoidalHeight       = Ellipsoidal height
 EndDate                 = End date
 EntryCount_1            = {0} entr{0,choice,0#y|2#ies}
 Envelope                = Envelope
+Error                   = Error
 Exit                    = Exit
 File                    = File
 FillValue               = Fill value
@@ -97,6 +98,7 @@ Longitude               = Longitude
 Legend                  = Legend
 Level                   = Level
 Libraries               = Libraries
+LinearTransformation    = Linear transformation
 LocalConfiguration      = Local configuration
 Locale                  = Locale
 Localization            = Localization
@@ -133,6 +135,7 @@ OtherSurface            = Other surface
 Parenthesis_2           = {0} ({1})
 Paths                   = Paths
 Plugins                 = Plug-ins
+Preprocessing           = Preprocessing
 Quoted_1                = \u201c{0}\u201d
 Read                    = Read
 Remarks                 = Remarks
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 30d2755..29b4b42 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
@@ -79,6 +79,7 @@ EllipsoidalHeight       = Hauteur ellipso\u00efdale
 EntryCount_1            = {0} entr\u00e9e{0,choice,0#|2#s}
 EndDate                 = Date de fin
 Envelope                = Enveloppe
+Error                   = Erreur
 Exit                    = Quitter
 File                    = Fichier
 FillValue               = Valeur de remplissage
@@ -104,6 +105,7 @@ Longitude               = Longitude
 Legend                  = L\u00e9gende
 Level                   = Niveau
 Libraries               = Biblioth\u00e8ques
+LinearTransformation    = Transformation lin\u00e9aire
 LocalConfiguration      = Configuration locale
 Locale                  = Locale
 Localization            = R\u00e9gionalisation
@@ -140,6 +142,7 @@ OtherSurface            = Autre surface
 Parenthesis_2           = {0} ({1})
 Paths                   = Chemins
 Plugins                 = Modules d\u2019extension
+Preprocessing           = Pr\u00e9traitement
 Quoted_1                = \u00ab\u202f{0}\u202f\u00bb
 Read                    = Lecture
 Remarks                 = Remarques
diff --git a/ide-project/NetBeans/nbproject/genfiles.properties b/ide-project/NetBeans/nbproject/genfiles.properties
index 35088b0..2859e14 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
 build.xml.stylesheet.CRC32=28e38971@1.53.1.46
-nbproject/build-impl.xml.data.CRC32=6673fb19
+nbproject/build-impl.xml.data.CRC32=f55f037a
 nbproject/build-impl.xml.script.CRC32=a7689f96
 nbproject/build-impl.xml.stylesheet.CRC32=3a2fa800@1.89.1.48
diff --git a/ide-project/NetBeans/nbproject/project.xml b/ide-project/NetBeans/nbproject/project.xml
index e24aedb..c4f6cda 100644
--- a/ide-project/NetBeans/nbproject/project.xml
+++ b/ide-project/NetBeans/nbproject/project.xml
@@ -117,6 +117,7 @@
             <word>parsable</word>
             <word>polyline</word>
             <word>polylines</word>
+            <word>preprocessing</word>
             <word>recursivity</word>
             <word>scanline</word>
             <word>spliterator</word>


Mime
View raw message