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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 27b57b6  Add a LinearTransformBuilder.getControlPoints() method in 
complement to setControlPoints(Map). Use that new method for adding a 
LocalizationGridBuilder(LinearTransformBuilder) constructor.
27b57b6 is described below

commit 27b57b629a5b1d49eadc6d1d6622433d89f58255
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Oct 5 20:39:06 2018 -0400

    Add a LinearTransformBuilder.getControlPoints() method in complement to 
setControlPoints(Map).
    Use that new method for adding a 
LocalizationGridBuilder(LinearTransformBuilder) constructor.
---
 .../operation/builder/LinearTransformBuilder.java  | 332 ++++++++++++++++++++-
 .../operation/builder/LocalizationGridBuilder.java |  54 +++-
 .../builder/LinearTransformBuilderTest.java        |  81 ++++-
 .../builder/LocalizationGridBuilderTest.java       |  39 ++-
 .../java/org/apache/sis/util/resources/Errors.java |   5 +
 .../apache/sis/util/resources/Errors.properties    |   1 +
 .../apache/sis/util/resources/Errors_fr.properties |   1 +
 7 files changed, 494 insertions(+), 19 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 88123bb..e3bb860 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
@@ -18,6 +18,7 @@ package org.apache.sis.referencing.operation.builder;
 
 import java.util.Map;
 import java.util.Arrays;
+import java.util.NoSuchElementException;
 import java.io.IOException;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
@@ -31,12 +32,16 @@ import org.apache.sis.io.TableAppender;
 import org.apache.sis.math.Line;
 import org.apache.sis.math.Plane;
 import org.apache.sis.math.Vector;
+import org.apache.sis.geometry.DirectPosition1D;
+import org.apache.sis.geometry.DirectPosition2D;
+import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix;
 import org.apache.sis.internal.referencing.Resources;
