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