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 *@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}