blob: ceb82e3888d52751a668ca01b91ade2bf2f9db9b [file] [log] [blame]
/*
* 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.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;
import org.opengis.referencing.operation.Matrix;
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;
import org.apache.sis.referencing.operation.matrix.Matrix3;
import org.apache.sis.referencing.datum.DatumShiftGrid;
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;
/**
* 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>) 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
* specified for all grid cells), a transformation from grid coordinates to "real world" coordinates can be obtained
* with the {@link #create(MathTransformFactory)} method. If this transform is close enough to an affine transform,
* then an instance of {@link LinearTransform} is returned.
* Otherwise, a transform backed by the localization grid is returned.
*
* <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 are not thread-safe. Builders can be used only once;
* points can not be added or modified after {@link #create(MathTransformFactory)} has been invoked.
*
* <h2>Linearizers</h2>
* 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.1
*
* @see InterpolatedTransform
* @see LinearTransform
* @see DatumShiftGrid
*
* @since 0.8
* @module
*/
public class LocalizationGridBuilder extends TransformBuilder {
/**
* Tolerance threshold for comparing pixel coordinates relative to integer values.
*/
private static final double EPS = Numerics.COMPARISON_THRESHOLD;
/**
* The transform for the linear part.
* Always created with a grid size specified to the constructor.
*/
private final LinearTransformBuilder linear;
/**
* A temporary array for two-dimensional source coordinates.
* Used for reducing object allocations.
*/
private final int[] tmp = new int[SOURCE_DIMENSION];
/**
* Conversions from source real-world coordinates to grid indices before interpolation.
* If there is no such conversion to apply, then this is the identity transform.
*/
private LinearTransform sourceToGrid;
/**
* The desired precision of inverse transformations in unit of source coordinates, or 0 in unspecified.
* If no {@link #sourceToGrid} transform has been specified, then this is in unit of grid cells
* (i.e. a value of 1 is the size of one grid cell).
*
* @see #setDesiredPrecision(double)
*/
private double precision;
/**
* Arbitrary default {@link #precision} value, in unit of grid cells. Used if no explicit inverse transform
* precision has been specified. The {@code sourceToGrid} transform shall not be applied on this value.
* This default precision may change in any future SIS version.
*/
private static final double DEFAULT_PRECISION = 1E-7;
/**
* The transform created by {@link #create(MathTransformFactory)}.
*/
private MathTransform transform;
/**
* If the coordinates in some dimensions are cyclic, their periods. Otherwise {@code null}.
* Values are in units of the target CRS. For longitude wraparounds, the period is typically 360°.
* Array length shall be {@code linear.getTargetDimensions()} and non-cyclic dimensions shall have
* a period of zero (not {@link Double#NaN}, because we will use this array as a displacement vector).
*
* @see #resolveWraparoundAxis(int, int, double)
* @see ResidualGrid#periodVector
*/
private double[] periods;
/**
* 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.
* @param height the number of rows in the grid of target positions.
*/
public LocalizationGridBuilder(final int width, final int height) {
linear = new LinearTransformBuilder(width, height);
sourceToGrid = MathTransforms.identity(SOURCE_DIMENSION);
}
/**
* Creates a new, initially empty, builder for a localization grid of a size inferred from the given points.
* This constructor uses the given vectors for computing a grid size and the following initial conversion:
*
* <blockquote>({@code sourceX}, {@code sourceY}) → ({@code gridX}, {@code gridY})</blockquote>
*
* Above conversion can be obtained by {@link #getSourceToGrid()}.
*
* <p>Values in the given vectors should be integers, but this constructor is tolerant to non-integer values
* if they have a constant offset (typically 0.5) relative to integer values. The two vectors do not need to
* have the same length (i.e. {@code sourceX[i]} are not necessarily related to {@code sourceY[i]}).</p>
*
* @param sourceX all possible <var>x</var> inputs before conversion to grid coordinates.
* @param sourceY all possible <var>y</var> inputs before conversion to grid coordinates.
* @throws ArithmeticException if this constructor can not infer a reasonable grid size from the given vectors.
*/
public LocalizationGridBuilder(final Vector sourceX, final Vector sourceY) {
final Matrix fromGrid = new Matrix3();
final int width = infer(sourceX, fromGrid, 0);
final int height = infer(sourceY, fromGrid, 1);
linear = new LinearTransformBuilder(width, height);
try {
sourceToGrid = MathTransforms.linear(fromGrid).inverse();
} catch (NoninvertibleTransformException e) {
// Should not happen because infer(…) verified that the coefficients are okay.
throw (ArithmeticException) new ArithmeticException(e.getLocalizedMessage()).initCause(e);
}
}
/**
* Creates a new builder for a localization grid inferred from the given provider of control points.
* The {@linkplain LinearTransformBuilder#getSourceDimensions() number of source dimensions} in the
* given {@code localizations} argument shall be 2. The {@code localization} can be used in two ways:
*
* <ul class="verbose">
* <li>If the {@code localizations} instance has been
* {@linkplain LinearTransformBuilder#LinearTransformBuilder(int...) created with a fixed grid size},
* then that instance is used as-is — it is not copied. It is okay to specify an empty instance and
* to provide control points later by calls to {@link #setControlPoint(int, int, double...)}.</li>
* <li>If the {@code localizations} instance has been
* {@linkplain LinearTransformBuilder#LinearTransformBuilder() created for a grid of unknown size},
* then this constructor tries to infer a grid size by inspection of the control points present in
* {@code localizations} at the time this constructor is invoked. Changes in {@code localizations}
* after construction will not be reflected in this new builder.</li>
* </ul>
*
* @param localizations the provider of control points for which to create a localization grid.
* @throws ArithmeticException if this constructor can not infer a reasonable grid size from the given localizations.
*
* @since 1.0
*/
public LocalizationGridBuilder(final LinearTransformBuilder localizations) {
ArgumentChecks.ensureNonNull("localizations", localizations);
int n = localizations.getGridDimensions();
if (n == SOURCE_DIMENSION) {
linear = localizations;
sourceToGrid = MathTransforms.identity(SOURCE_DIMENSION);
} else {
if (n < 0) {
final Vector[] sources = localizations.sources();
n = sources.length;
if (n == SOURCE_DIMENSION) {
final Matrix fromGrid = new Matrix3();
final int width = infer(sources[0], fromGrid, 0);
final int height = infer(sources[1], fromGrid, 1);
linear = new LinearTransformBuilder(width, height);
linear.setControlPoints(localizations.getControlPoints());
try {
sourceToGrid = MathTransforms.linear(fromGrid).inverse();
} catch (NoninvertibleTransformException e) {
throw (ArithmeticException) new ArithmeticException(e.getLocalizedMessage()).initCause(e);
}
linear.setLinearizers(localizations);
return;
}
}
throw new IllegalArgumentException(Resources.format(
Resources.Keys.MismatchedTransformDimension_3, 0, SOURCE_DIMENSION, n));
}
}
/**
* Infers a grid size by searching for the greatest common divisor (GCD) for values in the given vector.
* The vector values should be integers, but this method is tolerant to constant offsets (typically 0.5).
* The GCD is taken as a "grid to source" scale factor and the minimal value as the translation term.
* Those two values are stored in the {@code dim} row of the given matrix.
*
* @param source the vector of values for which to get the GCD and minimum value.
* @param fromGrid matrix where to store the minimum value and the GCD.
* @param dim index of the matrix row to update.
* @return grid size.
*/
private static int infer(final Vector source, final Matrix fromGrid, final int dim) {
final NumberRange<?> range = source.range();
final double min = range.getMinDouble(true);
final double span = range.getMaxDouble(true) - min;
final Number increment = source.increment(EPS * span);
double inc;
if (increment != null) {
inc = increment.doubleValue();
} else {
inc = span;
final int size = source.size();
for (int i=0; i<size; i++) {
double v = source.doubleValue(i) - min;
if (Math.abs(v % inc) > EPS) {
do {
final double r = (inc % v); // Both 'inc' and 'v' are positive, so 'r' will be positive too.
inc = v;
v = r;
} while (Math.abs(v) > EPS);
}
}
}
/*
* Compute the size from the increment that we found. If the size is larger than the vector length,
* consider as too large. The rational is that attempt to create a localization grid with that size
* would fail anyway, because it would contain holes where no value is defined. A limit is important
* for preventing useless allocation of large arrays - https://issues.apache.org/jira/browse/SIS-407
*/
fromGrid.setElement(dim, dim, inc);
fromGrid.setElement(dim, SOURCE_DIMENSION, min);
final double n = span / inc;
if (n >= 0.5 && n < source.size() - 0.5) { // Compare as 'double' in case the value is large.
return ((int) Math.round(n)) + 1;
}
throw new ArithmeticException(Resources.format(Resources.Keys.CanNotInferGridSizeFromValues_1, range));
}
/**
* 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
* (i.e. a value of 1 is the size of one grid cell).
*
* <div class="note"><b>Note:</b>
* there is no method for setting the desired target precision because forward transformations <em>precision</em>
* (not to be confused with <em>accuracy</em>) are limited only by rounding errors. Of course the accuracy of both
* 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.
* @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;
}
/**
* Returns the desired precision of <em>inverse</em> transformations, in units of source coordinates.
* This is the precision sets by the last call to {@link #setDesiredPrecision(double)}.
*
* @return desired precision of the results of inverse transformations.
*/
public double getDesiredPrecision() {
return precision;
}
/**
* Defines relationship between "real-world" source coordinates and grid coordinates.
* The given transform is usually two-dimensional, in which case conversions from (<var>x</var>,<var>y</var>)
* source coordinates to ({@code gridX}, {@code gridY}) indices can be done with the following formulas:
* <ul>
* <li><var>gridX</var> = (<var>x</var> - <var>x₀</var>) / <var>Δx</var></li>
* <li><var>gridY</var> = (<var>y</var> - <var>y₀</var>) / <var>Δy</var></li>
* </ul>
*
* where:
* <ul>
* <li>(<var>x₀</var>, <var>y₀</var>) is the coordinate of the center of the cell at grid index (0,0).</li>
* <li><var>Δx</var> and <var>Δy</var> are the distances between two cells on the <var>x</var> and <var>y</var>
* axes respectively, in the same unit of measurement than the one documented in the
* {@link #setDesiredPrecision(double)} method.</li>
* </ul>
*
* The {@code coordinateToGrid} transform for the above formulas can be represented by the following matrix:
*
* {@preformat math
* ┌ ┐
* │ 1/Δx 0 -x₀/Δx │
* │ 0 1/Δy -y₀/Δy │
* │ 0 0 1 │
* └ ┘
* }
*
* If this method is never invoked, then the default conversion is identity.
* 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.
* @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();
if (dim >= SOURCE_DIMENSION) {
isTarget = 1;
dim = sourceToGrid.getTargetDimensions();
if (dim == SOURCE_DIMENSION) {
this.sourceToGrid = sourceToGrid;
return;
}
}
throw new MismatchedDimensionException(Resources.format(
Resources.Keys.MismatchedTransformDimension_3, isTarget, SOURCE_DIMENSION, dim));
}
/**
* Returns the current relationship between "real-world" source coordinates and grid coordinates.
* This is the value set by the last call to {@link #setSourceToGrid(LinearTransform)}.
* If that setter method has never been invoked, then this is an automatically computed transform
* if the grid coordinates {@linkplain #LocalizationGridBuilder(Vector, Vector) have been specified
* to the constructor}, or the identity transform {@linkplain #LocalizationGridBuilder(int, int) otherwise}.
*
* @return the current relationship between "real-world" source coordinates and grid coordinates.
*/
public LinearTransform getSourceToGrid() {
return sourceToGrid;
}
/**
* Returns the envelope of source coordinates. The {@code fullArea} argument control whether
* the returned envelope shall encompass full surface of every cells or only their centers:
* <ul>
* <li>If {@code true}, then the returned envelope encompasses full cell surfaces,
* from lower border to upper border. In other words, the returned envelope encompasses all
* {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CORNER cell corners}.</li>
* <li>If {@code false}, then the returned envelope encompasses only
* {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CENTER cell centers}, inclusive.</li>
* </ul>
*
* This is the envelope of the grid domain (i.e. the ranges of valid {@code gridX} and {@code gridY} argument
* values in calls to {@code get/setControlPoint(…)} methods) transformed as below:
* <ol>
* <li>expanded by ½ cell on each side if {@code fullArea} is {@code true}</li>
* <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.
* @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.
*
* @see LinearTransformBuilder#getSourceEnvelope()
*
* @since 1.0
*/
public Envelope getSourceEnvelope(final boolean fullArea) throws TransformException {
Envelope envelope = linear.getSourceEnvelope();
if (fullArea) {
for (int i = envelope.getDimension(); --i >= 0;) {
final GeneralEnvelope ge = GeneralEnvelope.castOrCopy(envelope);
ge.setRange(i, ge.getLower(i) - 0.5,
ge.getUpper(i) + 0.5);
envelope = ge;
}
}
return Envelopes.transform(sourceToGrid.inverse(), envelope);
}
/**
* Sets all control points. The length of given vectors must be equal to the total number of cells in the grid.
* The first vector provides the <var>x</var> coordinates; the second vector provides the <var>y</var> coordinates,
* <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
*/
public void setControlPoints(final Vector... coordinates) {
ensureModifiable();
ArgumentChecks.ensureNonNull("coordinates", coordinates);
linear.setControlPoints(coordinates);
}
/**
* Sets a single matching control point pair. Source position is assumed precise and target position is assumed uncertain.
* If the given source position was already associated with another target position, then the old target position is discarded.
*
* <p>If a {@linkplain #getSourceToGrid() source to grid} conversion exists, it shall have been applied
* by the caller for computing the ({@code gridX}, {@code gridY}) coordinates given to this method.</p>
*
* @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.
*/
public void setControlPoint(final int gridX, final int gridY, final double... target) {
ensureModifiable();
tmp[0] = gridX;
tmp[1] = gridY;
linear.setControlPoint(tmp, target);
}
/**
* 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.
* @return the target coordinates associated to the given source, or {@code null} if none.
* @throws IllegalArgumentException if the {@code x} or {@code y} coordinate value is out of grid range.
*/
public double[] getControlPoint(final int gridX, final int gridY) {
tmp[0] = gridX;
tmp[1] = gridY;
return linear.getControlPoint(tmp);
}
/**
* Returns a row of coordinate values in the given dimension.
* The returned vector is a view; changes in the returned vector will be reflected in this builder.
*
* @param dimension the target dimension for which to get coordinate values.
* @param row index of the row to get.
* @return coordinate values of the specified row in the specified dimension.
*
* @since 1.0
*/
public Vector getRow(final int dimension, final int row) {
tmp[0] = 0;
tmp[1] = row;
return linear.getTransect(dimension, tmp, 0);
}
/**
* Returns a column of coordinate values in the given dimension.
* The returned vector is a view; changes in the returned vector will be reflected in this builder.
*
* @param dimension the target dimension for which to get coordinate values.
* @param column index of the column to get.
* @return coordinate values of the specified column in the specified dimension.
*
* @since 1.0
*/
public Vector getColumn(final int dimension, final int column) {
tmp[0] = column;
tmp[1] = 0;
return linear.getTransect(dimension, tmp, 1);
}
/**
* Tries to remove discontinuities in coordinates values caused by anti-meridian crossing.
* This method can be invoked when the localization grid may cross the anti-meridian,
* where longitude values may suddenly jump from +180° to -180° or conversely.
* This method walks through the coordinate values of the given dimension (typically the longitudes dimension)
* in the given direction (grid rows or grid columns).
* If a difference greater than {@code period/2} (typically 180°) is found between two consecutive values,
* then a multiple of {@code period} (typically 360°) is added or subtracted in order to make a value as close
* as possible from its previous value.
*
* <p>This method needs a direction to be specified:</p>
* <ul>
* <li>Direction 0 means that each value is compared with the value in the previous column,
* except the value in the first column which is compared to the value in previous row.</li>
* <li>Direction 1 means that each value is compared with the value in the previous row,
* except the value in the first row which is compared to the value in previous column.</li>
* </ul>
* The recommended value is the direction of most stable values. Typically, longitude values increase with column indices
* and are almost constant when increasing row indices. In such case, the recommended direction is 1 for comparing each
* value with the value in previous row, since that value should be closer than the value in previous column.
*
* <div class="note"><b>Example:</b>
* for a grid of (<var>longitude</var>, <var>latitude</var>) values in decimal degrees where longitude values
* vary (increase or decrease) with increasing column indices and latitude values vary (increase or decrease)
* with increasing row indices, the the following method should be invoked for protecting the grid against
* discontinuities on anti-meridian:
*
* {@preformat java
* grid.resolveWraparoundAxis(0, 1, 360);
* }
* </div>
*
* @param dimension the dimension to process.
* This is 0 for longitude dimension in a (<var>longitudes</var>, <var>latitudes</var>) grid.
* @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). Must be strictly positive.
* @return the range of coordinate values in the specified dimension after correction for wraparound values.
* @throws IllegalStateException if this method has already been invoked for the same dimension,
* or if {@link #create(MathTransformFactory) create(…)} has already been invoked.
*
* @since 1.0
*/
public NumberRange<Double> 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);
if (periods == null) {
periods = new double[linear.getTargetDimensions()];
}
if (periods[dimension] != 0) {
throw new IllegalStateException(Errors.format(
Errors.Keys.ValueAlreadyDefined_1, Strings.bracket("periods", dimension)));
}
periods[dimension] = period;
return linear.resolveWraparoundAxis(dimension, direction, period);
}
/**
* 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.
* @return the transform from source to target points.
* @throws FactoryException if the transform can not be created,
* for example because the target points have not be specified.
*/
@Override
public MathTransform create(final MathTransformFactory factory) throws FactoryException {
if (transform == null) {
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;
}
}
MathTransform step;
if (isExact) {
step = MathTransforms.concatenate(sourceToGrid, gridToCoord);
} else {
final int width = linear.gridSize(0);
final int height = linear.gridSize(1);
final float[] residual = new float [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, grid, 0, width); // As grid coordinate.
for (int i=0,x=0; x<width; x++) {
final double dx = grid[i++] - x;
final double dy = grid[i++] - y;
isLinear &= (dx <= gridPrecision);
isLinear &= (dy <= gridPrecision);
residual[k++] = (float) dx;
residual[k++] = (float) dy;
}
}
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, periods));
}
} catch (TransformException e) {
throw new FactoryException(e); // Should never happen.
}
}
/*
* 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 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);
}
transform = step; // Set only after everything succeeded.
}
return transform;
}
/**
* Returns statistics of differences between values calculated by the given transform and actual values.
* The given math transform is typically the transform computed by {@link #create(MathTransformFactory)},
* but not necessarily. The returned statistics are:
*
* <ol class="verbose">
* <li>One {@code Statistics} instance for each target dimension, containing statistics about the differences between
* coordinates computed by the given transform and expected coordinates. For each (<var>i</var>,<var>j</var>) indices
* in this grid, the indices are transformed by a call to {@code mt.transform(…)} and the result is compared with the
* coordinates given by <code>{@linkplain #getControlPoint(int, int) getControlPoint}(i,j)</code>.
* Those statistics are identified by labels like “P → x” and “P → y” where <var>P</var> stands for pixel coordinates.</li>
* <li>One {@code Statistics} instance for each source dimension, containing statistics about the differences between
* coordinates computed by the <em>inverse</em> of the transform and expected coordinates.
* For each (<var>x</var>,<var>y</var>) control point in this grid, the points are transformed by a call
* to {@code mt.inverse().transform(…)} and the result is compared with the pixel indices of that point.
* Those statistics are identified by labels like “i ← P′” and “j ← P′” where <var>P′</var> stands for
* the control point.</li>
* </ol>
*
* @param mt the transform to test.
* @return statistics of difference between computed values and expected values for each target dimension.
* @throws NoninvertibleTransformException if an error occurred while inverting a transform.
*
* @since 1.0
*/
public Statistics[] error(final MathTransform mt) throws NoninvertibleTransformException {
final int tgtDim = mt.getTargetDimensions();
final double[] point = new double[Math.max(tgtDim, SOURCE_DIMENSION)];
final Statistics[] stats = new Statistics[tgtDim + SOURCE_DIMENSION];
final StringBuilder buffer = new StringBuilder();
for (int i=0; i<stats.length; i++) {
buffer.setLength(0);
if (i < tgtDim) {
buffer.append("P → ");
if (i < 3) {
buffer.append((char) ('x' + i));
} else {
buffer.append('z').append(i - 1); // After (x,y,z) continue with z2, z3, z4, etc.
}
} else {
buffer.append((char) ('i' + (i - tgtDim))).append(" ← P′");
}
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 MathTransform inverse = mt.inverse();
final int width = linear.gridSize(0);
final int height = linear.gridSize(1);
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
point[0] = tmp[0] = x;
point[1] = tmp[1] = y;
final double[] expected;
try {
mt.transform(point, 0, point, 0, 1);
expected = linear.getControlPoint(tmp);
if (complete != null) {
complete.transform(expected, 0, expected, 0, 1);
}
} catch (TransformException e) {
continue; // Ignore the points that we fail to transform.
}
for (int i=0; i<tgtDim; i++) {
stats[i].accept(point[i] - expected[i]);
}
/*
* Transform the geographic point back to grid indices and check error.
*/
try {
inverse.transform(expected, 0, expected, 0, 1);
} catch (TransformException e) {
continue; // Ignore the points that we fail to transform.
}
for (int i=0; i<SOURCE_DIMENSION; i++) {
stats[tgtDim + i].accept(expected[i] - tmp[i]);
}
}
}
return stats;
}
/**
* Returns a string representation of this builder in the given locale.
* 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, as documented in the {@link #error(MathTransform)} method.</li>
* </ul>
*
* The string representation 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.Errors, 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 (NoninvertibleTransformException e) {
// Ignore - we will not report error statistics.
}
Strings.insertLineInLeftMargin(buffer, lineSeparator);
return buffer.toString();
}
/**
* Returns a string representation of this builder for debugging purpose.
* The string representation is for debugging purpose and may change in any future version.
* The default implementation delegates to {@link #toString(Locale)} with a null locale.
*
* @return a string representation of this builder.
*
* @since 1.0
*/
@Override
public String toString() {
return toString(null);
}
}