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