| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.referencing.operation.transform; |
| |
| import java.util.Random; |
| import java.util.Iterator; |
| import org.opengis.util.FactoryException; |
| import org.opengis.geometry.DirectPosition; |
| import org.opengis.referencing.operation.Matrix; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.MathTransformFactory; |
| import org.opengis.referencing.operation.TransformException; |
| import org.apache.sis.internal.system.DefaultFactories; |
| import org.apache.sis.referencing.datum.HardCodedDatum; |
| import org.apache.sis.referencing.operation.matrix.Matrix2; |
| import org.apache.sis.referencing.operation.matrix.Matrix3; |
| import org.apache.sis.referencing.operation.matrix.Matrix4; |
| import org.apache.sis.referencing.operation.matrix.Matrices; |
| import org.apache.sis.geometry.GeneralDirectPosition; |
| import org.apache.sis.test.TestUtilities; |
| import org.apache.sis.test.DependsOnMethod; |
| import org.apache.sis.test.DependsOn; |
| import org.apache.sis.test.TestCase; |
| import org.junit.Test; |
| |
| import static java.lang.Double.NaN; |
| import static org.opengis.test.Assert.*; |
| |
| |
| /** |
| * Tests {@link TransformSeparator}. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.0 |
| * @since 0.7 |
| * @module |
| */ |
| @DependsOn({ |
| PassThroughTransformTest.class, |
| ConcatenatedTransformTest.class |
| }) |
| public final strictfp class TransformSeparatorTest extends TestCase { |
| /** |
| * Verifies the argument checks performed by the {@code add} methods. |
| */ |
| @Test |
| public void testArgumentChecks() { |
| final TransformSeparator s = new TransformSeparator(MathTransforms.identity(8)); |
| try { |
| s.getSourceDimensions(); |
| fail("Shall not return unspecified dimensions."); |
| } catch (IllegalStateException e) { |
| // This is the expected exception. |
| } |
| try { |
| s.getTargetDimensions(); |
| fail("Shall not return unspecified dimensions."); |
| } catch (IllegalStateException e) { |
| // This is the expected exception. |
| } |
| s.addSourceDimensionRange(1, 4); |
| s.addTargetDimensions(0, 3, 4); |
| assertArrayEquals("sourceDimensions", new int[] {1, 2, 3}, s.getSourceDimensions()); |
| assertArrayEquals("targetDimensions", new int[] {0, 3, 4}, s.getTargetDimensions()); |
| try { |
| s.addSourceDimensions(3, 4, 5); |
| fail("Shall not accept non-increasing value."); |
| } catch (IllegalArgumentException e) { |
| // This is the expected exception. |
| assertTrue(e.getMessage().contains("dimensions[0]")); |
| } |
| try { |
| s.addTargetDimensionRange(3, 5); |
| fail("Shall not accept non-increasing value."); |
| } catch (IllegalArgumentException e) { |
| // This is the expected exception. |
| assertTrue(e.getMessage().contains("lower")); |
| } |
| s.addSourceDimensions(4, 6); |
| s.addTargetDimensionRange(6, 8); |
| assertArrayEquals("sourceDimensions", new int[] {1, 2, 3, 4, 6}, s.getSourceDimensions()); |
| assertArrayEquals("targetDimensions", new int[] {0, 3, 4, 6, 7}, s.getTargetDimensions()); |
| try { |
| s.addSourceDimensions(8); |
| fail("Shall not accept value out of range."); |
| } catch (IllegalArgumentException e) { |
| // This is the expected exception. |
| assertTrue(e.getMessage().contains("dimensions[0]")); |
| } |
| try { |
| s.addTargetDimensions(3, 8); |
| fail("Shall not accept value out of range."); |
| } catch (IllegalArgumentException e) { |
| // This is the expected exception. |
| assertTrue(e.getMessage().contains("dimensions[0]")); |
| } |
| } |
| |
| /** |
| * Tests separation of a linear transform. |
| * |
| * @throws FactoryException if an error occurred while creating a new transform. |
| */ |
| @Test |
| public void testLinearTransform() throws FactoryException { |
| Matrix matrix = new Matrix4( |
| 2, 0, 0, 7, // Some random values. |
| 0, 5, 0, 6, |
| 1, 0, 3, 8, |
| 0, 0, 0, 1 |
| ); |
| final TransformSeparator s = new TransformSeparator(MathTransforms.linear(matrix)); |
| /* |
| * Trivial case: no dimension specified, we should get the transform unchanged. |
| */ |
| assertSame("transform", s.transform, s.separate()); |
| assertArrayEquals("sourceDimensions", new int[] {0, 1, 2}, s.getSourceDimensions()); |
| assertArrayEquals("targetDimensions", new int[] {0, 1, 2}, s.getTargetDimensions()); |
| /* |
| * Filter only target dimensions. This is the easiest non-trivial case since we just |
| * need to drop some rows. There is no analysis to perform on the matrix values. |
| */ |
| matrix = Matrices.create(3, 4, new double[] { |
| 2, 0, 0, 7, |
| 1, 0, 3, 8, |
| 0, 0, 0, 1 |
| }); |
| s.clear(); |
| s.addTargetDimensions(0, 2); |
| s.addSourceDimensionRange(0, 3); |
| assertMatrixEquals("transform", matrix, ((LinearTransform) s.separate()).getMatrix(), STRICT); |
| assertArrayEquals("sourceDimensions", new int[] {0, 1, 2}, s.getSourceDimensions()); |
| assertArrayEquals("targetDimensions", new int[] {0, 2}, s.getTargetDimensions()); |
| /* |
| * Filter only source dimensions. Do not specify any target dimensions for now. |
| * TransformSeparator needs to examine the matrix values and drop all target dimensions |
| * that depend on an excluded source dimensions. |
| */ |
| matrix = Matrices.create(2, 3, new double[] { |
| 5, 0, 6, |
| 0, 0, 1 |
| }); |
| s.clear(); |
| s.addSourceDimensions(1, 2); |
| assertMatrixEquals("transform", matrix, ((LinearTransform) s.separate()).getMatrix(), STRICT); |
| assertArrayEquals("sourceDimensions", new int[] {1, 2}, s.getSourceDimensions()); |
| assertArrayEquals("targetDimensions", new int[] {1}, s.getTargetDimensions()); |
| /* |
| * Filter both source and target dimensions. Source dimensions 0 and 2 allow the target dimensions 0 and 2 |
| * (target dimension 1 is discarded because it depends on source dimension 1). Then the target dimensions |
| * are filtered for retaining only dimension 0. |
| */ |
| matrix = Matrices.create(2, 3, new double[] { |
| 2, 0, 7, |
| 0, 0, 1 |
| }); |
| s.clear(); |
| s.addSourceDimensions(0, 2); |
| s.addTargetDimensions(0); |
| assertMatrixEquals("transform", matrix, ((LinearTransform) s.separate()).getMatrix(), STRICT); |
| assertArrayEquals("sourceDimensions", new int[] {0, 2}, s.getSourceDimensions()); |
| assertArrayEquals("targetDimensions", new int[] {0}, s.getTargetDimensions()); |
| /* |
| * Try again, but with the addition of a target dimension that TransformSeparator can not keep. |
| * It shall cause an exception to be thrown. |
| */ |
| s.addTargetDimensions(1); |
| try { |
| s.separate(); |
| fail("Should not have been able to separate that transform."); |
| } catch (FactoryException e) { |
| // This is the expected exception. |
| assertNotNull(e.getMessage()); |
| } |
| } |
| |
| /** |
| * Tests separation of a linear transform containing {@link Double#NaN} values. |
| * |
| * @throws FactoryException if an error occurred while creating a new transform. |
| */ |
| @Test |
| public void testIncompleteTransform() throws FactoryException { |
| Matrix matrix = new Matrix4( |
| 1, 0, 0, 7, |
| 0, 0, 1, 8, |
| 0, NaN, 0, 6, |
| 0, 0, 0, 1 |
| ); |
| TransformSeparator s = new TransformSeparator(MathTransforms.linear(matrix)); |
| s.addSourceDimensions(1); |
| assertMatrixEquals("transform", new Matrix2( |
| NaN, 6, |
| 0, 1 |
| ), ((LinearTransform) s.separate()).getMatrix(), STRICT); |
| assertArrayEquals(new int[] {2}, s.getTargetDimensions()); |
| } |
| |
| /** |
| * Tests separation of a concatenated transform. |
| * |
| * @throws FactoryException if an error occurred while creating a new transform. |
| */ |
| @Test |
| @DependsOnMethod("testLinearTransform") |
| public void testConcatenatedTransform() throws FactoryException { |
| final MathTransformFactory factory = DefaultFactories.forBuildin(MathTransformFactory.class); |
| final TransformSeparator s = new TransformSeparator(EllipsoidToCentricTransform.createGeodeticConversion( |
| factory, HardCodedDatum.WGS84.getEllipsoid(), false), factory); |
| |
| s.addSourceDimensions(0, 1); |
| s.addTargetDimensions(0, 1); |
| final Iterator<MathTransform> it = MathTransforms.getSteps(s.separate()).iterator(); |
| assertInstanceOf("normalize", LinearTransform.class, it.next()); |
| assertInstanceOf("transform", EllipsoidToCentricTransform.class, it.next()); |
| assertInstanceOf("denormalize", LinearTransform.class, it.next()); |
| assertFalse(it.hasNext()); |
| } |
| |
| /** |
| * Tests separation of a pass through transform. |
| * |
| * @throws FactoryException if an error occurred while creating a new transform. |
| * @throws TransformException if an error occurred while transforming coordinates for comparison purpose. |
| */ |
| @Test |
| @DependsOnMethod("testLinearTransform") |
| public void testPassThroughTransform() throws FactoryException, TransformException { |
| /* |
| * This non-linear transform increase the number of dimensions from 2 to 3. |
| * In addition we let 2 dimensions passthrough before and 3 passtrough after. |
| */ |
| final MathTransform nonLinear = new PseudoTransform(2, 3); |
| final TransformSeparator s = new TransformSeparator(MathTransforms.passThrough(2, nonLinear, 3)); |
| /* |
| * Trivial case: no dimension specified, we should get the transform unchanged. |
| */ |
| assertSame("transform", s.transform, s.separate()); |
| assertArrayEquals("sourceDimensions", new int[] {0, 1, 2, 3, 4, 5, 6}, s.getSourceDimensions()); |
| assertArrayEquals("targetDimensions", new int[] {0, 1, 2, 3, 4, 5, 6, 7}, s.getTargetDimensions()); |
| /* |
| * Filter only target dimensions. If the requested indices overlap the pass-through transform, |
| * TransformSeparator will just concatenate a matrix after the transform for dropping dimensions. |
| */ |
| Matrix expected = Matrices.create(4, 9, new double[] { |
| 0, 1, 0, 0, 0, 0, 0, 0, 0, |
| 0, 0, 1, 0, 0, 0, 0, 0, 0, |
| 0, 0, 0, 0, 0, 0, 0, 1, 0, |
| 0, 0, 0, 0, 0, 0, 0, 0, 1 |
| }); |
| s.clear(); |
| s.addTargetDimensions(1, 2, 7); |
| s.addSourceDimensionRange(0, 7); |
| MathTransform result = s.separate(); |
| assertArrayEquals("sourceDimensions", new int[] {0, 1, 2, 3, 4, 5, 6}, s.getSourceDimensions()); |
| assertArrayEquals("targetDimensions", new int[] {1, 2, 7}, s.getTargetDimensions()); |
| final Random random = TestUtilities.createRandomNumberGenerator(); |
| compare(s.transform, MathTransforms.linear(expected), result, random); |
| /* |
| * Filter only target dimensions, but with indices that are all outside the pass-through transform. |
| * TransformSeparator should be able to give us a simple affine transform. |
| */ |
| expected = Matrices.create(4, 8, new double[] { |
| 0, 1, 0, 0, 0, 0, 0, 0, |
| 0, 0, 0, 0, 1, 0, 0, 0, |
| 0, 0, 0, 0, 0, 0, 1, 0, |
| 0, 0, 0, 0, 0, 0, 0, 1 |
| }); |
| s.clear(); |
| s.addTargetDimensions(1, 5, 7); |
| s.addSourceDimensionRange(0, 7); |
| result = s.separate(); |
| assertArrayEquals ("sourceDimensions", new int[] {0, 1, 2, 3, 4, 5, 6}, s.getSourceDimensions()); |
| assertArrayEquals ("targetDimensions", new int[] {1, 5, 7}, s.getTargetDimensions()); |
| assertInstanceOf ("separate()", LinearTransform.class, result); |
| assertMatrixEquals("separate().transform2", expected, ((LinearTransform) result).getMatrix(), STRICT); |
| /* |
| * Filter source dimensions. If we ask only for dimensions not in the pass-through transform, |
| * then TransformSeparator should return an affine transform. |
| */ |
| expected = new Matrix3( |
| 1, 0, 0, |
| 0, 1, 0, |
| 0, 0, 1 |
| ); |
| s.clear(); |
| s.addSourceDimensions(0, 6); |
| result = s.separate(); |
| assertArrayEquals ("sourceDimensions", new int[] {0, 6}, s.getSourceDimensions()); |
| assertArrayEquals ("targetDimensions", new int[] {0, 7}, s.getTargetDimensions()); |
| assertInstanceOf ("separate()", LinearTransform.class, result); |
| assertMatrixEquals("separate().transform2", expected, ((LinearTransform) result).getMatrix(), STRICT); |
| /* |
| * Filter source dimensions, now with overlapping in the pass-through transform. |
| * TransformSeparator is expected to create a new PassThroughTransform. |
| */ |
| s.clear(); |
| s.addSourceDimensions(1, 2, 3, 4, 5); |
| result = s.separate(); |
| assertArrayEquals("sourceDimensions", new int[] {1, 2, 3, 4, 5}, s.getSourceDimensions()); |
| assertArrayEquals("targetDimensions", new int[] {1, 2, 3, 4, 5, 6}, s.getTargetDimensions()); |
| assertInstanceOf ("separate()", PassThroughTransform.class, result); |
| assertSame ("subTransform", nonLinear, ((PassThroughTransform) result).subTransform); |
| assertEquals("firstAffectedCoordinate", 1, ((PassThroughTransform) result).firstAffectedCoordinate); |
| assertEquals("numTrailingCoordinates", 2, ((PassThroughTransform) result).numTrailingCoordinates); |
| } |
| |
| /** |
| * Compares coordinate computed by a reference with coordinates computed by the transform to test. |
| * We use this method when we can not easily analyze the {@link MathTransform} created by the test |
| * case, for example because it may have been rearranged in arbitrary ways for optimization purpose |
| * (e.g. {@link PassThroughTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}). |
| * |
| * @param tr1 first half of the transform to use as a reference. |
| * @param tr2 second half of the transform to use as a reference. |
| * @param test the transform to test. |
| * @param random random number generator for coordinate values. |
| */ |
| private static void compare(final MathTransform tr1, final MathTransform tr2, final MathTransform test, final Random random) |
| throws TransformException |
| { |
| DirectPosition source = new GeneralDirectPosition(tr1.getSourceDimensions()); |
| DirectPosition step = null; |
| DirectPosition expected = null; |
| DirectPosition actual = null; |
| for (int t=0; t<50; t++) { |
| for (int i=source.getDimension(); --i>=0;) { |
| source.setOrdinate(i, random.nextDouble()); |
| } |
| step = tr1 .transform(source, step); |
| expected = tr2 .transform(step, expected); |
| actual = test.transform(source, actual); |
| assertEquals(expected, actual); |
| } |
| } |
| |
| /** |
| * Tests separation of a concatenated transform containing a pass through transform. |
| * |
| * @throws FactoryException if an error occurred while creating a new transform. |
| * @throws TransformException if an error occurred while transforming coordinates for comparison purpose. |
| */ |
| @Test |
| @DependsOnMethod({"testConcatenatedTransform", "testPassThroughTransform"}) |
| public void testConcatenatedPassThroughTransform() throws FactoryException, TransformException { |
| final MathTransform linear = MathTransforms.scale(4, 1, 1, 1, 1, 6); |
| final MathTransform nonLinear = new PseudoTransform(3, 2); |
| final MathTransform passthrough = MathTransforms.passThrough(2, nonLinear, 1); |
| final MathTransform concatenated = new ConcatenatedTransform(linear, passthrough); // Bypass 'tryOptimized' method. |
| final TransformSeparator sep = new TransformSeparator(concatenated); |
| sep.addSourceDimensionRange(0, 2); |
| assertMatrixEquals("Leading passthrough dimensions", new Matrix3(4, 0, 0, 0, 1, 0, 0, 0, 1), |
| MathTransforms.getMatrix(sep.separate()), STRICT); |
| sep.clear(); |
| sep.addSourceDimensionRange(5, 6); |
| assertMatrixEquals("Trailing passthrough dimensions", new Matrix2(6, 0, 0, 1), |
| MathTransforms.getMatrix(sep.separate()), STRICT); |
| sep.clear(); |
| sep.addSourceDimensionRange(2, 5); |
| assertSame("subTransform", nonLinear, sep.separate()); |
| |
| sep.clear(); |
| sep.addSourceDimensionRange(1, 5); |
| MathTransform mt = sep.separate(); |
| assertInstanceOf("separate()", PassThroughTransform.class, mt); |
| final PassThroughTransform ps = ((PassThroughTransform) mt); |
| assertEquals("firstAffectedCoordinate", 1, ps.firstAffectedCoordinate); |
| assertEquals("numTrailingCoordinates", 0, ps.numTrailingCoordinates); |
| assertSame ("subTransform", nonLinear, ps.subTransform); |
| } |
| |
| /** |
| * Tests separation of a pass through transform containing another pass through transform. |
| * |
| * @throws FactoryException if an error occurred while creating a new transform. |
| * @throws TransformException if an error occurred while transforming coordinates for comparison purpose. |
| */ |
| @Test |
| @DependsOnMethod("testConcatenatedPassThroughTransform") |
| public void testNestedPassThroughTransform() throws FactoryException, TransformException { |
| final MathTransform nonLinear = new PseudoTransform(3, 2); |
| final MathTransform passthrough1 = MathTransforms.passThrough(2, nonLinear, 1); |
| final MathTransform concatenated = new ConcatenatedTransform(MathTransforms.scale(4, 3, 2, 1, 1, 6), passthrough1); |
| final MathTransform passthrough2 = new PassThroughTransform(2, concatenated, 3); |
| final TransformSeparator sep = new TransformSeparator(passthrough2); |
| sep.addSourceDimensionRange(3, 7); |
| MathTransform mt = sep.separate(); |
| assertInstanceOf("separate()", ConcatenatedTransform.class, mt); |
| assertMatrixEquals("Leading passthrough dimensions", Matrices.create(5, 5, new double[] { |
| 3, 0, 0, 0, 0, |
| 0, 2, 0, 0, 0, |
| 0, 0, 1, 0, 0, |
| 0, 0, 0, 1, 0, |
| 0, 0, 0, 0, 1}), MathTransforms.getMatrix(((ConcatenatedTransform) mt).transform1), STRICT); |
| |
| mt = ((ConcatenatedTransform) mt).transform2; |
| assertInstanceOf("subTransform", PassThroughTransform.class, mt); |
| final PassThroughTransform ps = ((PassThroughTransform) mt); |
| assertEquals("firstAffectedCoordinate", 1, ps.firstAffectedCoordinate); |
| assertEquals("numTrailingCoordinates", 0, ps.numTrailingCoordinates); |
| assertSame ("subTransform", nonLinear, ps.subTransform); |
| } |
| |
| /** |
| * Tests {@link TransformSeparator} with removal of unused source dimensions. |
| * |
| * @throws FactoryException if an error occurred while creating a new transform. |
| */ |
| @Test |
| @DependsOnMethod("testLinearTransform") |
| public void testTrimSourceDimensions() throws FactoryException { |
| MathTransform tr = MathTransforms.linear(Matrices.create(3, 4, new double[] { |
| 0, 0.5, 0, -90, |
| 0.5, 0, 0, -180, |
| 0, 0, 0, 1})); |
| /* |
| * Verify that TransformSeparator does not trim anything if not requested so. |
| */ |
| TransformSeparator s = new TransformSeparator(tr); |
| s.addSourceDimensionRange(0, tr.getSourceDimensions()); |
| assertSame("No source dimensions should be trimmed if not requested.", tr, s.separate()); |
| assertArrayEquals(new int[] {0, 1, 2}, s.getSourceDimensions()); |
| assertArrayEquals(new int[] {0, 1 }, s.getTargetDimensions()); |
| /* |
| * Trim the last dimension (most common case). |
| */ |
| final Matrix expected = new Matrix3( |
| 0, 0.5, -90, |
| 0.5, 0, -180, |
| 0, 0, 1); |
| s.clear(); |
| MathTransform reduced = s.separate(); |
| assertNotEquals("separate()", tr, reduced); |
| assertArrayEquals(new int[] {0, 1}, s.getSourceDimensions()); |
| assertArrayEquals(new int[] {0, 1}, s.getTargetDimensions()); |
| assertMatrixEquals("separate()", expected, MathTransforms.getMatrix(reduced), STRICT); |
| /* |
| * Trim the first dimension. |
| */ |
| tr = MathTransforms.linear(Matrices.create(3, 4, new double[] { |
| 0, 0, 0.5, -90, |
| 0, 0.5, 0, -180, |
| 0, 0, 0, 1})); |
| |
| s = new TransformSeparator(tr); |
| reduced = s.separate(); |
| assertNotEquals("separate()", tr, reduced); |
| assertArrayEquals(new int[] {1, 2}, s.getSourceDimensions()); |
| assertArrayEquals(new int[] {0, 1}, s.getTargetDimensions()); |
| assertMatrixEquals("separate()", new Matrix3( |
| 0, 0.5, -90, |
| 0.5, 0, -180, |
| 0, 0, 1), MathTransforms.getMatrix(reduced), STRICT); |
| } |
| } |