/* * 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. * * This package contains documentation from OpenGIS specifications. * OpenGIS consortium's work is fully acknowledged here. */ package org.geotools.referencing.operation.transform; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.io.Serializable; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import javax.vecmath.MismatchedSizeException; import javax.vecmath.SingularMatrixException; import javax.measure.unit.NonSI; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.operation.Conversion; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.geometry.DirectPosition; import org.geotools.metadata.iso.citation.Citations; import org.geotools.parameter.MatrixParameterDescriptors; import org.geotools.parameter.MatrixParameters; import org.geotools.referencing.NamedIdentifier; import org.geotools.referencing.operation.LinearTransform; import org.geotools.referencing.operation.MathTransformProvider; import org.geotools.referencing.operation.matrix.MatrixFactory; import org.geotools.referencing.operation.matrix.GeneralMatrix; import org.geotools.referencing.operation.matrix.XMatrix; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; /** * A usually affine, or otherwise a projective transform. A projective transform is capable of * mapping an arbitrary quadrilateral into another arbitrary quadrilateral, while preserving the * straightness of lines. In the special case where the transform is affine, the parallelism of * lines in the source is preserved in the output. * <p> * Such a coordinate transformation can be represented by a square {@linkplain GeneralMatrix matrix} * of an arbitrary size. Point coordinates must have a dimension equals to * <code>{@linkplain Matrix#getNumCol}-1</code>. For example, for square matrix of size 4×4, * coordinate points are three-dimensional. The transformed points <code>(x',y',z')</code> are * computed as below (note that this computation is similar to * {@link javax.media.jai.PerspectiveTransform} in <cite>Java Advanced Imaging</cite>): * * <blockquote><pre> * [ u ] [ m<sub>00</sub> m<sub>01</sub> m<sub>02</sub> m<sub>03</sub> ] [ x ] * [ v ] = [ m<sub>10</sub> m<sub>11</sub> m<sub>12</sub> m<sub>13</sub> ] [ y ] * [ w ] [ m<sub>20</sub> m<sub>21</sub> m<sub>22</sub> m<sub>23</sub> ] [ z ] * [ t ] [ m<sub>30</sub> m<sub>31</sub> m<sub>32</sub> m<sub>33</sub> ] [ 1 ] * * x' = u/t * y' = v/t * y' = w/t * </pre></blockquote> * * In the special case of an affine transform, the last row contains only zero * values except in the last column, which contains 1. * * @since 2.0 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * * @see javax.media.jai.PerspectiveTransform * @see java.awt.geom.AffineTransform * @see <A HREF="http://mathworld.wolfram.com/AffineTransformation.html">Affine transformation on MathWorld</A> */ public class ProjectiveTransform extends AbstractMathTransform implements LinearTransform, Serializable { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = -2104496465933824935L; /** * The number of rows. */ private final int numRow; /** * The number of columns. */ private final int numCol; /** * Elements of the matrix. Column indice vary fastest. */ private final double[] elt; /** * The inverse transform. Will be created only when first needed. */ private transient ProjectiveTransform inverse; /** * Constructs a transform from the specified matrix. * The matrix should be affine, but it will not be verified. * * @param matrix The matrix. */ protected ProjectiveTransform(final Matrix matrix) { numRow = matrix.getNumRow(); numCol = matrix.getNumCol(); elt = new double[numRow*numCol]; int index = 0; for (int j=0; j<numRow; j++) { for (int i=0; i<numCol; i++) { elt[index++] = matrix.getElement(j,i); } } } /** * Creates a transform for the specified matrix. * The matrix should be affine, but it is not be verified. * * @param matrix The affine transform as a matrix. * @return The transform for the given matrix. */ public static LinearTransform create(final Matrix matrix) { final int dimension = matrix.getNumRow()-1; if (dimension == matrix.getNumCol()-1) { if (matrix.isIdentity()) { return IdentityTransform.create(dimension); } final GeneralMatrix m = toGMatrix(matrix); if (m.isAffine()) { switch (dimension) { case 1: return LinearTransform1D.create(m.getElement(0,0), m.getElement(0,1)); case 2: return create(m.toAffineTransform2D()); } } } switch (dimension) { case 2: return new ProjectiveTransform2D(matrix); default: return new ProjectiveTransform (matrix); } } /** * Creates a transform for the specified matrix as a Java2D object. * This method is provided for interoperability with * <A HREF="http://java.sun.com/products/java-media/2D/index.jsp">Java2D</A>. * * @param matrix The affine transform as a matrix. * @return The transform for the given matrix. */ public static LinearTransform create(final AffineTransform matrix) { if (matrix.isIdentity()) { return IdentityTransform.create(2); } return new AffineTransform2D(matrix); } /** * Creates a transform that apply a uniform scale along all axis. * * @param dimension The input and output dimensions. * @param scale The scale factor. * @return The scale transform. * * @since 2.3 */ public static LinearTransform createScale(final int dimension, final double scale) { if (scale == 1) { return IdentityTransform.create(dimension); } final Matrix matrix = new GeneralMatrix(dimension + 1); for (int i=0; i<dimension; i++) { matrix.setElement(i, i, scale); } return create(matrix); } /** * Creates a transform that apply the same translation along all axis. * * @param dimension The input and output dimensions. * @param offset The translation. * @return The offset transform. * * @since 2.3 */ public static LinearTransform createTranslation(final int dimension, final double offset) { if (offset == 0) { return IdentityTransform.create(dimension); } final Matrix matrix = new GeneralMatrix(dimension + 1); for (int i=0; i<dimension; i++) { matrix.setElement(i, dimension, offset); } return create(matrix); } /** * Creates a matrix that keep only a subset of the ordinate values. * The dimension of source coordinates is {@code sourceDim} and * the dimension of target coordinates is {@code toKeep.length}. * * @param sourceDim the dimension of source coordinates. * @param toKeep the indices of ordinate values to keep. * @return The matrix to give to the {@link #create(Matrix)} * method in order to create the transform. * @throws IndexOutOfBoundsException if a value of {@code toKeep} * is lower than 0 or not smaller than {@code sourceDim}. */ public static Matrix createSelectMatrix(final int sourceDim, final int[] toKeep) throws IndexOutOfBoundsException { final int targetDim = toKeep.length; final XMatrix matrix = MatrixFactory.create(targetDim+1, sourceDim+1); matrix.setZero(); for (int j=0; j<targetDim; j++) { matrix.setElement(j, toKeep[j], 1); } matrix.setElement(targetDim, sourceDim, 1); return matrix; } /** * Returns the parameter descriptors for this math transform. */ @Override public ParameterDescriptorGroup getParameterDescriptors() { return ProviderAffine.PARAMETERS; } /** * Returns the matrix elements as a group of parameters values. The number of parameters * depends on the matrix size. Only matrix elements different from their default value * will be included in this group. * * @param matrix The matrix to returns as a group of parameters. * @return A copy of the parameter values for this math transform. */ static ParameterValueGroup getParameterValues(final Matrix matrix) { final MatrixParameters values; values = (MatrixParameters) ProviderAffine.PARAMETERS.createValue(); values.setMatrix(matrix); return values; } /** * Returns the matrix elements as a group of parameters values. The number of parameters * depends on the matrix size. Only matrix elements different from their default value * will be included in this group. * * @return A copy of the parameter values for this math transform. */ @Override public ParameterValueGroup getParameterValues() { return getParameterValues(getMatrix()); } /** * Transforms an array of floating point coordinates by this matrix. Point coordinates * must have a dimension equals to <code>{@link Matrix#getNumCol}-1</code>. For example, * for square matrix of size 4×4, coordinate points are three-dimensional and * stored in the arrays starting at the specified offset ({@code srcOff}) in the order * <code>[x<sub>0</sub>, y<sub>0</sub>, z<sub>0</sub>, * x<sub>1</sub>, y<sub>1</sub>, z<sub>1</sub>..., * x<sub>n</sub>, y<sub>n</sub>, z<sub>n</sub>]</code>. * * @param srcPts The array containing the source point coordinates. * @param srcOff The offset to the first point to be transformed in the source array. * @param dstPts The array into which the transformed point coordinates are returned. * @param dstOff The offset to the location of the first transformed point that is stored * in the destination array. The source and destination array sections can * be overlaps. * @param numPts The number of points to be transformed */ @Override public void transform(float[] srcPts, int srcOff, final float[] dstPts, int dstOff, int numPts) { final int inputDimension = numCol-1; // The last ordinate will be assumed equals to 1. final int outputDimension = numRow-1; final double[] buffer = new double[numRow]; if (srcPts == dstPts) { // We are going to write in the source array. Checks if // source and destination sections are going to clash. final int upperSrc = srcOff + numPts*inputDimension; if (upperSrc > dstOff) { if (inputDimension >= outputDimension ? dstOff > srcOff : dstOff + numPts*outputDimension > upperSrc) { // If source overlaps destination, then the easiest workaround is // to copy source data. This is not the most efficient however... srcPts = new float[numPts*inputDimension]; System.arraycopy(dstPts, srcOff, srcPts, 0, srcPts.length); srcOff = 0; } } } while (--numPts >= 0) { int mix = 0; for (int j=0; j<numRow; j++) { double sum=elt[mix + inputDimension]; for (int i=0; i<inputDimension; i++) { sum += srcPts[srcOff+i]*elt[mix++]; } buffer[j] = sum; mix++; } final double w = buffer[outputDimension]; for (int j=0; j<outputDimension; j++) { // 'w' is equals to 1 if the transform is affine. dstPts[dstOff++] = (float) (buffer[j]/w); } srcOff += inputDimension; } } /** * Transforms an array of floating point coordinates by this matrix. Point coordinates * must have a dimension equals to <code>{@link Matrix#getNumCol}-1</code>. For example, * for square matrix of size 4×4, coordinate points are three-dimensional and * stored in the arrays starting at the specified offset ({@code srcOff}) in the order * <code>[x<sub>0</sub>, y<sub>0</sub>, z<sub>0</sub>, * x<sub>1</sub>, y<sub>1</sub>, z<sub>1</sub>..., * x<sub>n</sub>, y<sub>n</sub>, z<sub>n</sub>]</code>. * * @param srcPts The array containing the source point coordinates. * @param srcOff The offset to the first point to be transformed in the source array. * @param dstPts The array into which the transformed point coordinates are returned. * @param dstOff The offset to the location of the first transformed point that is stored * in the destination array. The source and destination array sections can * be overlaps. * @param numPts The number of points to be transformed */ public void transform(double[] srcPts, int srcOff, final double[] dstPts, int dstOff, int numPts) { final int inputDimension = numCol-1; // The last ordinate will be assumed equals to 1. final int outputDimension = numRow-1; final double[] buffer = new double[numRow]; if (srcPts == dstPts) { // We are going to write in the source array. Checks if // source and destination sections are going to clash. final int upperSrc = srcOff + numPts*inputDimension; if (upperSrc > dstOff) { if (inputDimension >= outputDimension ? dstOff > srcOff : dstOff + numPts*outputDimension > upperSrc) { // If source overlaps destination, then the easiest workaround is // to copy source data. This is not the most efficient however... srcPts = new double[numPts*inputDimension]; System.arraycopy(dstPts, srcOff, srcPts, 0, srcPts.length); srcOff = 0; } } } while (--numPts >= 0) { int mix = 0; for (int j=0; j<numRow; j++) { double sum=elt[mix + inputDimension]; for (int i=0; i<inputDimension; i++) { sum += srcPts[srcOff+i]*elt[mix++]; } buffer[j] = sum; mix++; } final double w = buffer[outputDimension]; for (int j=0; j<outputDimension; j++) { // 'w' is equals to 1 if the transform is affine. dstPts[dstOff++] = buffer[j]/w; } srcOff += inputDimension; } } /** * Gets the derivative of this transform at a point. * For a matrix transform, the derivative is the * same everywhere. */ @Override public Matrix derivative(final Point2D point) { return derivative((DirectPosition)null); } /** * Gets the derivative of this transform at a point. * For a matrix transform, the derivative is the * same everywhere. */ @Override public Matrix derivative(final DirectPosition point) { final GeneralMatrix matrix = getGeneralMatrix(); matrix.setSize(numRow-1, numCol-1); return matrix; } /** * Returns a copy of the matrix. */ public Matrix getMatrix() { return getGeneralMatrix(); } /** * Returns a copy of the matrix. */ private GeneralMatrix getGeneralMatrix() { return new GeneralMatrix(numRow, numCol, elt); } /** * Gets the dimension of input points. */ public int getSourceDimensions() { return numCol - 1; } /** * Gets the dimension of output points. */ public int getTargetDimensions() { return numRow - 1; } /** * Tests whether this transform does not move any points. */ @Override public boolean isIdentity() { if (numRow != numCol) { return false; } int index=0; for (int j=0; j<numRow; j++) { for (int i=0; i<numCol; i++) { if (elt[index++] != (i==j ? 1 : 0)) { return false; } } } assert isIdentity(0); return true; } /** * Tests whether this transform does not move any points by using the provided tolerance. * This method work in the same way than * {@link org.geotools.referencing.operation.matrix.XMatrix#isIdentity(double)}. * * @since 2.4 */ public boolean isIdentity(double tolerance) { tolerance = Math.abs(tolerance); if (numRow != numCol) { return false; } int index=0; for (int j=0; j<numRow; j++) { for (int i=0; i<numCol; i++) { double e = elt[index++]; if (i == j) { e--; } // Uses '!' in order to catch NaN values. if (!(Math.abs(e) <= tolerance)) { return false; } } } return true; } /** * Creates the inverse transform of this object. */ @Override public synchronized MathTransform inverse() throws NoninvertibleTransformException { if (inverse == null) { if (isIdentity()) { inverse = this; } else { final XMatrix matrix = getGeneralMatrix(); try { matrix.invert(); } catch (SingularMatrixException exception) { throw new NoninvertibleTransformException(Errors.format( ErrorKeys.NONINVERTIBLE_TRANSFORM), exception); } catch (MismatchedSizeException exception) { // This exception is thrown if the matrix is not square. throw new NoninvertibleTransformException(Errors.format( ErrorKeys.NONINVERTIBLE_TRANSFORM), exception); } inverse = new ProjectiveTransform(matrix); inverse.inverse = this; } } return inverse; } /** * Creates an inverse transform using the specified matrix. * To be overridden by {@link GeocentricTranslation}. */ MathTransform createInverse(final Matrix matrix) { return new ProjectiveTransform(matrix); } /** * Returns a hash value for this transform. * This value need not remain consistent between * different implementations of the same class. */ @Override public int hashCode() { long code = serialVersionUID; for (int i=elt.length; --i>=0;) { code = code*37 + Double.doubleToLongBits(elt[i]); } return (int)(code >>> 32) ^ (int)code; } /** * Compares the specified object with * this math transform for equality. */ @Override public boolean equals(final Object object) { if (object == this) { // Slight optimization return true; } if (super.equals(object)) { final ProjectiveTransform that = (ProjectiveTransform) object; return this.numRow == that.numRow && this.numCol == that.numCol && Arrays.equals(this.elt, that.elt); } return false; } /** * The provider for the "<cite>Affine general parametric transformation</cite>" (EPSG 9624). * The OGC's name is {@code "Affine"}. The default matrix size is * {@value org.geotools.parameter.MatrixParameterDescriptors#DEFAULT_MATRIX_SIZE}×{@value * org.geotools.parameter.MatrixParameterDescriptors#DEFAULT_MATRIX_SIZE}. * <p> * Note that affine transform is a special case of projective transform. * * @version $Id$ * @author Martin Desruisseaux (IRD) */ public static final class ProviderAffine extends MathTransformProvider { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 649555815622129472L; /** * The set of predefined providers. */ private static final ProviderAffine[] methods = new ProviderAffine[8]; /** * The parameters group. * * @todo We should register EPSG parameter identifiers (A0, A1, A2, B0, B1, B2) as well. */ static final ParameterDescriptorGroup PARAMETERS; static { final NamedIdentifier name = new NamedIdentifier(Citations.OGC, "Affine"); final Map<String,Object> properties = new HashMap<String,Object>(4, 0.8f); properties.put(NAME_KEY, name); properties.put(IDENTIFIERS_KEY, name); properties.put(ALIAS_KEY, new NamedIdentifier[] {name, new NamedIdentifier(Citations.EPSG, "Affine general parametric transformation"), new NamedIdentifier(Citations.EPSG, "9624"), new NamedIdentifier(Citations.GEOTOOLS, Vocabulary.formatInternational(VocabularyKeys.AFFINE_TRANSFORM)) }); PARAMETERS = new MatrixParameterDescriptors(properties); } /** * Creates a provider for affine transform with a default matrix size. */ public ProviderAffine() { this(MatrixParameterDescriptors.DEFAULT_MATRIX_SIZE-1, MatrixParameterDescriptors.DEFAULT_MATRIX_SIZE-1); methods[MatrixParameterDescriptors.DEFAULT_MATRIX_SIZE-2] = this; } /** * Creates a provider for affine transform with the specified dimensions. */ private ProviderAffine(final int sourceDimensions, final int targetDimensions) { super(sourceDimensions, targetDimensions, PARAMETERS); } /** * Returns the operation type. */ @Override public Class<Conversion> getOperationType() { return Conversion.class; } /** * Creates a projective transform from the specified group of parameter values. * * @param values The group of parameter values. * @return The created math transform. * @throws ParameterNotFoundException if a required parameter was not found. */ protected MathTransform createMathTransform(final ParameterValueGroup values) throws ParameterNotFoundException { final MathTransform transform; transform = create(((MatrixParameterDescriptors) getParameters()).getMatrix(values)); return new Delegate(transform, getProvider(transform.getSourceDimensions(), transform.getTargetDimensions())); } /** * Returns the operation method for the specified source and target dimensions. * This method provides different methods for different matrix sizes. * * @param sourceDimensions The number of source dimensions. * @param targetDimensions The number of target dimensions. * @return The provider for transforms of the given source and target dimensions. */ public static ProviderAffine getProvider(final int sourceDimensions, final int targetDimensions) { if (sourceDimensions == targetDimensions) { final int i = sourceDimensions - 1; if (i>=0 && i<methods.length) { ProviderAffine method = methods[i]; if (method == null) { methods[i] = method = new ProviderAffine(sourceDimensions, targetDimensions); } return method; } } return new ProviderAffine(sourceDimensions, targetDimensions); } } /** * The provider for the "<cite>Longitude rotation</cite>" (EPSG 9601). * * @version $Id$ * @author Martin Desruisseaux (IRD) */ public static final class ProviderLongitudeRotation extends MathTransformProvider { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = -2104496465933824935L; /** * The longitude offset. */ public static final ParameterDescriptor OFFSET = createDescriptor( new NamedIdentifier[] { new NamedIdentifier(Citations.EPSG, "Longitude offset") }, Double.NaN, -180, +180, NonSI.DEGREE_ANGLE); /** * The parameters group. */ static final ParameterDescriptorGroup PARAMETERS = createDescriptorGroup(new NamedIdentifier[] { new NamedIdentifier(Citations.EPSG, "Longitude rotation"), new NamedIdentifier(Citations.EPSG, "9601") }, new ParameterDescriptor[] { OFFSET }); /** * Constructs a provider with default parameters. */ public ProviderLongitudeRotation() { super(2, 2, PARAMETERS); } /** * Returns the operation type. */ @Override public Class<Conversion> getOperationType() { return Conversion.class; } /** * Creates a transform from the specified group of parameter values. * * @param values The group of parameter values. * @return The created math transform. * @throws ParameterNotFoundException if a required parameter was not found. */ protected MathTransform createMathTransform(final ParameterValueGroup values) throws ParameterNotFoundException { final double offset = doubleValue(OFFSET, values); return create(AffineTransform.getTranslateInstance(offset, 0)); } } }