+import org.apache.sis.internal.util.AbstractMap;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
@@ -108,7 +113,8 @@ public class LinearTransformBuilder extends 
TransformBuilder {
      * Number of valid positions in the {@link #sources} or {@link #targets} 
arrays.
      * Note that the "valid" positions may contain {@link Double#NaN} ordinate 
values.
      * This field is only indicative if this {@code LinearTransformBuilder} 
instance
-     * has been created by {@link #LinearTransformBuilder(int...)}.
+     * has been created by {@link #LinearTransformBuilder(int...)} because we 
do not
+     * try to detect if user adds a new point or overwrites an existing one.
      */
     private int numPoints;
 
@@ -177,6 +183,8 @@ public class LinearTransformBuilder extends 
TransformBuilder {
     /**
      * Returns the grid size for the given dimension. It is caller's 
responsibility to ensure that
      * this method is invoked only on instances created by {@link 
#LinearTransformBuilder(int...)}.
+     *
+     * @see #getGridDimensions()
      */
     final int gridSize(final int srcDim) {
         return gridSize[srcDim];
@@ -214,11 +222,13 @@ public class LinearTransformBuilder extends 
TransformBuilder {
     /**
      * Returns the offset of the given source grid coordinate, or -1 if none. 
The algorithm implemented in this
      * method is inefficient, but should rarely be used. This is only a 
fallback when {@link #flatIndex(int[])}
-     * can not be used.
+     * can not be used. Callers is responsible to ensure that the number of 
dimensions match.
+     *
+     * @see ControlPoints#search(double[][], double[])
      */
     private int search(final int[] source) {
         assert gridSize == null;         // This method should not be invoked 
for points distributed on a grid.
-search: for (int j=0; j<numPoints; j++) {
+search: for (int j=numPoints; --j >= 0;) {
             for (int i=0; i<source.length; i++) {
                 if (source[i] != sources[i][j]) {
                     continue search;                            // Search 
another position for the same source.
@@ -256,6 +266,8 @@ search: for (int j=0; j<numPoints; j++) {
      * of known size. Callers must have verified the position dimension before 
to invoke this method.
      *
      * @throws IllegalArgumentException if an ordinate value is illegal.
+     *
+     * @see ControlPoints#flatIndex(DirectPosition)
      */
     private int flatIndex(final DirectPosition source) {
         assert sources == null;               // This method should not be 
invoked for randomly distributed points.
@@ -295,7 +307,8 @@ search: for (int j=0; j<numPoints; j++) {
 
     /**
      * Builds the exception message for an unexpected position dimension. This 
method assumes
-     * that positions are stored in this builder as they are read from 
user-provided collection.
+     * that positions are stored in this builder as they are read from 
user-provided collection,
+     * with {@link #numPoints} the index of the next point that we failed to 
add.
      */
     private String mismatchedDimension(final String name, final int expected, 
final int actual) {
         return Errors.format(Errors.Keys.MismatchedDimension_3, name + '[' + 
numPoints + ']', expected, actual);
@@ -309,6 +322,17 @@ search: for (int j=0; j<numPoints; j++) {
     }
 
     /**
+     * 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.
+     *
+     * @see #getSourceDimensions()
+     * @see #gridSize(int)
+     */
+    final int getGridDimensions() {
+        return (gridSize != null) ? gridSize.length : -1;
+    }
+
+    /**
      * Returns the number of dimensions in source positions.
      *
      * @return the dimension of source points.
@@ -388,11 +412,10 @@ search: for (int j=0; j<numPoints; j++) {
         }
         final int dim = points.length;
         final GeneralEnvelope envelope = new GeneralEnvelope(dim);
-        for (int i=0; i <dim; i++) {
-            final double[] data = points[i];
+        for (int i=0; i<dim; i++) {
             double lower = Double.POSITIVE_INFINITY;
             double upper = Double.NEGATIVE_INFINITY;
-            for (final double value : data) {
+            for (final double value : points[i]) {
                 if (value < lower) lower = value;
                 if (value > upper) upper = value;
             }
@@ -416,6 +439,7 @@ search: for (int j=0; j<numPoints; j++) {
      * the given map, and the target positions are the associated values in 
the map. The map should not contain two
      * entries with the same source position. Coordinate reference systems are 
ignored.
      * Null positions are silently ignored.
+     * Positions with NaN or infinite coordinates cause an exception to be 
thrown.
      *
      * <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>).
@@ -431,6 +455,7 @@ search: for (int j=0; j<numPoints; j++) {
      *
      * @param  sourceToTarget  a map of source positions to target positions.
      *         Source positions are assumed precise and target positions are 
assumed uncertain.
+     * @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 ordinates are not indices 
in that grid.
      * @throws MismatchedDimensionException if some positions do not have the 
expected number of dimensions.
@@ -481,19 +506,268 @@ search: for (int j=0; j<numPoints; j++) {
             int d;
             if ((d = src.getDimension()) != srcDim) throw new 
MismatchedDimensionException(mismatchedDimension("source", srcDim, d));
             if ((d = tgt.getDimension()) != tgtDim) throw new 
MismatchedDimensionException(mismatchedDimension("target", tgtDim, d));
+            boolean isValid = true;
             int index;
             if (gridSize != null) {
                 index = flatIndex(src);
             } else {
                 index = numPoints;
                 for (int i=0; i<srcDim; i++) {
-                    sources[i][index] = src.getOrdinate(i);
+                    isValid &= Double.isFinite(sources[i][index] = 
src.getOrdinate(i));
                 }
             }
             for (int i=0; i<tgtDim; i++) {
-                targets[i][index] = tgt.getOrdinate(i);
+                isValid &= Double.isFinite(targets[i][index] = 
tgt.getOrdinate(i));
+            }
+            /*
+             * If the point contains some NaN or infinite coordinate values, 
it is okay to leave it as-is
+             * (without incrementing 'numPoints') provided that we ensure that 
at least one value is NaN.
+             * For convenience, we set only the first coordinate to NaN. The 
ControlPoints map will check
+             * for the first coordinate too, so we need to keep this policy 
consistent.
+             */
+            if (isValid) {
+                numPoints++;
+            } else {
+                targets[0][index] = Double.NaN;
+                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IllegalMapping_2, src, tgt));
             }
-            numPoints++;
+        }
+    }
+
+    /**
+     * Returns all control points as a map. Values are source coordinates and 
keys are target coordinates.
+     * 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.
+     *
+     * @return all control points in this builder.
+     *
+     * @since 1.0
+     */
+    public Map<DirectPosition,DirectPosition> getControlPoints() {
+        return (gridSize != null) ? new ControlPoints() : new Ungridded();
+    }
+
+    /**
+     * Implementation of the map returned by {@link #getControlPoints()}. The 
default implementation
+     * is suitable for {@link LinearTransformBuilder} backed by a grid. For 
non-gridded sources, the
+     * {@link Ungridded} subclass shall be used instead.
+     */
+    private class ControlPoints extends 
AbstractMap<DirectPosition,DirectPosition> {
+        /**
+         * Creates a new map view of control points.
+         */
+        ControlPoints() {
+        }
+
+        /**
+         * Creates a point from the given data at the given offset. Before to 
invoke this method,
+         * caller should verify index validity and that the coordinate does 
not contain NaN values.
+         */
+        final DirectPosition position(final double[][] data, final int offset) 
{
+            switch (data.length) {
+                case 1: return new DirectPosition1D(data[0][offset]);
+                case 2: return new DirectPosition2D(data[0][offset], 
data[1][offset]);
+            }
+            final GeneralDirectPosition pos = new 
GeneralDirectPosition(data.length);
+            for (int i=0; i<data.length; i++) pos.setOrdinate(i, 
data[i][offset]);
+            return pos;
+        }
+
+        /**
+         * Returns the number of points to consider when searching in {@link 
#sources} or {@link #targets} arrays.
+         * For gridded data we can not rely on {@link #numPoints} because the 
coordinate values may be at any index,
+         * not necessarily at consecutive indices.
+         */
+        int domain() {
+            return gridLength;
+        }
+
+        /**
+         * Returns the index of the given coordinates in the given data array 
(source or target coordinates).
+         * This method is a copy of {@link 
LinearTransformBuilder#search(int[])}, but working on real values
+         * instead than integers and capable to work on {@link #targets} as 
well as {@link #sources}.
+         *
+         * <p>If the given coordinates contain NaN values, then this method 
will always return -1 even if the
+         * given data contains the same NaN values. We want this behavior 
because NaN mean that the point has
+         * not been set. There is no confusion with NaN values that users 
could have set explicitly because
+         * {@code setControlPoint} methods do not allow NaN values.</p>
+         *
+         * @see LinearTransformBuilder#search(int[])
+         */
+        final int search(final double[][] data, final double[] coord) {
+            if (data != null && coord.length == data.length) {
+search:         for (int j=domain(); --j >= 0;) {
+                    for (int i=0; i<coord.length; i++) {
+                        if (coord[i] != data[i][j]) {           // 
Intentionally want 'false' for NaN values.
+                            continue search;
+                        }
+                    }
+                    return j;
+                }
+            }
+            return -1;
+        }
+
+        /**
+         * Returns {@code true} if the given value is one of the target 
coordinates.
+         * This method requires a linear scan of the data.
+         */
+        @Override
+        public final boolean containsValue(final Object value) {
+            return (value instanceof Position) && search(targets, ((Position) 
value).getDirectPosition().getCoordinate()) >= 0;
+        }
+
+        /**
+         * Returns {@code true} if the given value is one of the source 
coordinates.
+         * This method is fast on gridded data, but requires linear scan on 
non-gridded data.
+         */
+        @Override
+        public final boolean containsKey(final Object key) {
+            return (key instanceof Position) && flatIndex(((Position) 
key).getDirectPosition()) >= 0;
+        }
+
+        /**
+         * Returns the target point for the given source point.
+         * This method is fast on gridded data, but requires linear scan on 
non-gridded data.
+         */
+        @Override
+        public final DirectPosition get(final Object key) {
+            if (key instanceof Position) {
+                final int index = flatIndex(((Position) 
key).getDirectPosition());
+                if (index >= 0) return position(targets, index);
+            }
+            return null;
+        }
+
+        /**
+         * Returns the index where to fetch a target position for the given 
source position in the flattened array.
+         * This is the same work as {@link 
LinearTransformBuilder#flatIndex(DirectPosition)}, but without throwing
+         * exception if the position is invalid. Instead, -1 is returned as a 
sentinel value for invalid source
+         * (including mismatched number of dimensions).
+         *
+         * <p>The default implementation assumes a grid. This method must be 
overridden by {@link Ungridded}.</p>
+         *
+         * @see LinearTransformBuilder#flatIndex(DirectPosition)
+         */
+        int flatIndex(final DirectPosition source) {
+            final double[][] targets = LinearTransformBuilder.this.targets;
+            if (targets != null) {
+                final int[] gridSize = LinearTransformBuilder.this.gridSize;
+                int i = gridSize.length;
+                if (i == source.getDimension()) {
+                    int offset = 0;
+                    while (i != 0) {
+                        final int size = gridSize[--i];
+                        final double ordinate = source.getOrdinate(i);
+                        final int index = (int) ordinate;
+                        if (index < 0 || index >= size || index != ordinate) {
+                            return -1;
+                        }
+                        offset = offset * size + index;
+                    }
+                    if (!Double.isNaN(targets[0][offset])) return offset;
+                }
+            }
+            return -1;
+        }
+
+        /**
+         * Returns an iterator over the entries.
+         * {@code DirectPosition} instances are created on-the-fly during the 
iteration.
+         *
+         * <p>The default implementation assumes a grid. This method must be 
overridden by {@link Ungridded}.</p>
+         */
+        @Override
+        protected EntryIterator<DirectPosition,DirectPosition> entryIterator() 
{
+            return new EntryIterator<DirectPosition,DirectPosition>() {
+                /**
+                 * Index in the flat arrays of the next entry to return.
+                 */
+                private int index = -1;
+
+                /**
+                 * Moves to the next entry and returns {@code true} if an 
entry has been found.
+                 * This method skips coordinates having NaN value. Those NaN 
values may happen
+                 * on gridded data (they mean that the point has not yet been 
set), but should
+                 * not happen on non-gridded data.
+                 */
+                @Override protected boolean next() {
+                    final double[][] targets = 
LinearTransformBuilder.this.targets;
+                    if (targets != null) {
+                        final double[] x = targets[0];
+                        final int gridLength = 
LinearTransformBuilder.this.gridLength;
+                        while (++index < gridLength) {
+                            if (!Double.isNaN(x[index])) {
+                                return true;
+                            }
+                        }
+                    }
+                    return false;
+                }
+
+                /**
+                 * Reconstructs the source coordinates for the current index.
+                 * This method is the converse of {@code 
ControlPoints.flatIndex(DirectPosition)}.
+                 * It assumes gridded data; {@link Ungridded} will have to do 
a different work.
+                 */
+                @Override protected DirectPosition getKey() {
+                    final int[] gridSize = 
LinearTransformBuilder.this.gridSize;
+                    final int dim = gridSize.length;
+                    final GeneralDirectPosition pos = new 
GeneralDirectPosition(dim);
+                    int offset = index;
+                    for (int i=0; i<dim; i++) {
+                        final int size = gridSize[i];
+                        pos.setOrdinate(i, offset % size);
+                        offset /= size;
+                    }
+                    if (offset == 0) {
+                        return pos;
+                    } else {
+                        throw new NoSuchElementException();
+                    }
+                }
+
+                /**
+                 * Returns the target coordinates at current index.
+                 */
+                @Override protected DirectPosition getValue() {
+                    return position(targets, index);
+                }
+            };
+        }
+    }
+
+    /**
+     * Implementation of the map returned by {@link #getControlPoints()} when 
no grid is used.
+     * This implementation is simpler than the gridded case, but less 
efficient as some methods
+     * require a linear scan.
+     */
+    private final class Ungridded extends ControlPoints {
+        /** Overrides default method with more efficient implementation. */
+        @Override public boolean isEmpty() {return numPoints == 0;}
+        @Override public int     size()    {return numPoints;}
+        @Override        int     domain()  {return numPoints;}
+
+        /**
+         * Returns the index where to fetch a target position for the given 
source position
+         * in the flattened array. In non-gridded case, this operation 
requires linear scan.
+         */
+        @Override int flatIndex(final DirectPosition source) {
+            return search(sources, source.getCoordinate());
+        }
+
+        /**
+         * Returns an iterator over the entries.
+         * {@code DirectPosition} instances are created on-the-fly during the 
iteration.
+         */
+        @Override protected EntryIterator<DirectPosition,DirectPosition> 
entryIterator() {
+            return new EntryIterator<DirectPosition,DirectPosition>() {
+                private int index = -1;
+
+                @Override protected boolean        next()     {return ++index 
< numPoints;}
+                @Override protected DirectPosition getKey()   {return 
position(sources, index);}
+                @Override protected DirectPosition getValue() {return 
position(targets, index);}
+            };
         }
     }
 
@@ -511,7 +785,7 @@ search: for (int j=0; j<numPoints; j++) {
      *                 If this builder has been created with the {@link 
#LinearTransformBuilder()} constructor, then no constraint apply.
      * @param  target  the target coordinates, assumed uncertain.
      * @throws IllegalArgumentException if this builder has been {@linkplain 
#LinearTransformBuilder(int...) created for a grid}
-     *         but some source ordinates are out of index range.
+     *         but some source ordinates 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.
      *
      * @since 0.8
@@ -554,8 +828,15 @@ search: for (int j=0; j<numPoints; j++) {
                 sources[i][index] = source[i];
             }
         }
+        boolean isValid = true;
         for (int i=0; i<tgtDim; i++) {
-            targets[i][index] = target[i];
+            isValid &= Double.isFinite(targets[i][index] = target[i]);
+        }
+        transform   = null;
+        correlation = null;
+        if (!isValid) {
+            if (gridSize == null) numPoints--;
+            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IllegalMapping_2, source, 
target));
         }
     }
 
@@ -593,12 +874,18 @@ search: for (int j=0; j<numPoints; j++) {
                 return null;
             }
         }
-        boolean isNaN = true;
+        /*
+         * A coordinate with NaN value means that the point has not been set.
+         * Not that the coordinate may have only one NaN value, not necessarily
+         * all of them, if the point has been deleted after insertion attempt.
+         */
         final double[] target = new double[targets.length];
         for (int i=0; i<target.length; i++) {
-            isNaN &= Double.isNaN(target[i] = targets[i][index]);
+            if (Double.isNaN(target[i] = targets[i][index])) {
+                return null;
+            }
         }
-        return isNaN ? null : target;
+        return target;
     }
 
     /**
@@ -616,6 +903,21 @@ search: for (int j=0; j<numPoints; j++) {
     }
 
     /**
+     * Returns the vector of source ordinate names.
+     * It is caller responsibility to ensure that this builder is not backed 
by a grid.
+     */
+    final Vector[] sources() {
+        if (sources != null) {
+            final Vector[] v = new Vector[sources.length];
+            for (int i=0; i<v.length; i++) {
+                v[i] = vector(sources[i]);
+            }
+            return v;
+        }
+        throw new IllegalStateException(noData());
+    }
+
+    /**
      * 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.
      *
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 f0abca3..7fc6e80 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
@@ -134,7 +134,9 @@ public class LocalizationGridBuilder extends 
TransformBuilder {
      */
     public LocalizationGridBuilder(final Vector sourceX, final Vector sourceY) 
{
         final Matrix fromGrid = new Matrix3();
-        linear = new LinearTransformBuilder(infer(sourceX, fromGrid, 0), 
infer(sourceY, fromGrid, 1));
+        final int width  = infer(sourceX, fromGrid, 0);
+        final int height = infer(sourceY, fromGrid, 1);
+        linear = new LinearTransformBuilder(width, height);
         try {
             sourceToGrid = MathTransforms.linear(fromGrid).inverse();
         } catch (NoninvertibleTransformException e) {
@@ -144,6 +146,56 @@ public class LocalizationGridBuilder extends 
TransformBuilder {
     }
 
     /**
+     * Creates a new builder for a localization grid inferred from the given 
provider of control points.
+     * The {@linkplain LinearTransformBuilder#getSourceDimensions() number of 
source dimensions} in the
+     * given {@code localizations} argument shall be 2. The {@code 
localization} can be used in two ways:
+     *
+     * <ul class="verbose">
+     *   <li>If the {@code localizations} instance has been
+     *     {@linkplain LinearTransformBuilder#LinearTransformBuilder(int...) 
created with a fixed grid size},
+     *     then that instance is used as-is — it is not copied. It is okay to 
specify an empty instance and
+     *     to provide control points later by calls to {@link 
#setControlPoint(int, int, double...)}.</li>
+     *   <li>If the {@code localizations} instance has been
+     *     {@linkplain LinearTransformBuilder#LinearTransformBuilder() created 
for a grid of unknown size},
+     *     then this constructor tries to infer a grid size by inspection of 
the control points present in
+     *     {@code localizations} at the time this constructor is invoked. 
Changes in {@code localizations}
+     *     after construction will not be reflected in this new builder.</li>
+     * </ul>
+     *
+     * @param  localizations  the provider of control points for which to 
create a localization grid.
+     * @throws ArithmeticException if this constructor can not infer a 
reasonable grid size from the given localizations.
+     *
+     * @since 1.0
+     */
+    public LocalizationGridBuilder(final LinearTransformBuilder localizations) 
{
+        ArgumentChecks.ensureNonNull("localizations", localizations);
+        int n = localizations.getGridDimensions();
+        if (n == 2) {
+            linear = localizations;
+            sourceToGrid = MathTransforms.identity(2);
+        } else {
+            if (n < 0) {
+                final Vector[] sources = localizations.sources();
+                n = sources.length;
+                if (n == 2) {
+                    final Matrix fromGrid = new Matrix3();
+                    final int width  = infer(sources[0], fromGrid, 0);
+                    final int height = infer(sources[1], fromGrid, 1);
+                    linear = new LinearTransformBuilder(width, height);
+                    linear.setControlPoints(localizations.getControlPoints());
+                    try {
+                        sourceToGrid = 
MathTransforms.linear(fromGrid).inverse();
+                    } catch (NoninvertibleTransformException e) {
+                        throw (ArithmeticException) new 
ArithmeticException(e.getLocalizedMessage()).initCause(e);
+                    }
+                    return;
+                }
+            }
+            throw new 
IllegalArgumentException(Resources.format(Resources.Keys.MismatchedTransformDimension_3,
 0, 2, n));
+        }
+    }
+
+    /**
      * Infers a grid size by searching for the greatest common divisor (GCD) 
for values in the given vector.
      * The vector values should be integers, but this method is tolerant to 
constant offsets (typically 0.5).
      * The GCD is taken as a "grid to source" scale factor and the minimal 
value as the translation term.
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 9c79d50..a36608e 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
@@ -29,14 +29,15 @@ import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
+import static org.apache.sis.test.Assert.*;
+import org.opengis.geometry.DirectPosition;
 
 
 /**
  * Tests {@link LinearTransformBuilder}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.5
  * @module
  */
@@ -329,4 +330,80 @@ public final strictfp class LinearTransformBuilderTest 
extends TestCase {
         assertEquals("m₁₂", ref.getTranslateY(), m.getElement(1, 2), 
translationTolerance);
         assertArrayEquals("correlation", new double[] {1, 1}, 
builder.correlation(), scaleTolerance);
     }
+
+    /**
+     * Tests {@link LinearTransformBuilder#getControlPoints()} with gridded 
source points.
+     */
+    @Test
+    public void testGetControlPoints() {
+        testGetControlPoints(new LinearTransformBuilder(3, 4));
+    }
+
+    /**
+     * Tests {@link LinearTransformBuilder#getControlPoints()} with 
non-gridded source points.
+     */
+    @Test
+    public void testGetUngriddedControlPoints() {
+        testGetControlPoints(new LinearTransformBuilder());
+    }
+
+    /**
+     * Tests {@link LinearTransformBuilder#getControlPoints()} with the given 
builder.
+     * If the builder is backed by a grid, then the grid size shall be at 
least 3×4.
+     */
+    private static void testGetControlPoints(final LinearTransformBuilder 
builder) {
+        final DirectPosition2D s12, s23, s00;
+        final DirectPosition2D t12, t23, t00;
+        s12 = new DirectPosition2D(1, 2);   t12 = new DirectPosition2D(3, 2);
+        s23 = new DirectPosition2D(2, 3);   t23 = new DirectPosition2D(4, 1);
+        s00 = new DirectPosition2D(0, 0);   t00 = new DirectPosition2D(7, 3);
+
+        final Map<DirectPosition2D,DirectPosition2D> expected = new 
HashMap<>();
+        final Map<DirectPosition,DirectPosition> actual = 
builder.getControlPoints();
+        assertEquals(0, actual.size());
+        assertTrue(actual.isEmpty());
+        assertFalse(actual.containsKey  (s12));
+        assertFalse(actual.containsKey  (s23));
+        assertFalse(actual.containsKey  (s00));
+        assertFalse(actual.containsValue(t12));
+        assertFalse(actual.containsValue(t23));
+        assertFalse(actual.containsValue(t00));
+        assertMapEquals(expected, actual);
+
+        builder.setControlPoint(new int[] {1, 2}, t12.getCoordinate());
+        assertNull(expected.put(s12, t12));
+        assertEquals(1, actual.size());
+        assertFalse(actual.isEmpty());
+        assertTrue (actual.containsKey  (s12));
+        assertFalse(actual.containsKey  (s23));
+        assertFalse(actual.containsKey  (s00));
+        assertTrue (actual.containsValue(t12));
+        assertFalse(actual.containsValue(t23));
+        assertFalse(actual.containsValue(t00));
+        assertMapEquals(expected, actual);
+
+        builder.setControlPoint(new int[] {2, 3}, t23.getCoordinate());
+        assertNull(expected.put(s23, t23));
+        assertEquals(2, actual.size());
+        assertFalse(actual.isEmpty());
+        assertTrue (actual.containsKey  (s12));
+        assertTrue (actual.containsKey  (s23));
+        assertFalse(actual.containsKey  (s00));
+        assertTrue (actual.containsValue(t12));
+        assertTrue (actual.containsValue(t23));
+        assertFalse(actual.containsValue(t00));
+        assertMapEquals(expected, actual);
+
+        builder.setControlPoint(new int[] {0, 0}, t00.getCoordinate());
+        assertNull(expected.put(s00, t00));
+        assertEquals(3, actual.size());
+        assertFalse(actual.isEmpty());
+        assertTrue (actual.containsKey  (s12));
+        assertTrue (actual.containsKey  (s23));
+        assertTrue (actual.containsKey  (s00));
+        assertTrue (actual.containsValue(t12));
+        assertTrue (actual.containsValue(t23));
+        assertTrue (actual.containsValue(t00));
+        assertMapEquals(expected, actual);
+    }
 }
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
index ecba94d..11a3201 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
@@ -21,21 +21,29 @@ import java.awt.geom.AffineTransform;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.test.referencing.TransformTestCase;
+import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.test.DependsOn;
 import org.junit.Test;
 
+import static org.apache.sis.test.ReferencingAssert.*;
+
 
 /**
  * Tests {@link LocalizationGridBuilder}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
 @DependsOn({LinearTransformBuilderTest.class, ResidualGridTest.class})
 public final strictfp class LocalizationGridBuilderTest extends 
TransformTestCase {
     /**
+     * For floating-point comparisons.
+     */
+    private static final double STRICT = 0;
+
+    /**
      * Creates a builder initialized with control points computed from the 
given affine transform.
      * Some non-linear terms will be added to the coordinates computed by the 
given transform.
      *
@@ -102,4 +110,33 @@ public final strictfp class LocalizationGridBuilderTest 
extends TransformTestCas
         verifyTransform(new double[] {0, 3}, new double[] {  1.3,   -8.5});
         verifyTransform(new double[] {4, 3}, new double[] { 87.7, -123.7});
     }
+
+    /**
+     * Tests {@link 
LocalizationGridBuilder#LocalizationGridBuilder(LinearTransformBuilder)}.
+     *
+     * @throws TransformException if an error occurred while computing the 
envelope.
+     */
+    @Test
+    public void testCreateFromLocalizations() throws TransformException {
+        final LinearTransformBuilder localizations = new 
LinearTransformBuilder();
+        localizations.setControlPoint(new int[] {0, 0}, new double[] {-20.0,   
 8.0});
+        localizations.setControlPoint(new int[] {1, 0}, new double[] {  0.4,  
-21.7});
+        localizations.setControlPoint(new int[] {0, 1}, new double[] {-14.3,   
 3.5});
+        localizations.setControlPoint(new int[] {1, 1}, new double[] {  6.1,  
-26.2});
+        localizations.setControlPoint(new int[] {0, 2}, new double[] {  1.3,   
-8.5});
+        localizations.setControlPoint(new int[] {1, 2}, new double[] { 87.7, 
-123.7});
+        LocalizationGridBuilder builder = new 
LocalizationGridBuilder(localizations);
+        /*
+         * Verifies the grid size by checking the source envelope.
+         * Minimum and maximum values are inclusive.
+         */
+        assertEnvelopeEquals(new Envelope2D(null, 0, 0, 1, 2), 
builder.getSourceEnvelope(false), STRICT);
+        /*
+         * Verify a few random positions.
+         */
+        assertArrayEquals(new double[] {-20.0,    8.0}, 
builder.getControlPoint(0, 0), STRICT);
+        assertArrayEquals(new double[] {  0.4,  -21.7}, 
builder.getControlPoint(1, 0), STRICT);
+        assertArrayEquals(new double[] {  1.3,   -8.5}, 
builder.getControlPoint(0, 2), STRICT);
+        assertArrayEquals(new double[] { 87.7, -123.7}, 
builder.getControlPoint(1, 2), STRICT);
+    }
 }
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
index 365cdee..67668b2 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
@@ -383,6 +383,11 @@ public final class Errors extends IndexedResourceBundle {
         public static final short IllegalLanguageCode_1 = 54;
 
         /**
+         * Illegal mapping: {0} → {1}.
+         */
+        public static final short IllegalMapping_2 = 185;
+
+        /**
          * Member “{0}” can not be associated to type “{1}”.
          */
         public static final short IllegalMemberType_2 = 55;
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
index d238722..926be8e 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
@@ -87,6 +87,7 @@ IllegalCRSType_1                  = Coordinate reference 
system can not be of ty
 IllegalFormatPatternForClass_2    = The \u201c{1}\u201d pattern can not be 
applied to formatting of objects of type \u2018{0}\u2019.
 IllegalIdentifierForCodespace_2   = \u201c{1}\u201d is not a valid identifier 
for the \u201c{0}\u201d code space.
 IllegalLanguageCode_1             = The \u201c{0}\u201d language is not 
recognized.
+IllegalMapping_2                  = Illegal mapping: {0} \u2192 {1}.
 IllegalMemberType_2               = Member \u201c{0}\u201d can not be 
associated to type \u201c{1}\u201d.
 IllegalOptionValue_2              = Option \u2018{0}\u2019 can not take the 
\u201c{1}\u201d value.
 IllegalOrdinateRange_3            = The [{0} \u2026 {1}] range of ordinate 
values is not valid for the \u201c{2}\u201d axis.
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
index 176b593..4beb847 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
@@ -84,6 +84,7 @@ IllegalCRSType_1                  = Le syst\u00e8me de 
r\u00e9f\u00e9rence des c
 IllegalFormatPatternForClass_2    = Le mod\u00e8le \u00ab\u202f{1}\u202f\u00bb 
ne peut pas \u00eatre appliqu\u00e9 au formatage d\u2019objets de type 
\u2018{0}\u2019.
 IllegalIdentifierForCodespace_2   = \u00ab\u202f{1}\u202f\u00bb n\u2019est pas 
un identifiant valide pour l\u2019espace de codes \u00ab\u202f{0}\u202f\u00bb.
 IllegalLanguageCode_1             = Le code de langue 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas reconnu.
+IllegalMapping_2                  = Correspondance ill\u00e9gale: {0} \u2192 
{1}.
 IllegalMemberType_2               = Le membre \u00ab\u202f{0}\u202f\u00bb ne 
peut pas \u00eatre associ\u00e9 au type \u00ab\u202f{1}\u202f\u00bb.
 IllegalOptionValue_2              = L\u2019option \u2018{0}\u2019 
n\u2019accepte pas la valeur \u00ab\u202f{1}\u202f\u00bb.
 IllegalOrdinateRange_3            = La plage de valeurs de coordonn\u00e9es 
[{0} \u2026 {1}] n\u2019est pas valide pour l\u2019axe 
\u00ab\u202f{2}\u202f\u00bb.

Reply via email to