001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    http://www.geoapi.org
004 *
005 *    Copyright (C) 2011-2019 Open Geospatial Consortium, Inc.
006 *    All Rights Reserved. http://www.opengeospatial.org/ogc/legal
007 *
008 *    Permission to use, copy, and modify this software and its documentation, with
009 *    or without modification, for any purpose and without fee or royalty is hereby
010 *    granted, provided that you include the following on ALL copies of the software
011 *    and documentation or portions thereof, including modifications, that you make:
012 *
013 *    1. The full text of this NOTICE in a location viewable to users of the
014 *       redistributed or derivative work.
015 *    2. Notice of any changes or modifications to the OGC files, including the
016 *       date changes were made.
017 *
018 *    THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE
019 *    NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
020 *    TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT
021 *    THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY
022 *    PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
023 *
024 *    COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR
025 *    CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION.
026 *
027 *    The name and trademarks of copyright holders may NOT be used in advertising or
028 *    publicity pertaining to the software without specific, written prior permission.
029 *    Title to copyright in this software and any associated documentation will at all
030 *    times remain with copyright holders.
031 */
032package org.opengis.test.referencing;
033
034import java.util.List;
035import java.util.Arrays;
036import java.util.Random;
037import java.awt.geom.AffineTransform;
038
039import org.opengis.util.Factory;
040import org.opengis.util.FactoryException;
041import org.opengis.referencing.operation.Matrix;
042import org.opengis.referencing.operation.MathTransform;
043import org.opengis.referencing.operation.TransformException;
044import org.opengis.referencing.operation.MathTransformFactory;
045import org.opengis.test.Configuration;
046
047
048import org.junit.Test;
049import org.junit.runner.RunWith;
050import org.junit.runners.Parameterized;
051
052import static java.lang.StrictMath.*;
053import static org.junit.Assume.*;
054import static org.junit.Assert.*;
055import static org.opengis.test.referencing.PseudoEpsgFactory.FEET;
056
057
058/**
059 * Tests {@linkplain MathTransformFactory#createAffineTransform(Matrix) affine transforms}
060 * from the {@code org.opengis.referencing.operation} package. Math transform instances are
061 * created using the factory given at construction time.
062 *
063 * <div class="note"><b>Usage example:</b>
064 * in order to specify their factories and run the tests in a JUnit framework, implementors can
065 * define a subclass in their own test suite as in the example below:
066 *
067 * <blockquote><pre>import org.junit.runner.RunWith;
068 *import org.junit.runners.JUnit4;
069 *import org.opengis.test.referencing.AffineTransformTest;
070 *
071 *&#64;RunWith(JUnit4.class)
072 *public class MyTest extends AffineTransformTest {
073 *    public MyTest() {
074 *        super(new MyMathTransformFactory());
075 *    }
076 *}</pre></blockquote>
077 * </div>
078 *
079 * @see ParameterizedTransformTest
080 * @see org.opengis.test.TestSuite
081 *
082 * @author  Martin Desruisseaux (Geomatys)
083 * @version 3.1
084 * @since   3.1
085 */
086@RunWith(Parameterized.class)
087public strictfp class AffineTransformTest extends TransformTestCase {
088    /**
089     * The default tolerance threshold for comparing the results of direct transforms.
090     * Because affine transform are linear, only rounding errors should exist.
091     */
092    private static final double TRANSFORM_TOLERANCE = 1E-8;
093
094    /**
095     * The delta value to use for computing an approximation of the derivative by finite
096     * difference, in metres. Because affine transform are linear, this value should
097     * actually have no impact on the result.
098     */
099    private static final double DERIVATIVE_DELTA = 1;
100
101    /**
102     * Approximate number of points to transform in each test.
103     */
104    private static final int NUM_POINTS = 2500;
105
106    /**
107     * The factory for creating {@link MathTransform} objects, or {@code null} if none.
108     */
109    protected final MathTransformFactory mtFactory;
110
111    /**
112     * The matrix of the math transform being tested. This field is set, together with the
113     * {@link #transform transform} field, after the execution of every {@code testFoo()}
114     * method in this class.
115     *
116     * <p>If this field is non-null before a test is run, then those parameters will be used
117     * directly. This allow implementors to alter the parameters before to run the test one
118     * more time.</p>
119     */
120    protected Matrix matrix;
121
122    /**
123     * {@code true} if {@link MathTransformFactory#createAffineTransform(Matrix)} accepts
124     * non-square matrixes. Transforms defined by non-square matrixes have a number of
125     * input dimensions different than the number of output dimensions.
126     */
127    protected boolean isNonSquareMatrixSupported;
128
129    /**
130     * {@code true} if {@link MathTransformFactory#createAffineTransform(Matrix)} accepts
131     * matrixes of size different than 3×3. If {@code false}, then only matrixes of
132     * size 3×3 (i.e. affine transforms between two-dimensional spaces) will be tested.
133     */
134    protected boolean isNonBidimensionalSpaceSupported;
135
136    /**
137     * Returns a default set of factories to use for running the tests. Those factories are given
138     * in arguments to the constructor when this test class is instantiated directly by JUnit (for
139     * example as a {@linkplain org.junit.runners.Suite.SuiteClasses suite} element), instead than
140     * sub-classed by the implementor. The factories are fetched as documented in the
141     * {@link #factories(Class[])} javadoc.
142     *
143     * @return the default set of arguments to be given to the {@code AffineTransformTest} constructor.
144     */
145    @Parameterized.Parameters
146    @SuppressWarnings("unchecked")
147    public static List<Factory[]> factories() {
148        return factories(MathTransformFactory.class);
149    }
150
151    /**
152     * Creates a new test using the given factory. If the given factory is {@code null},
153     * then the tests will be skipped.
154     *
155     * @param factory  factory for creating {@link MathTransform} instances.
156     */
157    public AffineTransformTest(final MathTransformFactory factory) {
158        super(factory);
159        mtFactory = factory;
160        @SuppressWarnings("unchecked")
161        final boolean[] isEnabled = getEnabledFlags(
162                Configuration.Key.isNonSquareMatrixSupported,
163                Configuration.Key.isNonBidimensionalSpaceSupported);
164        isNonSquareMatrixSupported       = isEnabled[0];
165        isNonBidimensionalSpaceSupported = isEnabled[1];
166    }
167
168    /**
169     * Returns information about the configuration of the test which has been run.
170     * This method returns a map containing:
171     *
172     * <ul>
173     *   <li>All the entries defined in the {@linkplain TransformTestCase#configuration() parent class}.</li>
174     *   <li>All the following values associated to the {@link org.opengis.test.Configuration.Key} of the same name:
175     *     <ul>
176     *       <li>{@link #isNonSquareMatrixSupported}</li>
177     *       <li>{@link #isNonBidimensionalSpaceSupported}</li>
178     *       <li>{@link #mtFactory}</li>
179     *     </ul>
180     *   </li>
181     * </ul>
182     *
183     * @return {@inheritDoc}
184     */
185    @Override
186    public Configuration configuration() {
187        final Configuration op = super.configuration();
188        assertNull(op.put(Configuration.Key.isNonSquareMatrixSupported,       isNonSquareMatrixSupported));
189        assertNull(op.put(Configuration.Key.isNonBidimensionalSpaceSupported, isNonBidimensionalSpaceSupported));
190        assertNull(op.put(Configuration.Key.mtFactory,                        mtFactory));
191        return op;
192    }
193
194    /**
195     * Runs a test using the given Java2D affine transform as a reference.
196     *
197     * @param reference  the affine transform to use as a reference for checking the results.
198     */
199    private void runTest(final AffineTransform reference) throws FactoryException, TransformException {
200        assumeNotNull(mtFactory);
201        if (matrix == null) {
202            matrix = new SimpleMatrix(3, 3,
203                    reference.getScaleX(), reference.getShearX(), reference.getTranslateX(),
204                    reference.getShearY(), reference.getScaleY(), reference.getTranslateY(),
205                                 0,              0,                  1);
206        }
207        if (transform == null) {
208            transform = mtFactory.createAffineTransform(matrix);
209            assertNotNull(transform);
210        }
211        final float[] coordinates = verifyInternalConsistency(reference.hashCode());
212        /*
213         * At this point, we have performed internal consistency check of the
214         * implementor transform. Now compute the expected values using the
215         * Java2D transform and compare with the implementor transform.
216         */
217        final double[] source = new double[coordinates.length];
218        final double[] target = new double[coordinates.length];
219        for (int i=0; i<coordinates.length; i++) {
220            source[i] = coordinates[i];
221        }
222        reference.transform(source, 0, target, 0, coordinates.length/2);
223        verifyTransform(source, target);
224        for (int i=0; i<coordinates.length; i++) {
225            assertEquals("Source array should be unmodified.", coordinates[i], source[i], 0.0);
226        }
227    }
228
229    /**
230     * Runs a test using the given matrix. This method checks only for internal consistency,
231     * i.e. we don't have an external implementation that we can use for comparing the results.
232     */
233    private void runTest(final int numRow, final int numCol, final double... elements)
234            throws FactoryException, TransformException
235    {
236        assumeNotNull(mtFactory);
237        if (matrix == null) {
238            matrix = new SimpleMatrix(numRow, numCol, elements);
239        }
240        if (transform == null) {
241            transform = mtFactory.createAffineTransform(matrix);
242            assertNotNull(transform);
243        }
244        verifyInternalConsistency(Arrays.hashCode(elements));
245    }
246
247    /**
248     * Tests the transform consistency using many random points inside an arbitrary domain.
249     *
250     * @param  seed  the random seed. We recommend a constant value for each transform or CRS to be tested.
251     * @return the generated random coordinates inside the arbitrary domain.
252     * @throws TransformException if a point can not be transformed.
253     */
254    private float[] verifyInternalConsistency(final long seed) throws TransformException {
255        validators.validate(transform);
256        if (!(tolerance >= TRANSFORM_TOLERANCE)) { // !(a>=b) instead than (a<b) in order to catch NaN.
257            tolerance = TRANSFORM_TOLERANCE;
258        }
259        final int dimension = transform.getSourceDimensions();
260        final int[]    num = new int   [dimension];
261        final double[] min = new double[dimension];
262        final double[] max = new double[dimension];
263        derivativeDeltas   = new double[dimension];
264        Arrays.fill(num, (int) ceil(pow(NUM_POINTS, 1.0/num.length)));
265        Arrays.fill(min, -1000);
266        Arrays.fill(max, +1000);
267        Arrays.fill(derivativeDeltas, DERIVATIVE_DELTA);
268        return verifyInDomain(min, max, num, new Random(seed));
269    }
270
271    /**
272     * Tests using an identity transform in an one-dimensional space.
273     * This test is executed only if the {@link #isNonBidimensionalSpaceSupported}
274     * flag is set to {@code true}.
275     *
276     * @throws FactoryException should never happen.
277     * @throws TransformException should never happen.
278     */
279    @Test
280    public void testIdentity1D() throws FactoryException, TransformException {
281        assumeTrue(isNonBidimensionalSpaceSupported);
282        configurationTip = Configuration.Key.isNonBidimensionalSpaceSupported;
283        runTest(2, 2,
284            1, 0,
285            0, 1);
286        assertTrue("MathTransform.isIdentity().", transform.isIdentity());
287    }
288
289    /**
290     * Tests using an identity transform in a two-dimensional space.
291     *
292     * @throws FactoryException should never happen.
293     * @throws TransformException should never happen.
294     */
295    @Test
296    public void testIdentity2D() throws FactoryException, TransformException {
297        runTest(new AffineTransform());
298        assertTrue("MathTransform.isIdentity().", transform.isIdentity());
299    }
300
301    /**
302     * Tests using an identity transform in a three-dimensional space.
303     * This test is executed only if the {@link #isNonBidimensionalSpaceSupported}
304     * flag is set to {@code true}.
305     *
306     * @throws FactoryException should never happen.
307     * @throws TransformException should never happen.
308     */
309    @Test
310    public void testIdentity3D() throws FactoryException, TransformException {
311        assumeTrue(isNonBidimensionalSpaceSupported);
312        configurationTip = Configuration.Key.isNonBidimensionalSpaceSupported;
313        runTest(4, 4,
314            1, 0, 0, 0,
315            0, 1, 0, 0,
316            0, 0, 1, 0,
317            0, 0, 0, 1);
318        assertTrue("MathTransform.isIdentity().", transform.isIdentity());
319    }
320
321    /**
322     * Tests a transform swapping the axes in a two-dimensional space.
323     *
324     * @throws FactoryException should never happen.
325     * @throws TransformException should never happen.
326     */
327    @Test
328    public void testAxisSwapping2D() throws FactoryException, TransformException {
329        runTest(new AffineTransform(0, 1, 1, 0, 0, 0));
330        assertFalse("MathTransform.isIdentity().", transform.isIdentity());
331    }
332
333    /**
334     * Tests using a 180° rotation in a two-dimensional space.
335     *
336     * @throws FactoryException should never happen.
337     * @throws TransformException should never happen.
338     */
339    @Test
340    public void testSouthOrientated2D() throws FactoryException, TransformException {
341        runTest(AffineTransform.getQuadrantRotateInstance(2));
342        assertFalse("MathTransform.isIdentity().", transform.isIdentity());
343    }
344
345    /**
346     * Tests using a translation of (400000,-100000) metres in a two-dimensional space.
347     * This translation is the (<cite>False easting</cite>, <cite>False northing</cite>)
348     * parameter values of the <cite>OSGB 1936 / British National Grid</cite> projection.
349     *
350     * @throws FactoryException should never happen.
351     * @throws TransformException should never happen.
352     */
353    @Test
354    public void testTranslatation2D() throws FactoryException, TransformException {
355        runTest(AffineTransform.getTranslateInstance(400000, -100000));
356        assertFalse("MathTransform.isIdentity().", transform.isIdentity());
357    }
358
359    /**
360     * Tests using a uniform scale factor of 0.3048 in a two-dimensional space.
361     * This is the conversion factor from <cite>feet</cite> to <cite>metres</cite>.
362     *
363     * @throws FactoryException should never happen.
364     * @throws TransformException should never happen.
365     */
366    @Test
367    public void testUniformScale2D() throws FactoryException, TransformException {
368        runTest(AffineTransform.getScaleInstance(FEET, FEET));
369        assertFalse("MathTransform.isIdentity().", transform.isIdentity());
370    }
371
372    /**
373     * Tests using a non-uniform scale factor of (3,4) in a two-dimensional space.
374     *
375     * @throws FactoryException should never happen.
376     * @throws TransformException should never happen.
377     */
378    @Test
379    public void testGenericScale2D() throws FactoryException, TransformException {
380        runTest(AffineTransform.getScaleInstance(3, 4));
381        assertFalse("MathTransform.isIdentity().", transform.isIdentity());
382    }
383
384    /**
385     * Tests using an anticlockwise 30° rotation in a two-dimensional space.
386     *
387     * @throws FactoryException should never happen.
388     * @throws TransformException should never happen.
389     */
390    @Test
391    public void testRotation2D() throws FactoryException, TransformException {
392        runTest(AffineTransform.getRotateInstance(toRadians(30)));
393        assertFalse("MathTransform.isIdentity().", transform.isIdentity());
394    }
395
396    /**
397     * Tests using a combination of scale, rotation and translation in a two-dimensional space.
398     *
399     * @throws FactoryException should never happen.
400     * @throws TransformException should never happen.
401     */
402    @Test
403    public void testGeneral() throws FactoryException, TransformException {
404        final AffineTransform reference = AffineTransform.getTranslateInstance(10,-20);
405        reference.rotate(0.5);
406        reference.scale(0.2, 0.3);
407        reference.translate(300, 500);
408        runTest(reference);
409        assertFalse("MathTransform.isIdentity().", transform.isIdentity());
410    }
411
412    /**
413     * Tests a transform which reduce the number of dimensions from 4 to 2.
414     * This test is executed only if the {@link #isNonSquareMatrixSupported}
415     * flag is set to {@code true}.
416     *
417     * @throws FactoryException should never happen.
418     * @throws TransformException should never happen.
419     */
420    @Test
421    public void testDimensionReduction() throws FactoryException, TransformException {
422        assumeTrue(isNonSquareMatrixSupported);
423        configurationTip = Configuration.Key.isNonSquareMatrixSupported;
424        final int sourceDim = 4;
425        final int targetDim = 2;
426        final boolean inverseSupported = isInverseTransformSupported;
427        isInverseTransformSupported = false;
428        try {
429            runTest(targetDim+1, sourceDim+1,
430                2,  0,  0,  0,  8,
431                0,  0,  4,  0,  5,
432                0,  0,  0,  0,  1);
433            /*
434             * Tests hard-coded known points.
435             */
436            final double[] source = new double[] {0,0,0,0 , 1,1,1,1 , 8,3,-7,5};
437            final double[] target = new double[] {8,5 ,     10,9 ,    24,-23};
438            verifyTransform(source, target);
439            /*
440             * Inverse the transform (this is the interesting part of this test) and try again.
441             * The ordinates at index 1 and 3 (they are the index of columns were all elements
442             * are 0 in the above matrix) are expected to be NaN.
443             */
444            if (inverseSupported) {
445                configurationTip = Configuration.Key.isInverseTransformSupported;
446                for (int i=0; i<source.length; i += sourceDim) {
447                    source[i + 1] = Double.NaN;
448                    source[i + 3] = Double.NaN;
449                }
450                final MathTransform direct = transform;
451                transform = direct.inverse();
452                try {
453                    verifyTransform(target, source);
454                } finally {
455                    transform = direct;
456                }
457            }
458        } finally {
459            isInverseTransformSupported = inverseSupported;
460        }
461        assertFalse("MathTransform.isIdentity().", transform.isIdentity());
462    }
463}