This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 7498ef3 More robust concatenation of transforms when some steps may
be dropping or adding a dimension.
7498ef3 is described below
commit 7498ef3fc02859c7cb2bc23c114554dac9829416
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu May 7 17:19:50 2020 +0200
More robust concatenation of transforms when some steps may be dropping or
adding a dimension.
---
.../operation/transform/ConcatenatedTransform.java | 66 +++++++++++++++++++---
.../operation/transform/IdentityTransform.java | 10 +++-
.../transform/ConcatenatedTransformTest.java | 43 +++++++++++++-
3 files changed, 110 insertions(+), 9 deletions(-)
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
index 987429e..801919d 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
@@ -37,6 +37,7 @@ import
org.apache.sis.internal.referencing.provider.GeocentricAffine;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.system.Semaphores;
+import org.apache.sis.internal.system.Loggers;
import org.apache.sis.internal.util.Strings;
import org.apache.sis.util.Classes;
import org.apache.sis.util.ComparisonMode;
@@ -44,6 +45,7 @@ import org.apache.sis.util.Utilities;
import org.apache.sis.io.wkt.Convention;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.io.wkt.FormattableObject;
+import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;
@@ -56,7 +58,7 @@ import org.apache.sis.util.resources.Errors;
* <p>Concatenated transforms are serializable if all their step transforms
are serializable.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
*
* @see
org.opengis.referencing.operation.MathTransformFactory#createConcatenatedTransform(MathTransform,
MathTransform)
*
@@ -182,7 +184,7 @@ class ConcatenatedTransform extends AbstractMathTransform
implements Serializabl
/**
* Tries to returns an optimized concatenation, for example by merging two
affine transforms
- * into a single one. If no optimized cases has been found, returns {@code
null}. In the later
+ * into a single one. If no optimized case has been found, returns {@code
null}. In the later
* case, the caller will need to create a more heavy {@link
ConcatenatedTransform} instance.
*
* @param factory the factory which is (indirectly) invoking this
method, or {@code null} if none.
@@ -240,6 +242,53 @@ class ConcatenatedTransform extends AbstractMathTransform
implements Serializabl
* If both transforms use matrix, then we can create
* a single transform using the concatenated matrix.
*/
+ final MathTransform concatenated = multiply(tr1, tr2, factory);
+ if (concatenated instanceof AbstractLinearTransform) {
+ /*
+ * Following code computes the inverse of `concatenated`
transform. In principle this is
+ * not necessary because `MathTransform.inverse()` would do that
computation itself when
+ * first needed. However if the matrices are not square (e.g. if a
transform is dropping
+ * a dimension) some information may be lost. By computing inverse
transform immediately
+ * as the concatenation of the inverse of individual transforms,
we use information that
+ * would otherwise be lost (e.g. the inverse of the transform
dropping a dimension may be
+ * a transform setting that dimension to a constant value, often
zero). Consequently the
+ * inverse transform here may have real values for coefficients
that `MathTransform.inverse()`
+ * would have set to NaN, or may succeed while
`MathTransform.inverse()` would have throw
+ * an exception. Even with square matrices, computing the inverse
transform now may avoid
+ * some rounding errors.
+ */
+ final AbstractLinearTransform impl = (AbstractLinearTransform)
concatenated;
+ if (impl.inverse == null) try {
+ final MathTransform inverse = multiply(tr2.inverse(),
tr1.inverse(), factory);
+ if (inverse != null) {
+ if (inverse.isIdentity()) {
+ return inverse;
+ }
+ if (inverse instanceof LinearTransform) {
+ impl.inverse = (LinearTransform) inverse;
+ }
+ }
+ } catch (NoninvertibleTransformException e) {
+ /*
+ * Ignore. The `concatenated.inverse()` method will try to
compute the inverse itself,
+ * possible at the cost of more NaN values than what above
code would have produced.
+ */
+
Logging.recoverableException(Logging.getLogger(Loggers.COORDINATE_OPERATION),
+ ConcatenatedTransform.class, "create", e);
+ }
+ }
+ return concatenated;
+ }
+
+ /**
+ * Returns a transform resulting from the multiplication of the matrices
of given transforms.
+ * If the given transforms does not provide matrix, then this method
returns {@code null}.
+ *
+ * @param factory the factory which is (indirectly) invoking this
method, or {@code null} if none.
+ */
+ private static MathTransform multiply(final MathTransform tr1, final
MathTransform tr2,
+ final MathTransformFactory factory) throws FactoryException
+ {
final Matrix matrix1 = MathTransforms.getMatrix(tr1);
if (matrix1 != null) {
final Matrix matrix2 = MathTransforms.getMatrix(tr2);
@@ -251,9 +300,9 @@ class ConcatenatedTransform extends AbstractMathTransform
implements Serializabl
/*
* NOTE: It is quite tempting to "fix rounding errors" in the
matrix before to create the transform.
* But this is often wrong for datum shift transformations
(Molodensky and the like) since the datum
- * shifts are very small. The shift may be the order of
magnitude of the tolerance threshold. Intead,
+ * shifts are very small. The shift may be the order of
magnitude of the tolerance threshold. Instead,
* Apache SIS performs matrix operations using double-double
arithmetic in the hope to get exact
- * results at the 'double' accuracy, which avoid the need for
a tolerance threshold.
+ * results at the `double` accuracy, which avoid the need for
a tolerance threshold.
*/
if (factory != null) {
return factory.createAffineTransform(matrix);
@@ -355,10 +404,13 @@ class ConcatenatedTransform extends AbstractMathTransform
implements Serializabl
}
/**
- * Returns all concatenated transforms, modified with the pre- and
post-processing required for WKT formating.
+ * Returns all concatenated transforms, modified with the pre- and
post-processing required for WKT formatting.
* More specifically, if there is any Apache SIS implementation of Map
Projection in the chain, then the
* (<var>normalize</var>, <var>normalized projection</var>,
<var>denormalize</var>) tuples are replaced by single
* (<var>projection</var>) elements, which does not need to be instances
of {@link MathTransform}.
+ *
+ * <p>This method is used only for producing human-readable parameter
values.
+ * It is not used for coordinate operations or construction of operation
chains.</p>
*/
private List<Object> getPseudoSteps() {
final List<Object> transforms = new ArrayList<>();
@@ -821,8 +873,8 @@ class ConcatenatedTransform extends AbstractMathTransform
implements Serializabl
/**
* Concatenates or pre-concatenates in an optimized way this transform
with the given transform, if possible.
- * This method try to delegate the concatenation to {@link #transform1} or
{@link #transform2}. Assuming that
- * transforms are associative, this is equivalent to trying the following
arrangements:
+ * This method tries to delegate the concatenation to {@link #transform1}
or {@link #transform2}.
+ * Assuming that transforms are associative, this is equivalent to trying
the following arrangements:
*
* {@preformat text
* Instead of : other → tr1 → tr2
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/IdentityTransform.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/IdentityTransform.java
index 6cce32a..0663f4c 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/IdentityTransform.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/IdentityTransform.java
@@ -63,6 +63,13 @@ final class IdentityTransform extends
AbstractLinearTransform {
*/
private IdentityTransform(final int dimension) {
this.dimension = dimension;
+ /*
+ * This value is not used by `inverse()` method, but we set it anyway
because
+ * `ConcatenatedTransform.createOptimized(…)` checks if the value is
non-null.
+ * A null value would cause unwanted computation of inverse
transform(because
+ * `ConcatenatedTransform` computes it itself.
+ */
+ inverse = this;
}
/**
@@ -224,10 +231,11 @@ final class IdentityTransform extends
AbstractLinearTransform {
}
/**
- * Returns the inverse transform of this object, which is this transform
itself
+ * Returns the inverse transform of this object, which is this transform
itself.
*/
@Override
public LinearTransform inverse() {
+ // Avoid unnecessary access to `inverse` volatile field.
return this;
}
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransformTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransformTest.java
index bf29c78..aebf03f 100644
---
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransformTest.java
+++
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransformTest.java
@@ -19,11 +19,13 @@ package org.apache.sis.referencing.operation.transform;
import org.opengis.util.FactoryException;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.Matrix4;
import org.apache.sis.test.DependsOn;
import org.junit.Test;
+import org.opengis.test.Assert;
import static org.opengis.test.Assert.*;
@@ -32,7 +34,7 @@ import static org.opengis.test.Assert.*;
* Tests the {@link ConcatenatedTransform} class.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
* @since 0.5
* @module
*/
@@ -147,4 +149,43 @@ public final strictfp class ConcatenatedTransformTest
extends MathTransformTestC
assertEquals("Source dimensions", 3, transform.getSourceDimensions());
assertEquals("Target dimensions", 4, transform.getTargetDimensions());
}
+
+ /**
+ * Tests concatenation of transforms built from non-square matrices. The
transforms are invertible
+ * when taken separately, but the transform resulting from concatenation
can not be inverted unless
+ * {@link ConcatenatedTransform#createOptimized(MathTransform,
MathTransform, MathTransformFactory)}
+ * prepares in advance the inverse transform using the inverse of original
transforms.
+ *
+ * @throws NoninvertibleTransformException if a transform can not be
inverted.
+ */
+ @Test
+ public void testNonSquares() throws NoninvertibleTransformException {
+ final LinearTransform tr1 = MathTransforms.scale(8, 6, 0.5);
+ final LinearTransform tr2 = MathTransforms.linear(Matrices.create(4,
3, new double[] {
+ 2, 0, 0, // Scale first dimension.
+ 0, 3, 0, // Scale second dimension.
+ 0, 0, 5, // Set third dimension to a constant.
+ 0, 0, 1})); // Usual row in affine transforms.
+ /*
+ * Request for a transform going from 3D points to 2D points.
+ * Dropping a dimension is not a problem.
+ */
+ final MathTransform c = MathTransforms.concatenate(tr1, tr2.inverse());
+ Assert.assertMatrixEquals("Forward", Matrices.create(3, 4, new
double[] {
+ 4, 0, 0, 0, // scale = 8/2
+ 0, 2, 0, 0, // scale = 6/3
+ 0, 0, 0, 1}), MathTransforms.getMatrix(c), 0);
+ /*
+ * Following test is the interesting part. By inverting the transform,
we ask for a conversion
+ * from 2D points to 3D points. Without contextual information we
would not know which value to
+ * put in the third dimension (that value would fallback on NaN). But
with the knowledge that
+ * this concatenation was built from a transform which was putting
value 5 in third dimension,
+ * we can complete the matrix as below with value 10 in third
dimension.
+ */
+ Assert.assertMatrixEquals("Inverse", Matrices.create(4, 3, new
double[] {
+ 0.25, 0, 0,
+ 0, 0.5, 0,
+ 0, 0, 10, // Having value 10 instead of NaN is the main
purpose of this test.
+ 0, 0, 1}), MathTransforms.getMatrix(c.inverse()), 0);
+ }
}