/*
* 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.io.Serializable;
import org.opengis.referencing.operation.MathTransform;
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.opengis.geometry.MismatchedDimensionException;
import org.geotools.geometry.GeneralDirectPosition;
import org.geotools.referencing.operation.LinearTransform;
import org.geotools.referencing.operation.matrix.GeneralMatrix;
import org.geotools.referencing.wkt.Formatter;
import org.geotools.util.Utilities;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
/**
* Transform which passes through a subset of ordinates to another transform.
* This allows transforms to operate on a subset of ordinates. For example,
* if you have (<var>latitude</var>,<var>longitude</var>,<var>height</var>)
* coordinates, then you may wish to convert the height values from feet to
* meters without affecting the latitude and longitude values.
*
* @since 2.0
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*
* @see DimensionFilter
*/
public class PassThroughTransform extends AbstractMathTransform implements Serializable {
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = -1673997634240223449L;
/**
* Index of the first affected ordinate.
*/
protected final int firstAffectedOrdinate;
/**
* Number of unaffected ordinates after the affected ones.
* Always 0 when used through the strict OpenGIS API.
*/
protected final int numTrailingOrdinates;
/**
* The sub transform.
*
* @see #getSubTransform
*/
protected final MathTransform subTransform;
/**
* The inverse transform. This field will be computed only when needed.
* But it is serialized in order to avoid rounding error.
*/
private PassThroughTransform inverse;
/**
* Create a pass through transform.
*
* @param firstAffectedOrdinate Index of the first affected ordinate.
* @param subTransform The sub transform.
* @param numTrailingOrdinates Number of trailing ordinates to pass through.
* Affected ordinates will range from {@code firstAffectedOrdinate}
* inclusive to {@code dimTarget-numTrailingOrdinates} exclusive.
*/
protected PassThroughTransform(final int firstAffectedOrdinate,
final MathTransform subTransform,
final int numTrailingOrdinates)
{
if (firstAffectedOrdinate < 0) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
"firstAffectedOrdinate", firstAffectedOrdinate));
}
if (numTrailingOrdinates < 0) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
"numTrailingOrdinates", numTrailingOrdinates));
}
if (subTransform instanceof PassThroughTransform) {
final PassThroughTransform passThrough = (PassThroughTransform) subTransform;
this.firstAffectedOrdinate = passThrough.firstAffectedOrdinate + firstAffectedOrdinate;
this.numTrailingOrdinates = passThrough.numTrailingOrdinates + numTrailingOrdinates;
this.subTransform = passThrough.subTransform;
} else {
this.firstAffectedOrdinate = firstAffectedOrdinate;
this.numTrailingOrdinates = numTrailingOrdinates;
this.subTransform = subTransform;
}
}
/**
* Creates a transform which passes through a subset of ordinates to another transform.
* This allows transforms to operate on a subset of ordinates. For example, if you have
* (<var>latitidue</var>,<var>longitude</var>,<var>height</var>) coordinates, then you
* may wish to convert the height values from feet to meters without affecting the
* latitude and longitude values.
*
* @param firstAffectedOrdinate Index of the first affected ordinate.
* @param subTransform The sub transform.
* @param numTrailingOrdinates Number of trailing ordinates to pass through.
* Affected ordinates will range from {@code firstAffectedOrdinate}
* inclusive to {@code dimTarget-numTrailingOrdinates} exclusive.
* @return A pass through transform with the following dimensions:<br>
* <pre>
* Source: firstAffectedOrdinate + subTransform.getSourceDimensions() + numTrailingOrdinates
* Target: firstAffectedOrdinate + subTransform.getTargetDimensions() + numTrailingOrdinates</pre>
*/
public static MathTransform create(final int firstAffectedOrdinate,
final MathTransform subTransform,
final int numTrailingOrdinates)
{
if (firstAffectedOrdinate < 0) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
"firstAffectedOrdinate", firstAffectedOrdinate));
}
if (numTrailingOrdinates < 0) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
"numTrailingOrdinates", numTrailingOrdinates));
}
if (firstAffectedOrdinate==0 && numTrailingOrdinates==0) {
return subTransform;
}
/*
* Optimize the "Identity transform" case.
*/
if (subTransform.isIdentity()) {
final int dimension = subTransform.getSourceDimensions();
if (dimension == subTransform.getTargetDimensions()) {
return IdentityTransform.create(firstAffectedOrdinate + dimension + numTrailingOrdinates);
}
}
/*
* Special case for transformation backed by a matrix. Is is possible to use a
* new matrix for such transform, instead of wrapping the sub-transform into a
* PassThroughTransform object. It is faster and easier to concatenate.
*/
if (subTransform instanceof LinearTransform) {
GeneralMatrix matrix = toGMatrix(((LinearTransform)subTransform).getMatrix());
matrix = PassThroughTransform.expand(matrix, firstAffectedOrdinate, numTrailingOrdinates, 1);
return ProjectiveTransform.create(matrix);
}
/*
* Constructs the general PassThroughTransform object. An optimisation
* for the "Pass through case" is done right in the constructor.
*/
return new PassThroughTransform(firstAffectedOrdinate, subTransform, numTrailingOrdinates);
}
/**
* Returns the sub transform.
*
* @return The sub transform.
*
* @since 2.2
*/
public MathTransform getSubTransform() {
return subTransform;
}
/**
* Ordered sequence of positive integers defining the positions in a coordinate
* tuple of the coordinates affected by this pass-through transform. The returned
* index are for source coordinates.
*
* @return The modified coordinates.
*/
public int[] getModifiedCoordinates() {
final int[] index = new int[subTransform.getSourceDimensions()];
for (int i=0; i<index.length; i++) {
index[i] = i + firstAffectedOrdinate;
}
return index;
}
/**
* Gets the dimension of input points.
*/
public int getSourceDimensions() {
return firstAffectedOrdinate + subTransform.getSourceDimensions() + numTrailingOrdinates;
}
/**
* Gets the dimension of output points.
*/
public int getTargetDimensions() {
return firstAffectedOrdinate + subTransform.getTargetDimensions() + numTrailingOrdinates;
}
/**
* Tests whether this transform does not move any points.
*/
@Override
public boolean isIdentity() {
return subTransform.isIdentity();
}
/**
* Transforms a list of coordinate point ordinal values.
*/
@Override
public void transform(final float[] srcPts, int srcOff,
final float[] dstPts, int dstOff, int numPts)
throws TransformException
{
final int subDimSource = subTransform.getSourceDimensions();
final int subDimTarget = subTransform.getTargetDimensions();
int srcStep = numTrailingOrdinates;
int dstStep = numTrailingOrdinates;
if (srcPts==dstPts && srcOff<dstOff) {
final int dimSource = getSourceDimensions();
final int dimTarget = getTargetDimensions();
srcOff += numPts * dimSource;
dstOff += numPts * dimTarget;
srcStep -= 2*dimSource;
dstStep -= 2*dimTarget;
}
while (--numPts >= 0) {
System.arraycopy (srcPts, srcOff, dstPts, dstOff, firstAffectedOrdinate);
subTransform.transform(srcPts, srcOff+=firstAffectedOrdinate, dstPts, dstOff+=firstAffectedOrdinate, 1);
System.arraycopy (srcPts, srcOff+=subDimSource, dstPts, dstOff+=subDimTarget, numTrailingOrdinates);
srcOff += srcStep;
dstOff += dstStep;
}
}
/**
* Transforms a list of coordinate point ordinal values.
*/
public void transform(final double[] srcPts, int srcOff,
final double[] dstPts, int dstOff, int numPts)
throws TransformException
{
final int subDimSource = subTransform.getSourceDimensions();
final int subDimTarget = subTransform.getTargetDimensions();
int srcStep = numTrailingOrdinates;
int dstStep = numTrailingOrdinates;
if (srcPts==dstPts && srcOff<dstOff) {
final int dimSource = getSourceDimensions();
final int dimTarget = getTargetDimensions();
srcOff += numPts * dimSource;
dstOff += numPts * dimTarget;
srcStep -= 2*dimSource;
dstStep -= 2*dimTarget;
}
while (--numPts >= 0) {
System.arraycopy (srcPts, srcOff, dstPts, dstOff, firstAffectedOrdinate);
subTransform.transform(srcPts, srcOff+=firstAffectedOrdinate, dstPts, dstOff+=firstAffectedOrdinate, 1);
System.arraycopy (srcPts, srcOff+=subDimSource, dstPts, dstOff+=subDimTarget, numTrailingOrdinates);
srcOff += srcStep;
dstOff += dstStep;
}
}
/**
* Gets the derivative of this transform at a point.
*/
@Override
public Matrix derivative(final DirectPosition point) throws TransformException {
final int nSkipped = firstAffectedOrdinate + numTrailingOrdinates;
final int transDim = subTransform.getSourceDimensions();
final int pointDim = point.getDimension();
if (pointDim != transDim+nSkipped) {
throw new MismatchedDimensionException(Errors.format(
ErrorKeys.MISMATCHED_DIMENSION_$3, "point", pointDim, transDim + nSkipped));
}
final GeneralDirectPosition subPoint = new GeneralDirectPosition(transDim);
for (int i=0; i<transDim; i++) {
subPoint.ordinates[i] = point.getOrdinate(i + firstAffectedOrdinate);
}
return expand(toGMatrix(subTransform.derivative(subPoint)),
firstAffectedOrdinate, numTrailingOrdinates, 0);
}
/**
* Creates a pass through transform from a matrix. This method is invoked when the
* sub-transform can be express as a matrix. It is also invoked for computing the
* matrix returned by {@link #derivative}.
*
* @param subMatrix The sub-transform as a matrix.
* @param firstAffectedOrdinate Index of the first affected ordinate.
* @param numTrailingOrdinates Number of trailing ordinates to pass through.
* @param affine 0 if the matrix do not contains translation terms, or 1 if
* the matrix is an affine transform with translation terms.
*/
private static GeneralMatrix expand(final GeneralMatrix subMatrix,
final int firstAffectedOrdinate,
final int numTrailingOrdinates,
final int affine)
{
final int nSkipped = firstAffectedOrdinate + numTrailingOrdinates;
final int numRow = subMatrix.getNumRow() - affine;
final int numCol = subMatrix.getNumCol() - affine;
final GeneralMatrix matrix = new GeneralMatrix(numRow + nSkipped + affine,
numCol + nSkipped + affine);
matrix.setZero();
// Set UL part to 1: [ 1 0 ]
// [ 0 1 ]
// [ ]
// [ ]
// [ ]
for (int j=0; j<firstAffectedOrdinate; j++) {
matrix.setElement(j, j, 1);
}
// Set central part: [ 1 0 0 0 0 0 ]
// [ 0 1 0 0 0 0 ]
// [ 0 0 ? ? ? 0 ]
// [ 0 0 ? ? ? 0 ]
// [ ]
subMatrix.copySubMatrix(0, 0, numRow, numCol,
firstAffectedOrdinate, firstAffectedOrdinate, matrix);
// Set LR part to 1: [ 1 0 0 0 0 0 ]
// [ 0 1 0 0 0 0 ]
// [ 0 0 ? ? ? 0 ]
// [ 0 0 ? ? ? 0 ]
// [ 0 0 0 0 0 1 ]
final int offset = numCol-numRow;
final int numRowOut = numRow + nSkipped;
for (int j=numRowOut-numTrailingOrdinates; j<numRowOut; j++) {
matrix.setElement(j, j+offset, 1);
}
if (affine != 0) {
// Copy the translation terms in the last column.
subMatrix.copySubMatrix(0, numCol, numRow, affine,
firstAffectedOrdinate, numCol+nSkipped, matrix);
// Copy the last row as a safety, but it should contains only 0.
subMatrix.copySubMatrix(numRow, 0, affine, numCol,
numRow+nSkipped, firstAffectedOrdinate, matrix);
// Copy the lower right corner, which should contains only 1.
subMatrix.copySubMatrix(numRow, numCol, affine, affine,
numRow+nSkipped, numCol+nSkipped, matrix);
}
return matrix;
}
/**
* Creates the inverse transform of this object.
*/
@Override
public synchronized MathTransform inverse() throws NoninvertibleTransformException {
if (inverse == null) {
inverse = new PassThroughTransform(
firstAffectedOrdinate, subTransform.inverse(), numTrailingOrdinates);
inverse.inverse = this;
}
return inverse;
}
/**
* Returns a hash value for this transform.
* This value need not remain consistent between
* different implementations of the same class.
*/
@Override
public int hashCode() {
int code = (int)serialVersionUID + firstAffectedOrdinate + 37*numTrailingOrdinates;
if (subTransform != null) {
code ^= subTransform.hashCode();
}
return code;
}
/**
* Compares the specified object with this math transform for equality.
*/
@Override
public boolean equals(final Object object) {
if (object == this) {
return true;
}
if (super.equals(object)) {
final PassThroughTransform that = (PassThroughTransform) object;
return this.firstAffectedOrdinate == that.firstAffectedOrdinate &&
this.numTrailingOrdinates == that.numTrailingOrdinates &&
Utilities.equals(this.subTransform, that.subTransform);
}
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.
*
* @todo The {@link #numTrailingOrdinates} parameter is not part of OpenGIS specification.
* We should returns a more complex WKT when {@code numTrailingOrdinates != 0},
* using an affine transform to change the coordinates order.
*/
@Override
protected String formatWKT(final Formatter formatter) {
formatter.append(firstAffectedOrdinate);
if (numTrailingOrdinates != 0) {
formatter.append(numTrailingOrdinates);
formatter.setInvalidWKT(PassThroughTransform.class);
}
formatter.append(subTransform);
return "PASSTHROUGH_MT";
}
}