/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.referencing.operation.transform;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.io.Serializable;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.operation.TransformException;
import org.opengis.geometry.DirectPosition;
import org.geotools.geometry.GeneralDirectPosition;
import org.geotools.referencing.operation.matrix.XMatrix;
import org.geotools.referencing.operation.matrix.Matrix3;
import org.geotools.referencing.operation.matrix.GeneralMatrix;
import org.geotools.referencing.operation.LinearTransform;
import org.geotools.referencing.wkt.Formatter;
import org.geotools.resources.Classes;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.util.Utilities;
/**
* Base class for concatenated transform. Concatenated transforms are
* serializable if all their step transforms are serializables.
*
* @since 2.0
*
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class ConcatenatedTransform extends AbstractMathTransform implements Serializable {
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = 5772066656987558634L;
/**
* Small number for floating point comparaisons.
*/
private static final double EPSILON = 1E-10;
/**
* Maximum length of temporary {@double[]} arrays to be created, used for performing
* transformations in batch. A value of 256 will consumes 2 kilobytes of memory. It is
* better to avoid too high values since allocating and initializing the array elements
* to zero have a cost.
*/
private static final int TEMPORARY_ARRAY_LENGTH = 256;
/**
* The first math transform.
*/
public final MathTransform transform1;
/**
* The second math transform.
*/
public final MathTransform transform2;
/**
* The inverse transform. This field will be computed only when needed.
* But it is serialized in order to avoid rounding error if the inverse
* transform is serialized instead of the original one.
*/
private ConcatenatedTransform inverse;
/**
* Constructs a concatenated transform. This constructor is for subclasses only. To
* create a concatenated transform, use the factory method {@link #create} instead.
*
* @param transform1 The first math transform.
* @param transform2 The second math transform.
*/
protected ConcatenatedTransform(final MathTransform transform1,
final MathTransform transform2)
{
this.transform1 = transform1;
this.transform2 = transform2;
if (!isValid()) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.CANT_CONCATENATE_TRANSFORMS_$2,
getName(transform1), getName(transform2)));
}
}
/**
* Returns the underlying matrix for the specified transform,
* or {@code null} if the matrix is unavailable.
*/
private static XMatrix getMatrix(final MathTransform transform) {
if (transform instanceof LinearTransform) {
return toXMatrix(((LinearTransform) transform).getMatrix());
}
if (transform instanceof AffineTransform) {
return new Matrix3((AffineTransform) transform);
}
return null;
}
/**
* Tests if one math transform is the inverse of the other. This implementation
* can't detect every case. It just detect the case when {@code tr2} is an
* instance of {@link AbstractMathTransform.Inverse}.
*
* @todo We could make this test more general (just compare with tr2.inverse(),
* no matter if it is an instance of AbstractMathTransform.Inverse or not,
* and catch the exception if one is thrown). Would it be too expensive to
* create inconditionnaly the inverse transform?
*/
private static boolean areInverse(final MathTransform tr1, final MathTransform tr2) {
if (tr2 instanceof AbstractMathTransform.Inverse) {
return tr1.equals(((AbstractMathTransform.Inverse) tr2).inverse());
}
return false;
}
/**
* Constructs a concatenated transform. This factory method checks for step transforms
* dimension. The returned transform will implements {@link MathTransform2D} if source and
* target dimensions are equal to 2. Likewise, it will implements {@link MathTransform1D}
* if source and target dimensions are equal to 1. {@link MathTransform} implementations
* are available in two version: direct and non-direct. The "non-direct" version use an
* intermediate buffer when performing transformations; they are slower and consume more
* memory. They are used only as a fallback when a "direct" version can't be created.
*
* @param tr1 The first math transform.
* @param tr2 The second math transform.
* @return The concatenated transform.
*
* @todo We could add one more optimisation: if one transform is a matrix and the
* other transform is a PassThroughTransform, and if the matrix as 0 elements
* for all rows matching the PassThrough sub-transform, then we can get ride
* of the whole PassThroughTransform object.
*/
public static MathTransform create(MathTransform tr1, MathTransform tr2) {
final int dim1 = tr1.getTargetDimensions();
final int dim2 = tr2.getSourceDimensions();
if (dim1 != dim2) {
throw new IllegalArgumentException(
Errors.format(ErrorKeys.CANT_CONCATENATE_TRANSFORMS_$2,
getName(tr1), getName(tr2)) + ' ' +
Errors.format(ErrorKeys.MISMATCHED_DIMENSION_$2, dim1, dim2));
}
MathTransform mt = createOptimized(tr1, tr2);
if (mt != null) {
return mt;
}
/*
* If at least one math transform is an instance of ConcatenatedTransform and assuming
* that MathTransforms are associatives, tries the following arrangements and select
* one one with the fewest amount of steps:
*
* Assuming : tr1 = (A * B)
* tr2 = (C * D)
*
* Current : (A * B) * (C * D) Will be the selected one if nothing better.
* Try k=0 : A * (B * (C * D)) Implies A * ((B * C) * D) through recursivity.
* Try k=1 : ((A * B) * C) * D Implies (A * (B * C)) * D through recursivity.
* Try k=2 : Tried only if try k=1 changed something.
*
* TODO: The same combinaison may be computed more than once (e.g. (B * C) above).
* Should not be a big deal if there is not two many steps. In the even where
* it would appears a performance issue, we could maintains a Map of combinaisons
* already computed. The map would be local to a "create" method execution.
*/
int stepCount = getStepCount(tr1) + getStepCount(tr2);
boolean tryAgain = true; // Really 'true' because we want at least 2 iterations.
for (int k=0; ; k++) {
MathTransform c1 = tr1;
MathTransform c2 = tr2;
final boolean first = (k & 1) == 0;
MathTransform candidate = first ? c1 : c2;
while (candidate instanceof ConcatenatedTransform) {
final ConcatenatedTransform ctr = (ConcatenatedTransform) candidate;
if (first) {
c1 = candidate = ctr.transform1;
c2 = create(ctr.transform2, c2);
} else {
c1 = create(c1, ctr.transform1);
c2 = candidate = ctr.transform2;
}
final int c = getStepCount(c1) + getStepCount(c2);
if (c < stepCount) {
tr1 = c1;
tr2 = c2;
stepCount = c;
tryAgain = true;
}
}
if (!tryAgain) break;
tryAgain = false;
}
/*
* Tries again the check for optimized cases (identity, etc.), because a
* transform may have been simplified to identity as a result of the above.
*/
mt = createOptimized(tr1, tr2);
if (mt != null) {
return mt;
}
/*
* Can't avoid the creation of a ConcatenatedTransform object.
* Check for the type to create (1D, 2D, general case...)
*/
return createConcatenatedTransform(tr1, tr2);
}
/**
* Tries to returns an optimized concatenation, for example by merging to affine transforms
* into a single one. If no optimized cases has been found, returns {@code null}. In the later
* case, the caller will need to create a more heavy {@link ConcatenatedTransform} instance.
*/
private static MathTransform createOptimized(final MathTransform tr1, final MathTransform tr2) {
/*
* Trivial - but actually essential!! - check for the identity cases.
*/
if (tr1.isIdentity()) return tr2;
if (tr2.isIdentity()) return tr1;
/*
* If both transforms use matrix, then we can create
* a single transform using the concatenated matrix.
*/
final XMatrix matrix1 = getMatrix(tr1);
if (matrix1 != null) {
final XMatrix matrix2 = getMatrix(tr2);
if (matrix2 != null) {
// Compute "matrix = matrix2 * matrix1". Reuse an existing matrix object
// if possible, which is always the case when both matrix are square.
final int numRow = matrix2.getNumRow();
final int numCol = matrix1.getNumCol();
final XMatrix matrix;
if (numCol == matrix2.getNumCol()) {
matrix = matrix2;
matrix2.multiply(matrix1);
} else {
final GeneralMatrix m = new GeneralMatrix(numRow, numCol);
m.mul(toGMatrix(matrix2), toGMatrix(matrix1));
matrix = m;
}
if (matrix.isIdentity(EPSILON)) {
matrix.setIdentity();
}
// May not be really affine, but work anyway...
// This call will detect and optimize the special
// case where an 'AffineTransform' can be used.
return ProjectiveTransform.create(matrix);
}
}
/*
* If one transform is the inverse of the
* other, returns the identity transform.
*/
if (areInverse(tr1, tr2) || areInverse(tr2, tr1)) {
assert tr1.getSourceDimensions() == tr2.getTargetDimensions();
assert tr1.getTargetDimensions() == tr2.getSourceDimensions();
return IdentityTransform.create(tr1.getSourceDimensions());
}
/*
* Gives a chance to AbstractMathTransform to returns an optimized object.
* The main use case is Logarithmic vs Exponential transforms.
*/
if (tr1 instanceof AbstractMathTransform) {
final MathTransform optimized = ((AbstractMathTransform) tr1).concatenate(tr2, false);
if (optimized != null) {
return optimized;
}
}
if (tr2 instanceof AbstractMathTransform) {
final MathTransform optimized = ((AbstractMathTransform) tr2).concatenate(tr1, true);
if (optimized != null) {
return optimized;
}
}
// No optimized case found.
return null;
}
/**
* Continue the construction started by {@link #create}. The construction step is available
* separatly for testing purpose (in a JUnit test), and for {@link #inverse()} implementation.
*/
static ConcatenatedTransform createConcatenatedTransform(final MathTransform tr1,
final MathTransform tr2)
{
final int dimSource = tr1.getSourceDimensions();
final int dimTarget = tr2.getTargetDimensions();
/*
* Checks if the result need to be a MathTransform1D.
*/
if (dimSource == 1 && dimTarget == 1) {
if (tr1 instanceof MathTransform1D && tr2 instanceof MathTransform1D) {
return new ConcatenatedTransformDirect1D((MathTransform1D) tr1,
(MathTransform1D) tr2);
} else {
return new ConcatenatedTransform1D(tr1, tr2);
}
} else
/*
* Checks if the result need to be a MathTransform2D.
*/
if (dimSource == 2 && dimTarget == 2) {
if (tr1 instanceof MathTransform2D && tr2 instanceof MathTransform2D) {
return new ConcatenatedTransformDirect2D((MathTransform2D) tr1,
(MathTransform2D) tr2);
} else {
return new ConcatenatedTransform2D(tr1, tr2);
}
} else
/*
* Checks for the general case.
*/
if (dimSource == tr1.getTargetDimensions() && tr2.getSourceDimensions() == dimTarget) {
return new ConcatenatedTransformDirect(tr1, tr2);
} else {
return new ConcatenatedTransform(tr1, tr2);
}
}
/**
* Returns a name for the specified math transform.
*/
private static final String getName(final MathTransform transform) {
if (transform instanceof AbstractMathTransform) {
ParameterValueGroup params = ((AbstractMathTransform) transform).getParameterValues();
if (params != null) {
String name = params.getDescriptor().getName().getCode();
if (name!=null && (name=name.trim()).length()!=0) {
return name;
}
}
}
return Classes.getShortClassName(transform);
}
/**
* Checks if transforms are compatibles. The default
* implementation check if transfert dimension match.
*/
boolean isValid() {
return transform1.getTargetDimensions() == transform2.getSourceDimensions();
}
/**
* Gets the dimension of input points.
*/
public final int getSourceDimensions() {
return transform1.getSourceDimensions();
}
/**
* Gets the dimension of output points.
*/
public final int getTargetDimensions() {
return transform2.getTargetDimensions();
}
/**
* Returns the number of {@linkplain MathTransform math transform} steps performed by this
* concatenated transform.
*
* @return The number of transform steps.
*
* @since 2.5
*/
public final int getStepCount() {
return getStepCount(transform1) + getStepCount(transform2);
}
/**
* Returns the number of {@linkplain MathTransform math transform} steps performed by the
* given transform. As a special case, we returns 0 for the identity transform since it
* should be omitted from the final chain.
*/
private static int getStepCount(final MathTransform transform) {
if (transform.isIdentity()) {
return 0;
}
if (!(transform instanceof ConcatenatedTransform)) {
return 1;
}
return ((ConcatenatedTransform) transform).getStepCount();
}
/**
* Transforms the specified {@code ptSrc} and stores the result in {@code ptDst}.
*/
@Override
public DirectPosition transform(final DirectPosition ptSrc, final DirectPosition ptDst)
throws TransformException
{
assert isValid();
// Note: If we know that the transfert dimension is the same than source
// and target dimension, then we don't need to use an intermediate
// point. This optimization is done in ConcatenatedTransformDirect.
return transform2.transform(transform1.transform(ptSrc, null), ptDst);
}
/**
* Transforms a list of coordinate point ordinal values. The source points are first
* transformed by {@link #transform1}, then the intermediate points are transformed
* by {@link #transform2}. The transformations are performed without intermediate
* buffer if it can be avoided.
*/
public void transform(final double[] srcPts, int srcOff,
final double[] dstPts, int dstOff, int numPts)
throws TransformException
{
assert isValid();
final int intermDim = transform1.getTargetDimensions();
final int targetDim = getTargetDimensions();
/*
* If the transfert dimension is not greater than the target dimension, then we
* don't need to use an intermediate buffer. Note that this optimization is done
* inconditionnaly in ConcatenatedTransformDirect.
*/
if (intermDim <= targetDim) {
transform1.transform(srcPts, srcOff, dstPts, dstOff, numPts);
transform2.transform(dstPts, dstOff, dstPts, dstOff, numPts);
return;
}
if (numPts <= 0) {
return;
}
/*
* Creates a temporary array for the intermediate result. The array may be smaller than
* the length necessary for containing every coordinates. In such case the concatenated
* transform will need to be applied piecewise.
*/
int numTmp = numPts;
int length = numTmp * intermDim;
if (length > TEMPORARY_ARRAY_LENGTH) {
numTmp = Math.max(1, TEMPORARY_ARRAY_LENGTH / intermDim);
length = numTmp * intermDim;
}
final double[] tmp = new double[length];
final int sourceDim = getSourceDimensions();
do {
if (numTmp > numPts) {
numTmp = numPts;
}
transform1.transform(srcPts, srcOff, tmp, 0, numTmp);
transform2.transform(tmp, 0, dstPts, dstOff, numTmp);
srcOff += numTmp * sourceDim;
dstOff += numTmp * targetDim;
numPts -= numTmp;
} while (numPts != 0);
}
/**
* Transforms a list of coordinate point ordinal values. The source points are first copied
* in a temporary array of type {@code double[]}, transformed by {@link #transform1} first,
* then by {@link #transform2} and finally the result is casted to {@code float} primitive
* type and stored in the destination array. The use of {@code double} primitive type for
* intermediate results is necesssary for reducing rounding errors.
*/
@Override
public void transform(final float[] srcPts, int srcOff,
final float[] dstPts, int dstOff, int numPts)
throws TransformException
{
assert isValid();
if (numPts <= 0) {
return;
}
final int sourceDim = getSourceDimensions();
final int targetDim = getTargetDimensions();
final int intermDim = transform1.getTargetDimensions();
final int dimension = Math.max(Math.max(sourceDim, targetDim), intermDim);
int numTmp = numPts;
int length = numTmp * dimension;
if (length > TEMPORARY_ARRAY_LENGTH) {
numTmp = Math.max(1, TEMPORARY_ARRAY_LENGTH / dimension);
length = numTmp * dimension;
}
final double[] tmp = new double[length];
do {
if (numTmp > numPts) {
numTmp = numPts;
}
length = numTmp * sourceDim;
for (int i=0; i<length; i++) {
tmp[i] = srcPts[srcOff++];
}
transform1.transform(tmp, 0, tmp, 0, numTmp);
transform2.transform(tmp, 0, tmp, 0, numTmp);
length = numTmp * targetDim;
for (int i=0; i<length; i++) {
dstPts[dstOff++] = (float) tmp[i];
}
numPts -= numTmp;
} while (numPts != 0);
}
/**
* Creates the inverse transform of this object.
*/
@Override
public synchronized MathTransform inverse() throws NoninvertibleTransformException {
assert isValid();
if (inverse == null) {
inverse = createConcatenatedTransform(transform2.inverse(), transform1.inverse());
inverse.inverse = this;
}
return inverse;
}
/**
* Gets the derivative of this transform at a point. This method delegates to the
* {@link #derivative(DirectPosition)} method because the transformation steps
* {@link #transform1} and {@link #transform2} may not be instances of
* {@link MathTransform2D}.
*
* @param point The coordinate point where to evaluate the derivative.
* @return The derivative at the specified point as a 2×2 matrix.
* @throws TransformException if the derivative can't be evaluated at the specified point.
*/
@Override
public Matrix derivative(final Point2D point) throws TransformException {
return derivative(new GeneralDirectPosition(point));
}
/**
* Gets the derivative of this transform at a point.
*
* @param point The coordinate point where to evaluate the derivative.
* @return The derivative at the specified point (never {@code null}).
* @throws TransformException if the derivative can't be evaluated at the specified point.
*/
@Override
public Matrix derivative(final DirectPosition point) throws TransformException {
final Matrix matrix1 = transform1.derivative(point);
final Matrix matrix2 = transform2.derivative(transform1.transform(point, null));
// Compute "matrix = matrix2 * matrix1". Reuse an existing matrix object
// if possible, which is always the case when both matrix are square.
final int numRow = matrix2.getNumRow();
final int numCol = matrix1.getNumCol();
final XMatrix matrix;
if (numCol == matrix2.getNumCol()) {
matrix = toXMatrix(matrix2);
matrix.multiply(matrix1);
} else {
final GeneralMatrix m = new GeneralMatrix(numRow, numCol);
m.mul(toGMatrix(matrix2), toGMatrix(matrix1));
matrix = m;
}
return matrix;
}
/**
* Tests whether this transform does not move any points.
* Default implementation check if the two transforms are
* identity.
*/
@Override
public final boolean isIdentity() {
return transform1.isIdentity() && transform2.isIdentity();
}
/**
* Returns a hash value for this transform.
*/
@Override
public final int hashCode() {
return transform1.hashCode() + 37*transform2.hashCode();
}
/**
* Compares the specified object with this math transform for equality.
*/
@Override
public final boolean equals(final Object object) {
if (object == this) {
// Slight optimization
return true;
}
if (super.equals(object)) {
final ConcatenatedTransform that = (ConcatenatedTransform) object;
return Utilities.equals(this.transform1, that.transform1) &&
Utilities.equals(this.transform2, that.transform2);
}
return false;
}
/**
* Format the inner part of a
* <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
* Known Text</cite> (WKT)</A> element.
*
* @param formatter The formatter to use.
* @return The WKT element name.
*/
@Override
protected String formatWKT(final Formatter formatter) {
addWKT(formatter, transform1);
addWKT(formatter, transform2);
return "CONCAT_MT";
}
/**
* Append to a string buffer the WKT for the specified math transform.
*/
private static void addWKT(final Formatter formatter,
final MathTransform transform)
{
if (transform instanceof ConcatenatedTransform) {
final ConcatenatedTransform concat = (ConcatenatedTransform) transform;
addWKT(formatter, concat.transform1);
addWKT(formatter, concat.transform2);
} else {
formatter.append(transform);
}
}
}