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