/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-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.builder; import java.awt.Point; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Arrays; import org.opengis.parameter.ParameterValueGroup; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.Transformation; import org.opengis.referencing.operation.TransformException; import org.geotools.metadata.iso.citation.Citations; import org.geotools.referencing.NamedIdentifier; import org.geotools.referencing.operation.MathTransformProvider; import org.geotools.referencing.operation.matrix.Matrix2; import org.geotools.referencing.operation.transform.AbstractMathTransform; import org.geotools.util.logging.Logging; import org.geotools.util.Utilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * Transform a set of coordinate points using a grid of localization. * Input coordinates are index in this two-dimensional array. * Those input coordinates (or index) should be in the range * * <code>x</sub>input</sub> = [0..width-1]</code> and * <code>y</sub>input</sub> = [0..height-1]</code> inclusive, * * where {@code width} and {@code height} are the number of columns and * rows in the grid of localization. Output coordinates are the values stored in * the grid of localization at the specified index. If input coordinates (index) * are non-integer values, then output coordinates are interpolated using a bilinear * interpolation. If input coordinates are outside the grid range, then output * coordinates are extrapolated. * * @since 2.0 * @source $URL$ * @version $Id$ * @author Remi Eve * @author Martin Desruisseaux (IRD) * * @todo This class should extends {@link WarpTransform2D} and constructs a * {@link javax.media.jai.WarpGrid} the first time the {@link WarpTransform2D#getWarp()} * method is invoked (GEOT-522). */ final class LocalizationGridTransform2D extends AbstractMathTransform implements MathTransform2D, Serializable { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 1067560328828441295L; /** * Maximal number of iterations to try before to fail * during an inverse transformation. */ private static final int MAX_ITER = 40; /** * Set to {@code true} for a conservative (and maybe slower) algorithm * in {@link #inverseTransform}. */ private static final boolean CONSERVATIVE = true; /** * Set to {@code true} for forcing {@link #inverseTransform} to returns * a value instead of throwing an exception if the transform do not converge. * This is a temporary flag until we find why the inverse transform fails to * converge in some case. */ private static final boolean MASK_NON_CONVERGENCE; static { String property; try { property = System.getProperty("org.geotools.referencing.forceConvergence", "false"); } catch (SecurityException exception) { // We are running in an applet. property = "false"; } MASK_NON_CONVERGENCE = property.equalsIgnoreCase("true"); } /** * <var>x</var> (usually longitude) offset relative to an entry. * Points are stored in {@link #grid} as {@code (x,y)} pairs. */ static final int X_OFFSET = 0; /** * <var>y</var> (usually latitude) offset relative to an entry. * Points are stored in {@link #grid} as {@code (x,y)} pairs. */ static final int Y_OFFSET = 1; /** * Length of an entry in the {@link #grid} array. This lenght * is equals to the dimension of output coordinate points. */ static final int CP_LENGTH = 2; /** * Number of grid's columns. */ private final int width; /** * Number of grid's rows. */ private final int height; /** * Grid of coordinate points. * Points are stored as {@code (x,y)} pairs. */ private final double[] grid; /** * A global affine transform for the whole grid. */ private final AffineTransform global; /** * The inverse math transform. Will be constructed only when first requested. */ private transient MathTransform2D inverse; /** * Constructs a localization grid using the specified data. * * @param width Number of grid's columns. * @param height Number of grid's rows. * @param grid The localization grid as an array of {@code (x,y)} coordinates. * This array is not cloned; this is the caller's responsability to ensure * that it will not be modified as long as this transformation is strongly * reachable. * @param global A global affine transform for the whole grid. */ protected LocalizationGridTransform2D(final int width, final int height, final double[] grid, final AffineTransform global) { this.width = width; this.height = height; this.grid = grid; this.global = global; } /** * Returns the parameter descriptors for this math transform. */ @Override public ParameterDescriptorGroup getParameterDescriptors() { return Provider.PARAMETERS; } /** * Returns the dimension of input points. */ public int getSourceDimensions() { return 2; } /** * Returns the dimension of output points. */ public int getTargetDimensions() { return 2; } /** * Tests if this transform is the identity transform. */ @Override public boolean isIdentity() { return false; } /** * Gets the derivative of this transform at a point. */ @Override public Matrix derivative(final Point2D point) { final AffineTransform tr = new AffineTransform(); getAffineTransform(point.getX(), point.getY(), tr); return new Matrix2(tr.getScaleX(), tr.getShearX(), tr.getShearY(), tr.getScaleY()); } /** * Transforme des coordonnées sources (généralement des index de pixels) en coordonnées * destinations (généralement des degrés de longitude et latitude). Les transformations * feront intervenir des interpolations linéaires si les coordonnées sources ne sont pas * entières. * * @param srcPts Points d'entrée. * @param srcOff Index du premier point d'entrée à transformer. * @param dstPts Points de sortie. * @param dstOff Index du premier point de sortie. * @param numPts Nombre de points à transformer. */ @Override public void transform(final float[] srcPts, int srcOff, final float[] dstPts, int dstOff, int numPts) { transform(srcPts, null, srcOff, dstPts, null, dstOff, numPts); } /** * Transforme des coordonnées sources (généralement des index de pixels) en coordonnées * destinations (généralement des degrés de longitude et latitude). Les transformations * feront intervenir des interpolations linéaires si les coordonnées sources ne sont pas * entières. * * @param srcPts Points d'entrée. * @param srcOff Index du premier point d'entrée à transformer. * @param dstPts Points de sortie. * @param dstOff Index du premier point de sortie. * @param numPts Nombre de points à transformer. */ public void transform(final double[] srcPts, int srcOff, final double[] dstPts, int dstOff, int numPts) { transform(null, srcPts, srcOff, null, dstPts, dstOff, numPts); } /** * Implementation of direct transformation. */ private void transform(final float[] srcPts1, final double[] srcPts2, int srcOff, final float[] dstPts1, final double[] dstPts2, int dstOff, int numPts) { final int minCol = 0; final int minRow = 0; final int maxCol = width - 2; final int maxRow = height - 2; int postIncrement = 0; if (srcOff < dstOff) { if ((srcPts2!=null) ? srcPts2==dstPts2 : srcPts1==dstPts1) { srcOff += (numPts-1)*2; dstOff += (numPts-1)*2; postIncrement = -4; } } while (--numPts >= 0) { final double xi, yi; if (srcPts2 != null) { xi = srcPts2[srcOff++]; yi = srcPts2[srcOff++]; } else { xi = srcPts1[srcOff++]; yi = srcPts1[srcOff++]; } final int col = Math.max(Math.min((int)xi, maxCol), minCol); final int row = Math.max(Math.min((int)yi, maxRow), minRow); final int offset00 = (col + row*width)*CP_LENGTH; final int offset01 = offset00 + CP_LENGTH*width; // Une ligne plus bas final int offset10 = offset00 + CP_LENGTH; // Une colonne à droite final int offset11 = offset01 + CP_LENGTH; // Une colonne à droite, une ligne plus bas /* * Interpole les coordonnées de destination [00]--.(x0,y0)----[10] * sur la ligne courante (x0,y0) ainsi que | | * sur la ligne suivante (x1,y1). Exemple | .(x,y) | * ci-contre: les coordonnées sources sont | | * entre crochets, et les coordonnées de la [01]--.(x1,y1)----[11] * sortie (à calculer) sont entre parenthèses. */ final double x0 = linearInterpolation(col+0, grid[offset00 + X_OFFSET], col+1, grid[offset10 + X_OFFSET], xi); final double y0 = linearInterpolation(col+0, grid[offset00 + Y_OFFSET], col+1, grid[offset10 + Y_OFFSET], xi); final double x1 = linearInterpolation(col+0, grid[offset01 + X_OFFSET], col+1, grid[offset11 + X_OFFSET], xi); final double y1 = linearInterpolation(col+0, grid[offset01 + Y_OFFSET], col+1, grid[offset11 + Y_OFFSET], xi); /* * Interpole maintenant les coordonnées (x,y) entre les deux lignes. */ final double xf = linearInterpolation(row, x0, row+1, x1, yi); final double yf = linearInterpolation(row, y0, row+1, y1, yi); if (dstPts2 != null) { dstPts2[dstOff++] = xf; dstPts2[dstOff++] = yf; } else { dstPts1[dstOff++] = (float) xf; dstPts1[dstOff++] = (float) yf; } srcOff += postIncrement; dstOff += postIncrement; if (false) { final java.io.PrintStream out = System.out; out.print("TD ==> xi : "); out.print (xi); out.print( " / yi : "); out.print (yi); out.print(" ---> xo : "); out.print (xf); out.print( " / yo : "); out.println(yf); } } } /** * Interpole/extrapole entre deux points. * * @param x1 Coordonnee <var>x</var> du premier point. * @param y1 Coordonnee <var>y</var> du premier point. * @param x2 Coordonnee <var>x</var> du second point. * @param y2 Coordonnee <var>y</var> du second point. * @param x Position <var>x</var> à laquelle calculer la valeur de <var>y</var>. * @return La valeur <var>y</var> interpolée entre les deux points. */ private static double linearInterpolation(final double x1, final double y1, final double x2, final double y2, final double x) { return y1 + (y2-y1)/(x2-x1) * (x-x1); } /** * Retourne une approximation de la transformation affine à la position indiquée. * * @param col Coordonnee <var>x</var> du point. * @param row Coordonnee <var>y</var> du point. * @param dest Matrice dans laquelle écrire la transformation affine. */ private void getAffineTransform(double x, double y, final AffineTransform dest) { int col = (int) x; int row = (int) y; if (col > width -2) col = width -2; if (row > height-2) row = height-2; if (col < 0) col = 0; if (row < 0) row = 0; final int sgnCol; final int sgnRow; if (x-col > 0.5) { sgnCol = -1; col++; } else sgnCol = +1; if (y-row > 0.5) { sgnRow = -1; row++; } else sgnRow = +1; /* * Le calcul de la transformation affine comprend 6 P00------P10 * inconnues. Sa solution recquiert donc 6 équations. | | * Nous les obtenons en utilisant 3 points, chaque | | * points ayant 2 coordonnées. Voir exemple ci-contre: P01----(ignoré) */ final int offset00 = (col + row*width)*CP_LENGTH; final int offset01 = offset00 + sgnRow*CP_LENGTH*width; final int offset10 = offset00 + sgnCol*CP_LENGTH; x = grid[offset00 + X_OFFSET]; y = grid[offset00 + Y_OFFSET]; final double dxCol = (grid[offset10 + X_OFFSET] - x) * sgnCol; final double dyCol = (grid[offset10 + Y_OFFSET] - y) * sgnCol; final double dxRow = (grid[offset01 + X_OFFSET] - x) * sgnRow; final double dyRow = (grid[offset01 + Y_OFFSET] - y) * sgnRow; dest.setTransform(dxCol, dyCol, dxRow, dyRow, x - dxCol*col - dxRow*row, y - dyCol*col - dyRow*row); /* * Si l'on transforme les 3 points qui ont servit à déterminer la transformation * affine, on devrait obtenir un résultat identique (aux erreurs d'arrondissement * près) peu importe que l'on utilise la transformation affine ou la grille de * localisation. */ assert distance(new Point(col, row ), dest) < 1E-5; assert distance(new Point(col+sgnCol, row ), dest) < 1E-5; assert distance(new Point(col, row+sgnRow), dest) < 1E-5; } /** * Transform a point using the localization grid, transform it back using the inverse * of the specified affine transform, and returns the distance between the source and * the resulting point. This is used for assertions only. * * @param index The source point to test. * @param tr The affine transform to test. * @return The distance in grid coordinate. Should be close to 0. */ private double distance(final Point2D index, final AffineTransform tr) { try { Point2D geoCoord = transform(index, null); geoCoord = tr.inverseTransform(geoCoord, geoCoord); return geoCoord.distance(index); } catch (TransformException exception) { // Should not happen throw new AssertionError(exception); } catch (NoninvertibleTransformException exception) { // Not impossible. What should we do? Open question... throw new AssertionError(exception); } } /** * Apply the inverse transform to a set of points. More specifically, this method transform * "real world" coordinates to grid coordinates. This method use an iterative algorithm for * that purpose. A {@link TransformException} is thrown in the computation do not converge. * The algorithm applied by this method and its callers is: * * <ul> * <li>Transform the first point using a "global" affine transform (i.e. the affine * transformed computed using the "least squares" method in LocalizationGrid). * Other points will be transformed using the last successful affine transform, * since we assume that the points to transform are close to each other.</li> * * <li>Next, compute a local affine transform and use if for transforming the point * again. Recompute again the local affine transform and continue until the cell * (x0,y0) doesn't change.</li> * </ul> * * @param source The "real world" coordinate to transform. * @param target A pre-allocated destination point. <strong>This point * can't be the same than {@code source}!<strong> * @param tr In input, the affine transform to use for the first step. * In output, the last affine transform used for the transformation. * */ final void inverseTransform(final Point2D source, final Point2D.Double target, final AffineTransform tr) throws TransformException { if (CONSERVATIVE) { // In an optimal approach, we should reuse the same affine transform than the one used // in the last transformation, since it is likely to converge faster for a point close // to the previous one. However, it may lead to strange and hard to predict // discontinuity in transformations. tr.setTransform(global); } try { tr.inverseTransform(source, target); int previousX = (int)target.x; int previousY = (int)target.y; for (int iter=0; iter<MAX_ITER; iter++) { getAffineTransform(target.x, target.y, tr); tr.inverseTransform(source, target); final int ix = (int)target.x; final int iy = (int)target.y; if (previousX==ix && previousY==iy) { // Computation converged. if (target.x>=0 && target.x<width && target.y>=0 && target.y<height) { // Point is inside the grid. Check the precision. assert transform(target, null).distanceSq(source) < 1E-3 : target; } else { // Point is outside the grid. Use the global transform for uniformity. inverseTransform(source, target); } return; } previousX = ix; previousY = iy; } /* * No convergence found in the "ordinary" loop. The following code checks if * we are stuck in a never-ending loop. If yes, then it will try to minimize * the following function: * * {@code transform(target).distance(source)}. */ final int x0 = previousX; final int y0 = previousY; global.inverseTransform(source, target); double x,y; double bestX = x = target.x; double bestY = y = target.y; double minSq = Double.POSITIVE_INFINITY; for (int iter=1-MAX_ITER; iter<MAX_ITER; iter++) { previousX = (int)x; previousY = (int)y; getAffineTransform(x, y, tr); tr.inverseTransform(source, target); x = target.x; y = target.y; final int ix = (int)x; final int iy = (int)y; if (previousX==ix && previousY==iy) { // Computation converged. assert iter >= 0; if (x>=0 && x<width && y>=0 && y<height) { // Point is inside the grid. Check the precision. assert transform(target, null).distanceSq(source) < 1E-3 : target; } else { // Point is outside the grid. Use the global transform for uniformity. inverseTransform(source, target); } return; } if (iter == 0) { assert x0==ix && y0==iy; } else if (x0==ix && y0==iy) { // Loop detected. if (bestX>=0 && bestX<width && bestY>=0 && bestY<height) { target.x = bestX; target.y = bestY; } else { inverseTransform(source, target); } return; } transform(target, target); final double distanceSq = target.distanceSq(source); if (distanceSq < minSq) { minSq = distanceSq; bestX = x; bestY = y; } } /* * The transformation didn't converge, and no loop has been found. * If the following block is enabled (true), then the best point * will be returned. It may not be the best approach since we don't * know if this point is valid. Otherwise, an exception is thrown. */ if (MASK_NON_CONVERGENCE) { Logging.getLogger(LocalizationGridTransform2D.class).fine("No convergence"); if (bestX>=0 && bestX<width && bestY>=0 && bestY<height) { target.x = bestX; target.y = bestY; } else { inverseTransform(source, target); } return; } } catch (NoninvertibleTransformException exception) { final TransformException e; e = new TransformException(Errors.format(ErrorKeys.NONINVERTIBLE_TRANSFORM)); e.initCause(exception); throw e; } throw new TransformException(Errors.format(ErrorKeys.NO_CONVERGENCE)); } /** * Inverse transforms a point using the {@link #global} affine transform, and * make sure that the result point is outside the grid. This method is used * for the transformation of a point which shouldn't be found in the grid. * * @param source The source coordinate point. * @param target The target coordinate point (should not be {@code null}). * @throws NoninvertibleTransformException if the transform is non-invertible. * * @todo Current implementation project an inside point on the nearest border. * Could we do something better? */ private void inverseTransform(final Point2D source, final Point2D.Double target) throws NoninvertibleTransformException { if (global.inverseTransform(source, target) != target) { throw new AssertionError(); // Should not happen. } double x = target.x; double y = target.y; if (x>=0 && x<width && y>=0 && y<height) { // Project on the nearest border. TODO: Could we do something better here? x -= 0.5 * width; y -= 0.5 * height; if (Math.abs(x) < Math.abs(y)) { target.x = x>0 ? width : -1; } else { target.y = y>0 ? height : -1; } } } /** * Returns the inverse transform. */ @Override public MathTransform2D inverse() { if (inverse == null) { inverse = new Inverse(); } return inverse; } /** * The inverse transform. This inner class is the inverse of the enclosing math transform. * * @version $Id$ * @author Martin Desruisseaux (IRD) */ private final class Inverse extends AbstractMathTransform.Inverse implements MathTransform2D, Serializable { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 4876426825123740986L; /** * Default constructor. */ public Inverse() { LocalizationGridTransform2D.this.super(); } /** * Transform a "real world" coordinate into a grid coordinate. */ @Override public Point2D transform(final Point2D ptSrc, final Point2D ptDst) throws TransformException { final AffineTransform tr = new AffineTransform(global); if (ptDst == null) { final Point2D.Double target = new Point2D.Double(); inverseTransform(ptSrc, target, tr); return target; } if (ptDst!=ptSrc && (ptDst instanceof Point2D.Double)) { inverseTransform(ptSrc, (Point2D.Double) ptDst, tr); return ptDst; } final Point2D.Double target = new Point2D.Double(); inverseTransform(ptSrc, target, tr); ptDst.setLocation(target); return ptDst; } /** * Apply the inverse transform to a set of points. More specifically, this method transform * "real world" coordinates to grid coordinates. This method use an iterative algorithm for * that purpose. A {@link TransformException} is thrown in the computation do not converge. * * @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. * May be the same than {@code srcPts}. * @param dstOff the offset to the location of the first transformed * point that is stored in the destination array. * @param numPts the number of point objects to be transformed. * @throws TransformException if a point can't be transformed. */ @Override public void transform(final float[] srcPts, int srcOff, final float[] dstPts, int dstOff, int numPts) throws TransformException { int postIncrement = 0; if (srcPts == dstPts && srcOff < dstOff) { srcOff += (numPts-1)*2; dstOff += (numPts-1)*2; postIncrement = -4; } final Point2D.Double source = new Point2D.Double(); final Point2D.Double target = new Point2D.Double(); final AffineTransform tr = new AffineTransform(global); while (--numPts >= 0) { source.x = srcPts[srcOff++]; source.y = srcPts[srcOff++]; inverseTransform(source, target, tr); dstPts[dstOff++] = (float)target.x; dstPts[dstOff++] = (float)target.y; srcOff += postIncrement; dstOff += postIncrement; } } /** * Apply the inverse transform to a set of points. More specifically, this method transform * "real world" coordinates to grid coordinates. This method use an iterative algorithm for * that purpose. A {@link TransformException} is thrown in the computation do not converge. * * @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. * May be the same than {@code srcPts}. * @param dstOff the offset to the location of the first transformed * point that is stored in the destination array. * @param numPts the number of point objects to be transformed. * @throws TransformException if a point can't be transformed. */ public void transform(final double[] srcPts, int srcOff, final double[] dstPts, int dstOff, int numPts) throws TransformException { int postIncrement = 0; if (srcPts == dstPts && srcOff < dstOff) { srcOff += (numPts-1)*2; dstOff += (numPts-1)*2; postIncrement = -4; } final Point2D.Double source = new Point2D.Double(); final Point2D.Double target = new Point2D.Double(); final AffineTransform tr = new AffineTransform(global); while (--numPts >= 0) { source.x = srcPts[srcOff++]; source.y = srcPts[srcOff++]; inverseTransform(source, target, tr); dstPts[dstOff++] = target.x; dstPts[dstOff++] = target.y; srcOff += postIncrement; dstOff += postIncrement; } } /** * Returns the original localization grid transform. */ @Override public MathTransform2D inverse() { return (MathTransform2D) super.inverse(); } /** * Restore reference to this object after deserialization. */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); LocalizationGridTransform2D.this.inverse = this; } } /** * Returns a hash value for this transform. */ @Override public int hashCode() { return (int)serialVersionUID ^ super.hashCode() ^ global.hashCode(); } /** * Compares this transform with the specified object for equality. */ @Override public boolean equals(final Object object) { if (super.equals(object)) { final LocalizationGridTransform2D that = (LocalizationGridTransform2D) object; return this.width == that.width && this.height == that.height && Utilities.equals(this.global, that.global) && Arrays.equals(this.grid, that.grid); } return false; } /** * The provider for the {@link LocalizationGridTransform2D}. * * @version $Id$ * @author Martin Desruisseaux (IRD) * * @todo Not yet fully implemented. Once it is implemented, we need to add a * getParameterValues() method in LocalizationGridTransform2D. */ private static class Provider extends MathTransformProvider { /** Serial number for interoperability with different versions. */ private static final long serialVersionUID = -8263439392080019340L; /** * The parameters group. */ static final ParameterDescriptorGroup PARAMETERS = createDescriptorGroup(new NamedIdentifier[] { new NamedIdentifier(Citations.GEOTOOLS, "WarpPolynomial") }, new ParameterDescriptor[] {}); /** * Create a provider for warp transforms. */ public Provider() { super(2, 2, PARAMETERS); } /** * Returns the operation type. */ @Override public Class<Transformation> getOperationType() { return Transformation.class; } /** * Creates a warp 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, FactoryException { throw new FactoryException("Not yet implemented"); } } }