/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-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.util.Arrays;
import java.util.Random;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
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.DirectPosition1D;
import org.geotools.geometry.GeneralDirectPosition;
import org.geotools.referencing.WKT;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.operation.LinearTransform;
import org.geotools.referencing.operation.DefaultMathTransformFactory;
import org.geotools.referencing.operation.matrix.MatrixFactory;
import org.geotools.referencing.operation.matrix.GeneralMatrix;
import org.geotools.referencing.operation.matrix.XMatrix;
import org.junit.*;
import static org.junit.Assert.*;
/**
* Tests various classes of {@link MathTransform}, including {@link ConcatenatedTransform}.
* Actually, there is many {@link ConcatenatedTransform}, each optimized for special cases.
* This test tries to test a wide range of subclasses.
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*/
public final class MathTransformTest {
/**
* Random numbers generator.
*/
private Random random;
/**
* The default math transform factory.
*/
private DefaultMathTransformFactory factory;
/**
* Expected accuracy for tests on JTS objects.
*/
private static final double ACCURACY = 0.03;
/**
* Set up common objects used for all tests.
*/
@Before
public void setUp() {
random = new Random(-3531834320875149028L);
factory = new DefaultMathTransformFactory();
}
/**
* Tests a transformation on a {@link DirectPosition} object.
*/
@Test
public void testDirectPositionTransform() throws FactoryException, TransformException {
CoordinateReferenceSystem crs = ReferencingFactoryFinder.getCRSFactory(null).createFromWKT(WKT.UTM_10N);
MathTransform t = ReferencingFactoryFinder.getCoordinateOperationFactory(null).
createOperation(DefaultGeographicCRS.WGS84, crs).getMathTransform();
DirectPosition position = new GeneralDirectPosition(-123, 55);
position = t. transform(position, position);
position = t.inverse().transform(position, position);
assertEquals(-123, position.getOrdinate(0), 1E-6);
assertEquals( 55, position.getOrdinate(1), 1E-6);
}
/**
* Tests the {@link ProjectiveTransform} implementation.
*/
@Test
public void testAffineTransform() throws FactoryException, TransformException {
for (int pass=0; pass<10; pass++) {
final AffineTransform transform = new AffineTransform();
transform.rotate(Math.PI*random.nextDouble(), 100*random.nextDouble(), 100*random.nextDouble());
transform.scale (2*random.nextDouble(), 2*random.nextDouble());
transform.shear (2*random.nextDouble(), 2*random.nextDouble());
transform.translate (100*random.nextDouble(), 100*random.nextDouble());
compareTransforms("AffineTransform", new MathTransform[] {
new ProjectiveTransform(new GeneralMatrix(transform)),
new AffineTransform2D(transform)
});
}
AffineTransform at = new AffineTransform(23.157082917424454, 0.0, 3220.1613428464952,
0.0, -23.157082917424457, 1394.4593259871676);
MathTransform mt = factory.createAffineTransform(new GeneralMatrix(at));
final double[] points = new double[] {
-129.992589135802, 55.9226692948365, -129.987254340541,
55.9249676996729, -129.982715772093, 55.9308988434656,
-129.989772198265, 55.9289277997662, -129.992589135802, 55.9226692948365
};
final double[] transformedPoints = new double[points.length];
mt.transform(points, 0, transformedPoints, 0, points.length/2);
at.transform(points, 0, points, 0, points.length/2);
for (int i=0; i<transformedPoints.length; i++) {
assertEquals(points[i], transformedPoints[i], ACCURACY);
}
}
/**
* Test various linear transformations. We test for many differents dimensions.
* The factory class should have created specialized classes for 1D and 2D cases.
* This test is used in order to ensure that specialized case produces the same
* results than general cases.
*/
@Test
public void testSubAffineTransform() throws FactoryException, TransformException {
for (int pass=0; pass<5; pass++) {
/*
* Construct the reference matrix.
*/
final int dimension = 10;
final GeneralMatrix matrix = new GeneralMatrix(dimension+1, dimension+1);
for (int i=0; i<dimension; i++) {
matrix.setElement(i,i, 400*Math.random()-200);
matrix.setElement(i,dimension, 400*Math.random()-200);
}
assertTrue(matrix.isAffine());
/*
* Construct all math transforms.
*/
final MathTransform[] transforms = new MathTransform[dimension];
for (int i=1; i<=dimension; i++) {
final GeneralMatrix sub = new GeneralMatrix(i+1, i+1);
matrix.copySubMatrix(0, 0, i, i, 0, 0, sub); // Scale terms
matrix.copySubMatrix(0, dimension, i, 1, 0, i, sub); // Translation terms
final MathTransform transform = transforms[i-1] = factory.createAffineTransform(sub);
assertTrue (sub.isAffine());
assertEquals(sub, new GeneralMatrix(((LinearTransform) transform).getMatrix()));
assertInterfaced(transform);
assertTrue(i == transform.getSourceDimensions());
}
/*
* Check transformations and the inverse transformations.
*/
assertTrue("MathTransform1D", transforms[0] instanceof MathTransform1D);
assertTrue("MathTransform2D", transforms[1] instanceof MathTransform2D);
assertEquals(matrix, ((LinearTransform) transforms[dimension-1]).getMatrix());
compareTransforms("SubAffineTransform", transforms);
for (int i=0; i<transforms.length; i++) {
transforms[i] = transforms[i].inverse();
}
compareTransforms("SubAffineTransform.inverse", transforms);
}
}
/**
* Test the concatenation of linear transformations. Concatenation of linear
* transformations should involve only matrix multiplication. However, this
* test will also create {@link ConcatenatedTransform} objects in order to
* compare their results.
*/
@Test
public void testAffineTransformConcatenation() throws FactoryException, TransformException {
final MathTransform[] transforms = new MathTransform[2];
final int numDim = 4;
final int numPts = 200;
for (int pass=0; pass<100; pass++) {
final int dimSource = random.nextInt(numDim)+1;
final int dimTarget = random.nextInt(numDim)+1;
final int dimInterm = random.nextInt(numDim)+1;
final Matrix matrix1 = getRandomMatrix(dimSource, dimInterm);
final Matrix matrix2 = getRandomMatrix(dimInterm, dimTarget);
final MathTransform tr1 = factory.createAffineTransform(matrix1);
final MathTransform tr2 = factory.createAffineTransform(matrix2);
final double[] sourcePt = new double[dimSource * numPts];
final double[] intermPt = new double[dimInterm * numPts];
final double[] targetPt = new double[dimTarget * numPts];
final double[] compare = new double[dimTarget * numPts];
final double[] delta = new double[dimTarget];
for (int i=0; i<numPts; i++) {
sourcePt[i] = 100*random.nextDouble()-50;
}
tr1.transform(sourcePt, 0, intermPt, 0, numPts);
tr2.transform(intermPt, 0, targetPt, 0, numPts);
Arrays.fill(delta, 1E-6);
/*
* Create two set of concatenated transform: the first one computed from matrix
* multiplication; the second one is forced to a ConcatenatedTransform object
* for testing purpose.
*/
transforms[0] = factory.createConcatenatedTransform (tr1, tr2);
transforms[1] = ConcatenatedTransform.createConcatenatedTransform(tr1, tr2);
assertTrue (transforms[0] instanceof LinearTransform);
assertFalse(transforms[1] instanceof LinearTransform);
for (int i=0; i<transforms.length; i++) {
final MathTransform transform = transforms[i];
assertInterfaced(transform);
assertEquals("dimSource["+i+']', dimSource, transform.getSourceDimensions());
assertEquals("dimTarget["+i+']', dimTarget, transform.getTargetDimensions());
transform.transform(sourcePt, 0, compare, 0, numPts);
String name = "transform["+i+"]("+dimSource+" -> "+dimInterm+" -> "+dimTarget+')';
assertPointsEqual(name, targetPt, compare, delta);
}
}
}
/**
* Make sure that linear transformation preserve NaN values.
* This is required for {@link org.geotools.coverage.Category}.
*/
@Test
public void testNaN() throws FactoryException, TransformException {
final XMatrix matrix = MatrixFactory.create(2);
matrix.setElement(0,0,0);
for (int i=0; i<200; i++) {
final int rawBits = 0x7FC00000 + random.nextInt(100);
final float value = Float.intBitsToFloat(rawBits);
assertTrue("isNaN", Float.isNaN(value));
matrix.setElement(0,1, value);
final MathTransform1D tr = (MathTransform1D) factory.createAffineTransform(matrix);
assertTrue("ConstantTransform1D", tr instanceof ConstantTransform1D);
final float compare = (float) tr.transform(0);
assertEquals("rawBits", rawBits, Float.floatToRawIntBits(compare));
}
}
/**
* Test the {@link ExponentialTransform1D} and {@link LogarithmicTransform1D} classes
* using simple know values.
*/
@Test
public void testLogarithmicTransform() throws FactoryException, TransformException {
final double[] POWER_2 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
final double[] VALUE_2 = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192};
final double[] POWER_10 = { -5, -4, -3, -2, -1, 0, +1, +2, +3, +4, +5};
final double[] VALUE_10 = {1E-5, 1E-4, 1E-3, 1E-2, 1E-1, 1, 1E+1, 1E+2, 1E+3, 1E+4, 1E+5};
compareTransform1D("Exponential", 2, POWER_2, VALUE_2);
compareTransform1D("Exponential", 10, POWER_10, VALUE_10);
compareTransform1D("Logarithmic", 2, VALUE_2, POWER_2);
compareTransform1D("Logarithmic", 10, VALUE_10, POWER_10);
}
/**
* Test the concatenation of {@link LinearTransform1D}, {@link ExponentialTransform1D}
* and {@link LogarithmicTransform1D}.
*/
@Test
public void testLogarithmicAndExponentialConcatenation() throws FactoryException, TransformException {
final int numPts = 200;
final double[] sourcePt = new double[numPts];
final double[] targetPt = new double[numPts];
final double[] compare = new double[numPts];
final double[] delta = new double[numPts];
for (int pass=0; pass<100; pass++) {
for (int i=0; i<numPts; i++) {
sourcePt[i] = 20*random.nextDouble()+0.1;
}
MathTransform ctr = getRandomTransform1D();
ctr.transform(sourcePt, 0, targetPt, 0, numPts);
for (int i=random.nextInt(2)+1; --i>=0;) {
final MathTransform1D step = getRandomTransform1D();
ctr = (MathTransform1D) factory.createConcatenatedTransform(ctr, step);
step.transform(targetPt, 0, targetPt, 0, numPts);
}
ctr.transform(sourcePt, 0, compare, 0, numPts);
final double EPS = Math.pow(10, -5+countNonlinear(ctr));
for (int i=0; i<numPts; i++) {
delta[i] = Math.max(1E-9, Math.abs(targetPt[i]*EPS));
if (targetPt[i] >= +1E+300) targetPt[i] = Double.POSITIVE_INFINITY;
if (targetPt[i] <= -1E+300) targetPt[i] = Double.NEGATIVE_INFINITY;
}
assertPointsEqual("transform["+ctr+']', targetPt, compare, delta);
/*
* Test the inverse transform. It is difficult to get back the exact original value,
* since expressions like 'pow(b1, pow(b2,x))' tend to overflow or underflow very
* fast. We are very tolerant for this test because of this (exponential expression
* give exponential error). The 'testLogarithmicTransform' method tested the inverse
* transform in a more sever way.
*/
try {
final MathTransform inv = ctr.inverse();
Arrays.fill(delta, Math.pow(10, countNonlinear(inv)));
inv.transform(targetPt, 0, compare, 0, numPts);
for (int i=0; i<numPts; i++) {
if (!isReal(targetPt[i]) || !isReal(compare[i])) {
// Ignore all input points that produced NaN or infinity
// A succession of 2 "Exponentional" operation produces
// infinities pretty fast, so ignore it.
sourcePt[i] = Double.NaN;
}
}
assertPointsEqual("inverse["+inv+']', sourcePt, compare, delta);
} catch (NoninvertibleTransformException exception) {
// Some transforms may not be invertible. Ignore...
}
}
}
///////////////////////////////////////////////////////////////////////////////////
//////////// ////////////
//////////// U T I L I T Y M E T H O D S ////////////
//////////// ////////////
///////////////////////////////////////////////////////////////////////////////////
/**
* Returns a random matrix.
*
* @param dimSource Number of dimension for input points.
* @param dimTarget Number of dimension for outout points.
*/
private Matrix getRandomMatrix(final int dimSource, final int dimTarget) {
final XMatrix matrix = MatrixFactory.create(dimTarget+1, dimSource+1);
for (int j=0; j<dimTarget; j++) { // Don't touch to the last row!
for (int i=0; i<=dimSource; i++) {
matrix.setElement(j,i, 10*random.nextDouble()-5);
}
if (j <= dimSource) {
matrix.setElement(j,j, 40*random.nextDouble()+10);
}
matrix.setElement(j,dimSource, 80*random.nextDouble()-40);
}
if (dimSource == dimTarget) {
assertTrue("Affine", matrix.isAffine());
}
return matrix;
}
/**
* Gets a random one-dimensional transform.
*/
private MathTransform1D getRandomTransform1D() throws FactoryException {
final String[] candidates = {
"Logarithmic",
"Exponential",
"Affine"
};
final String classification = candidates[random.nextInt(candidates.length)];
final ParameterValueGroup parameters = factory.getDefaultParameters(classification);
if (classification.equalsIgnoreCase("Affine")) {
parameters.parameter("num_row").setValue(2);
parameters.parameter("num_col").setValue(2);
parameters.parameter("elt_0_0").setValue(random.nextDouble()*2+0.1); // scale
parameters.parameter("elt_0_1").setValue(random.nextDouble()*1 - 2); // offset
} else {
parameters.parameter("base").setValue(random.nextDouble()*4 + 0.1);
}
return (MathTransform1D) factory.createParameterizedTransform(parameters);
}
/**
* Compare the transformation performed by many math transforms. If some transforms
* don't have the same number of input or output dimension, only the first input or
* output dimensions will be taken in account.
*
* @throws TransformException if a transformation failed.
*/
private void compareTransforms(final String name, final MathTransform[] transforms)
throws TransformException
{
/*
* Initialisation...
*/
final GeneralDirectPosition[] sources = new GeneralDirectPosition[transforms.length];
final GeneralDirectPosition[] targets = new GeneralDirectPosition[transforms.length];
int maxDimSource = 0;
int maxDimTarget = 0;
for (int i=0; i<transforms.length; i++) {
final int dimSource = transforms[i].getSourceDimensions();
final int dimTarget = transforms[i].getTargetDimensions();
if (dimSource > maxDimSource) maxDimSource = dimSource;
if (dimTarget > maxDimTarget) maxDimTarget = dimTarget;
sources[i] = new GeneralDirectPosition(dimSource);
targets[i] = new GeneralDirectPosition(dimTarget);
}
/*
* Test with an arbitrary number of randoms points.
*/
for (int pass=0; pass<200; pass++) {
for (int j=0; j<maxDimSource; j++) {
final double ord = 100*random.nextDouble();
for (int i=0; i<sources.length; i++) {
final GeneralDirectPosition source = sources[i];
if (j < source.ordinates.length) {
source.ordinates[j] = ord;
}
}
}
for (int j=0; j<transforms.length; j++) {
assertSame(transforms[j].transform(sources[j], targets[j]), targets[j]);
}
/*
* Compare all target points.
*/
final StringBuilder buffer = new StringBuilder(name);
buffer.append(": Compare transform[");
final int lengthJ = buffer.length();
for (int j=0; j<targets.length; j++) {
buffer.setLength(lengthJ);
buffer.append(j).append("] with [");
final int lengthI = buffer.length();
final GeneralDirectPosition targetJ = targets[j];
for (int i=j+1; i<targets.length; i++) {
buffer.setLength(lengthI);
buffer.append(i).append(']');
final String label = buffer.toString();
final GeneralDirectPosition targetI = targets[i];
assertTrue(targetJ.ordinates != targetI.ordinates);
for (int k=Math.min(targetJ.ordinates.length, targetI.ordinates.length); --k>=0;) {
assertEquals(label, targetJ.ordinates[k], targetI.ordinates[k], 1E-6);
}
}
}
}
}
/**
* Compare the result of "Logarithmic" or "Exponential" transform with the expected results.
*
* @param classification The identifier name (e.g. "Exponential" or "Logarithmic").
* @param base The value for the "base" parameter.
* @param input Array of input values.
* @param expected Array of expected output values.
*/
private void compareTransform1D(final String classification,
final double base,
final double[] input,
final double[] expected)
throws FactoryException, TransformException
{
assertEquals(input.length, expected.length);
final ParameterValueGroup parameters = factory.getDefaultParameters(classification);
parameters.parameter("base").setValue(base);
final MathTransform1D direct =
(MathTransform1D) factory.createParameterizedTransform(parameters);
final MathTransform1D inverse = direct.inverse();
final DirectPosition1D point = new DirectPosition1D();
for (int i=0; i<expected.length; i++) {
final double x = input[i];
final double y = direct.transform(x);
assertEquals("transform[x="+x+']', expected[i], y, 1E-6);
assertEquals("inverse [y="+y+']', x, inverse.transform(y), 1E-6);
point.setOrdinate(0, x);
assertSame(direct.transform(point, point), point);
assertEquals(y, point.getOrdinate(0), 1E-9);
}
}
/**
* Count the number of non-linear steps in a {@link MathTransform}.
*/
private static int countNonlinear(final MathTransform transform) {
if ((transform instanceof ExponentialTransform1D) ||
(transform instanceof LogarithmicTransform1D))
{
return 1;
}
if (transform instanceof ConcatenatedTransform) {
final ConcatenatedTransform ct = (ConcatenatedTransform) transform;
return countNonlinear(ct.transform1) +
countNonlinear(ct.transform2);
}
return 0;
}
///////////////////////////////////////////////////////////////////////////////////
//////////// ////////////
//////////// A S S E R T I O N M E T H O D S ////////////
//////////// ////////////
///////////////////////////////////////////////////////////////////////////////////
/**
* Returns {@code true} if the specified number is real (neither NaN or infinite).
*/
private static boolean isReal(final double value) {
return !Double.isNaN(value) && !Double.isInfinite(value);
}
/**
* Verify that the specified transform implements {@link MathTransform1D}
* or {@link MathTransform2D} as needed.
*
* @param transform The transform to test.
*/
private static void assertInterfaced(final MathTransform transform) {
if (transform instanceof LinearTransform) {
final Matrix matrix = ((LinearTransform) transform).getMatrix();
if (!((XMatrix) matrix).isAffine()) {
// Special case: Non-affine transforms not yet declared as a 1D or 2D transform.
return;
}
}
int dim = transform.getSourceDimensions();
if (transform.getTargetDimensions() != dim) {
dim = 0;
}
assertTrue("MathTransform1D", (dim==1) == (transform instanceof MathTransform1D));
assertTrue("MathTransform2D", (dim==2) == (transform instanceof MathTransform2D));
}
/**
* Compare two arrays of points.
*
* @param name The name of the comparaison to be performed.
* @param expected The expected array of points.
* @param actual The actual array of points.
* @param delta The maximal difference tolerated in comparaisons for each dimension.
* This array length must be equal to coordinate dimension (usually 1, 2 or 3).
*/
private static void assertPointsEqual(final String name,
final double[] expected,
final double[] actual,
final double[] delta)
{
final int dimension = delta.length;
final int stop = Math.min(expected.length, actual.length)/dimension * dimension;
assertEquals("Array length for expected points", stop, expected.length);
assertEquals("Array length for actual points", stop, actual.length);
final StringBuffer buffer = new StringBuffer(name);
buffer.append(": point[");
final int start = buffer.length();
for (int i=0; i<stop; i++) {
buffer.setLength(start);
buffer.append(i/dimension);
buffer.append(", dimension ");
buffer.append(i % dimension);
buffer.append(" of ");
buffer.append(dimension);
buffer.append(']');
if (isReal(expected[i])) {
// The "two steps" method in ConcatenatedTransformTest sometime produces
// random NaN numbers. This "two steps" is used only for comparaison purpose;
// the "real" (tested) method work better.
assertEquals(buffer.toString(), expected[i], actual[i], delta[i % dimension]);
}
}
}
}