sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/02: When there is no direct Bursa-Wolf parameters between two CRS, accepts an indirect transformation through a common hub (typically WGS 84). This is a partial revert of commit 504e3ce3bfd44c6aea1ff9f4d5ba298d4b0a22f2 (November 14, 2013) together with new code for updating positional accuracy. An arbitrary value of 100 metres is taken when the domains of validity intersect, and 3000 metres (presumed worst case scenario) otherwise.
Date Mon, 31 Aug 2020 17:35:45 GMT
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

commit afdf695c15841e6f6d2ad23c24833111b4d0331e
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Aug 31 14:50:29 2020 +0200

    When there is no direct Bursa-Wolf parameters between two CRS, accepts an indirect transformation
through a common hub (typically WGS 84).
    This is a partial revert of commit 504e3ce3bfd44c6aea1ff9f4d5ba298d4b0a22f2 (November
14, 2013) together with new code for updating positional accuracy.
    An arbitrary value of 100 metres is taken when the domains of validity intersect, and
3000 metres (presumed worst case scenario) otherwise.
    
    Mailing list: https://lists.apache.org/thread.html/r751d8fca4997faa59202a6cf4c85f1b1824bf06abc315995fe927719%40%3Cuser.sis.apache.org%3E
---
 .../sis/internal/referencing/AnnotatedMatrix.java  | 140 +++++++++++++++++++++
 .../sis/internal/referencing/ExtentSelector.java   |  33 ++++-
 .../referencing/PositionalAccuracyConstant.java    |  40 ++++--
 .../referencing/datum/DefaultGeodeticDatum.java    |  53 ++++++--
 .../operation/CoordinateOperationFinder.java       |  15 ++-
 .../datum/DefaultGeodeticDatumTest.java            |  44 ++++++-
 .../operation/CoordinateOperationFinderTest.java   |  74 ++++++++++-
 7 files changed, 370 insertions(+), 29 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AnnotatedMatrix.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AnnotatedMatrix.java
new file mode 100644
index 0000000..8308ca4
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AnnotatedMatrix.java
@@ -0,0 +1,140 @@
+/*
+ * 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.internal.referencing;
+
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.metadata.quality.PositionalAccuracy;
+
+
+/**
+ * A matrix augmented with annotation about transformation accuracy.
+ * We use this class for passing additional information in methods that returns only a {@link
Matrix}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class AnnotatedMatrix implements Matrix, Cloneable {
+    /**
+     * The matrix which contains the actual values.
+     */
+    private final Matrix matrix;
+
+    /**
+     * Accuracy associated with this matrix.
+     */
+    public final PositionalAccuracy accuracy;
+
+    /**
+     * Creates a new matrix wrapping the given matrix.
+     *
+     * @param  matrix    the matrix which contains the actual values.
+     * @param  accuracy  accuracy associated with this matrix.
+     */
+    private AnnotatedMatrix(final Matrix matrix, final PositionalAccuracy accuracy) {
+        this.matrix   = matrix;
+        this.accuracy = accuracy;
+    }
+
+    /**
+     * Returns an {@link AnnotatedMatrix} associates with {@link PositionalAccuracyConstant#INDIRECT_SHIFT_APPLIED}.
+     *
+     * @param  matrix     the matrix to wrap.
+     * @param  intersect  whether an intersection has been found between the two datum.
+     * @return the annotated matrix.
+     */
+    public static Matrix indirect(final Matrix matrix, final boolean intersect) {
+        return new AnnotatedMatrix(matrix,
+                intersect ? PositionalAccuracyConstant.INDIRECT_SHIFT_APPLIED
+                          : PositionalAccuracyConstant.DATUM_SHIFT_OMITTED);
+    }
+
+    /**
+     * Returns the number of rows in this matrix.
+     */
+    @Override
+    public int getNumRow() {
+        return matrix.getNumRow();
+    }
+
+    /**
+     * Returns the number of columns in this matrix.
+     */
+    @Override
+    public int getNumCol() {
+        return matrix.getNumCol();
+    }
+
+    /**
+     * Returns {@code true} if this matrix is an identity matrix.
+     */
+    @Override
+    public boolean isIdentity() {
+        return matrix.isIdentity();
+    }
+
+    /**
+     * Retrieves the value at the specified row and column of this matrix.
+     */
+    @Override
+    public double getElement(int row, int column) {
+        return matrix.getElement(row, column);
+    }
+
+    /**
+     * Modifies the value at the specified row and column of this matrix.
+     */
+    @Override
+    public void setElement(int row, int column, double value) {
+        matrix.setElement(row, column, value);
+    }
+
+    /**
+     * Returns a clone of this matrix.
+     */
+    @Override
+    @SuppressWarnings("CloneDoesntCallSuperClone")
+    public Matrix clone() {
+        return new AnnotatedMatrix(matrix.clone(), accuracy);
+    }
+
+    /**
+     * Compares this matrix with the given object for equality.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj instanceof AnnotatedMatrix) {
+            final AnnotatedMatrix other = (AnnotatedMatrix) obj;
+            return matrix.equals(other.matrix) && accuracy.equals(other.accuracy);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value for this matrix.
+     */
+    @Override
+    public int hashCode() {
+        return matrix.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return matrix.toString();
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java
index 6201282..59b1a8f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java
@@ -27,7 +27,7 @@ import org.apache.sis.metadata.iso.extent.Extents;
  * This may be extended to other kind of extent in any future SIS version.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.4
+ * @version 1.1
  *
  * @param <T>  the type of object to be selected.
  *
@@ -37,8 +37,9 @@ import org.apache.sis.metadata.iso.extent.Extents;
 public final class ExtentSelector<T> {
     /**
      * The area of interest, or {@code null} if none.
+     * This is specified at construction time, but can be modified later.
      */
-    private final GeographicBoundingBox areaOfInterest;
+    private GeographicBoundingBox areaOfInterest;
 
     /**
      * The best object found so far.
@@ -62,6 +63,25 @@ public final class ExtentSelector<T> {
     }
 
     /**
+     * Returns the area of interest.
+     *
+     * @return area of interest, or {@code null} if none.
+     */
+    public final GeographicBoundingBox getAreaOfInterest() {
+        return areaOfInterest;
+    }
+
+    /**
+     * Sets the area of interest to the intersection of the two given arguments.
+     *
+     * @param  a1  first area of interest as a bounding box, or {@code null}.
+     * @param  a2  second area of interest as an extent, or {@code null}.
+     */
+    public final void setAreaOfInterest(final GeographicBoundingBox a1, final Extent a2)
{
+        areaOfInterest = Extents.intersection(a1, Extents.getGeographicBoundingBox(a2));
+    }
+
+    /**
      * Evaluates the given extent against the criteria represented by the {@code ExtentSelector}.
      * If the intersection between the given extent and the area of interest is greater than
any
      * previous intersection, then the given extent and object are remembered as the best
match
@@ -97,4 +117,13 @@ public final class ExtentSelector<T> {
     public T best() {
         return best;
     }
+
+    /**
+     * Returns {@code true} if an intersection has been found.
+     *
+     * @return whether an intersection has been found.
+     */
+    public boolean hasIntersection() {
+        return largestArea > 0;
+    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionalAccuracyConstant.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionalAccuracyConstant.java
index a510fae..9622a3d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionalAccuracyConstant.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionalAccuracyConstant.java
@@ -43,7 +43,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * Pre-defined positional accuracy resulting from some coordinate operations.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.6
+ * @version 1.1
  *
  * @see org.opengis.referencing.operation.Transformation#getCoordinateOperationAccuracy()
  *
@@ -80,6 +80,14 @@ public final class PositionalAccuracyConstant extends DefaultAbsoluteExternalPos
     private static final double DATUM_SHIFT_ACCURACY = 25;
 
     /**
+     * Default accuracy of datum shifts when using an intermediate datum (typically WGS 84).
+     * Since this is a concatenation of two datum shifts, we use twice {@link #DATUM_SHIFT_ACCURACY}.
+     * The result is multiplied by 2 again as a margin because we have no guarantees that
the domain
+     * of validity of the two datum are close enough for making this concatenation valid.
+     */
+    public static final double INDIRECT_SHIFT_ACCURACY = 100;
+
+    /**
      * Indicates that a {@linkplain org.opengis.referencing.operation.Transformation transformation}
      * requires a datum shift and some method has been applied. Datum shift methods often
use
      * {@linkplain org.apache.sis.referencing.datum.BursaWolfParameters Bursa Wolf parameters},
@@ -94,11 +102,19 @@ public final class PositionalAccuracyConstant extends DefaultAbsoluteExternalPos
      * been found. Such datum shifts are approximations and may have 1 kilometer error.
      */
     public static final PositionalAccuracy DATUM_SHIFT_OMITTED;
+
+    /**
+     * Indicates that a {@linkplain org.opengis.referencing.operation.Transformation transformation}
+     * requires a datum shift, but only an indirect method has been found. The indirect method
uses
+     * an intermediate datum, typically WGS 84.
+     */
+    public static final PositionalAccuracy INDIRECT_SHIFT_APPLIED;
     static {
         final InternationalString desc = Vocabulary.formatInternational(Vocabulary.Keys.TransformationAccuracy);
         final InternationalString eval = Resources .formatInternational(Resources.Keys.ConformanceMeansDatumShift);
-        DATUM_SHIFT_APPLIED = new PositionalAccuracyConstant(desc, eval, true);
-        DATUM_SHIFT_OMITTED = new PositionalAccuracyConstant(desc, eval, false);
+        DATUM_SHIFT_APPLIED    = new PositionalAccuracyConstant(desc, eval, true);
+        DATUM_SHIFT_OMITTED    = new PositionalAccuracyConstant(desc, eval, false);
+        INDIRECT_SHIFT_APPLIED = new PositionalAccuracyConstant(desc, eval, true);
     }
 
     /**
@@ -122,8 +138,9 @@ public final class PositionalAccuracyConstant extends DefaultAbsoluteExternalPos
      * @throws ObjectStreamException if the serialized object defines an unknown data type.
      */
     private Object readResolve() throws ObjectStreamException {
-        if (equals(DATUM_SHIFT_APPLIED)) return DATUM_SHIFT_APPLIED;
-        if (equals(DATUM_SHIFT_OMITTED)) return DATUM_SHIFT_OMITTED;
+        if (equals(DATUM_SHIFT_APPLIED))    return DATUM_SHIFT_APPLIED;
+        if (equals(DATUM_SHIFT_OMITTED))    return DATUM_SHIFT_OMITTED;
+        if (equals(INDIRECT_SHIFT_APPLIED)) return INDIRECT_SHIFT_APPLIED;
         return this;
     }
 
@@ -194,11 +211,14 @@ public final class PositionalAccuracyConstant extends DefaultAbsoluteExternalPos
              * about the return values chosen.
              */
             if (operation instanceof Transformation) {
-                if (accuracies.contains(DATUM_SHIFT_APPLIED)) {
-                    return DATUM_SHIFT_ACCURACY;
-                }
-                if (accuracies.contains(DATUM_SHIFT_OMITTED)) {
-                    return UNKNOWN_ACCURACY;
+                for (final PositionalAccuracy element : accuracies) {
+                    /*
+                     * Really need identity comparisons, not Object.equals(Object), because
the later
+                     * does not distinguish between DATUM_SHIFT_APPLIED and INDIRECT_SHIFT_APPLIED.
+                     */
+                    if (element == DATUM_SHIFT_APPLIED)    return DATUM_SHIFT_ACCURACY;
+                    if (element == DATUM_SHIFT_OMITTED)    return UNKNOWN_ACCURACY;
+                    if (element == INDIRECT_SHIFT_APPLIED) return INDIRECT_SHIFT_ACCURACY;
                 }
             }
             /*
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
index 47cf530..4d72af4 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
@@ -27,17 +27,20 @@ import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.extent.Extent;
+import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.referencing.crs.GeodeticCRS;
 import org.opengis.referencing.datum.Ellipsoid;
 import org.opengis.referencing.datum.PrimeMeridian;
 import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.matrix.NoninvertibleMatrixException;
 import org.apache.sis.metadata.iso.extent.Extents;
 import org.apache.sis.internal.referencing.WKTKeywords;
 import org.apache.sis.internal.metadata.NameToIdentifier;
 import org.apache.sis.internal.metadata.MetadataUtilities;
+import org.apache.sis.internal.referencing.AnnotatedMatrix;
 import org.apache.sis.internal.referencing.CoordinateOperations;
 import org.apache.sis.internal.referencing.ExtentSelector;
 import org.apache.sis.internal.util.CollectionsExt;
@@ -122,7 +125,7 @@ import static org.apache.sis.internal.referencing.WKTUtilities.toFormattable;
  * constants.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.1
  *
  * @see DefaultEllipsoid
  * @see DefaultPrimeMeridian
@@ -375,7 +378,7 @@ public class DefaultGeodeticDatum extends AbstractDatum implements GeodeticDatum
      * 1053 – <cite>Time-dependent Position Vector transformation</cite>.
      *
      * <p>If this datum and the given {@code targetDatum} do not use the same {@linkplain
#getPrimeMeridian() prime meridian},
-     * then it is caller's responsibility to to apply longitude rotation before to use the
matrix returned by this method.
+     * then it is caller's responsibility to apply longitude rotation before to use the matrix
returned by this method.
      * The target prime meridian should be Greenwich (see {@linkplain #DefaultGeodeticDatum(Map,
Ellipsoid, PrimeMeridian)
      * constructor javadoc}), in which case the datum shift should be applied in a geocentric
coordinate system having
      * Greenwich as the prime meridian.</p>
@@ -439,23 +442,47 @@ public class DefaultGeodeticDatum extends AbstractDatum implements GeodeticDatum
                 Logging.unexpectedException(Logging.getLogger(Loggers.COORDINATE_OPERATION),
                         DefaultGeodeticDatum.class, "getPositionVectorTransformation", e);
             }
+            /*
+             * No direct tranformation found. Search for a path through an intermediate datum.
+             * First, search if there is some BursaWolfParameters for the same target in
both
+             * `source` and `target` datum. If such an intermediate is found, ask for path:
+             *
+             *    source   →   [common datum]   →   target
+             *
+             * A consequence of such indirect path is that it may connect unrelated datums
+             * if [common datum] is a world datum such as WGS84. We do not have a solution
+             * for preventing that.
+             */
+            if (bursaWolf != null) {
+                final GeographicBoundingBox bbox = selector.getAreaOfInterest();
+                for (final BursaWolfParameters toPivot : bursaWolf) {
+                    selector.setAreaOfInterest(bbox, toPivot.getDomainOfValidity());
+                    candidate = ((DefaultGeodeticDatum) targetDatum).select(toPivot.getTargetDatum(),
selector);
+                    if (candidate != null) {
+                        final Matrix step1 = createTransformation(toPivot,   areaOfInterest);
+                        final Matrix step2 = createTransformation(candidate, areaOfInterest);
+                        /*
+                         * MatrixSIS.multiply(MatrixSIS) is equivalent to AffineTransform.concatenate(…):
+                         * First transform by the supplied transform and then transform the
result by the
+                         * original transform.
+                         */
+                        try {
+                            Matrix m = MatrixSIS.castOrCopy(step2).inverse().multiply(step1);
+                            return AnnotatedMatrix.indirect(m, selector.hasIntersection());
+                        } catch (NoninvertibleMatrixException e) {
+                            Logging.unexpectedException(Logging.getLogger(Loggers.COORDINATE_OPERATION),
+                                    DefaultGeodeticDatum.class, "getPositionVectorTransformation",
e);
+                        }
+                    }
+                }
+            }
         }
-        /*
-         * In a previous version, we were used to search for a transformation path through
a common datum:
-         *
-         *     source   →   [common datum]   →   target
-         *
-         * This has been removed, because it was dangerous (many paths may be possible -
we are better to rely on
-         * the EPSG database, which do define some transformation paths explicitly). Especially
since our javadoc
-         * now said that associating BursaWolfParameters to GeodeticDatum is not recommended
except in a few special
-         * cases, this method does not have a picture complete enough for attempting anything
else than a direct path.
-         */
         return null;
     }
 
     /**
      * Invokes {@link BursaWolfParameters#getPositionVectorTransformation(Date)} for a date
calculated from
-     * the temporal elements on the given extent.  This method chooses an instant located
midway between the
+     * the temporal elements on the given extent. This method chooses an instant located
midway between the
      * start and end time.
      */
     private static Matrix createTransformation(final BursaWolfParameters bursaWolf, final
Extent areaOfInterest) {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index 0ca99e2..f31c746 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
@@ -32,10 +32,12 @@ import org.opengis.referencing.crs.*;
 import org.opengis.referencing.datum.*;
 import org.opengis.referencing.operation.*;
 import org.opengis.metadata.Identifier;
+import org.opengis.metadata.quality.PositionalAccuracy;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.internal.referencing.AnnotatedMatrix;
 import org.apache.sis.internal.referencing.CoordinateOperations;
 import org.apache.sis.internal.referencing.EllipsoidalHeightCombiner;
 import org.apache.sis.internal.referencing.ReferencingUtilities;
@@ -685,7 +687,18 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry
{
                 transform = mtFactory.createConcatenatedTransform(transform, after);
             }
         }
-        return asList(createFromMathTransform(properties(identifier), sourceCRS, targetCRS,
transform, method, parameters, null));
+        /*
+         * Adjust the accuracy information if the datum shift has been computed by an indirect
path.
+         * The indirect path uses a third datum (typically WGS84) as an intermediate between
the two
+         * specified datum.
+         */
+        final Map<String, Object> properties = properties(identifier);
+        if (datumShift instanceof AnnotatedMatrix) {
+            properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, new PositionalAccuracy[]
{
+                ((AnnotatedMatrix) datumShift).accuracy
+            });
+        }
+        return asList(createFromMathTransform(properties, sourceCRS, targetCRS, transform,
method, parameters, null));
     }
 
     /**
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/datum/DefaultGeodeticDatumTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/datum/DefaultGeodeticDatumTest.java
index b6ac4fc..f1dfa77 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/datum/DefaultGeodeticDatumTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/datum/DefaultGeodeticDatumTest.java
@@ -27,6 +27,9 @@ import org.opengis.test.Validators;
 import org.apache.sis.measure.Units;
 import org.apache.sis.xml.Namespaces;
 import org.apache.sis.io.wkt.Convention;
+import org.apache.sis.referencing.operation.matrix.Matrix4;
+import org.apache.sis.internal.referencing.AnnotatedMatrix;
+import org.apache.sis.internal.referencing.PositionalAccuracyConstant;
 import org.apache.sis.metadata.iso.extent.DefaultExtent;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.test.xml.TestCase;
@@ -43,7 +46,7 @@ import static org.apache.sis.referencing.GeodeticObjectVerifier.*;
  * Tests the {@link DefaultGeodeticDatum} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.1
  * @since   0.4
  * @module
  */
@@ -185,6 +188,45 @@ public final strictfp class DefaultGeodeticDatumTest extends TestCase
{
     }
 
     /**
+     * Tests {@link DefaultGeodeticDatum#getPositionVectorTransformation(GeodeticDatum, Extent)}
+     * going through an indirect transformation. The main purpose of this test is to verify
that
+     * the matrix is associated with {@link PositionalAccuracyConstant#INDIRECT_SHIFT_APPLIED}.
+     */
+    @Test
+    @DependsOnMethod("testGetPositionVectorTransformation")
+    public void testIndirectTransformation() {
+        final Map<String,Object> properties = new HashMap<>();
+        assertNull(properties.put(DefaultGeodeticDatum.NAME_KEY, "Invalid dummy datum"));
+        assertNull(properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, BursaWolfParametersTest.createWGS72_to_WGS84()));
+        final DefaultGeodeticDatum global = new DefaultGeodeticDatum(properties,
+                GeodeticDatumMock.WGS72.getEllipsoid(),
+                GeodeticDatumMock.WGS72.getPrimeMeridian());
+        /*
+         * Create a datum valid only in a specific region of the world and with no direct
transformation to WGS72.
+         * However an indirect transformation to WGS72 is available through WGS84.
+         */
+        properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, BursaWolfParametersTest.createED87_to_WGS84());
+        final DefaultGeodeticDatum local = new DefaultGeodeticDatum(properties,
+                GeodeticDatumMock.ED50.getEllipsoid(),
+                GeodeticDatumMock.ED50.getPrimeMeridian());
+        /*
+         * Main test: verify that the transformation found is associated with accuracy information.
+         */
+        final Matrix m = local.getPositionVectorTransformation(global, null);
+        assertInstanceOf("Should have accuracy information.", AnnotatedMatrix.class, m);
+        assertSame(PositionalAccuracyConstant.INDIRECT_SHIFT_APPLIED, ((AnnotatedMatrix)
m).accuracy);
+        /*
+         * Following is an anti-regression test only (no authoritative values).
+         * Verified only opportunistically.
+         */
+        assertMatrixEquals("getPositionVectorTransformation", new Matrix4(
+                   1,   7.961E-7,  7.287E-7,   -82.981,
+           -7.961E-7,          1,  2.461E-6,   -99.719,
+           -7.287E-7,  -2.461E-6,         1,  -115.209,
+                   0,          0,         0,         1), m, 0.01);
+    }
+
+    /**
      * Tests {@link DefaultGeodeticDatum#toWKT()}.
      */
     @Test
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
index b381f62..3ab3dea 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
@@ -38,12 +38,14 @@ import org.opengis.referencing.operation.Transformation;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.ConcatenatedOperation;
 import org.opengis.referencing.operation.OperationNotFoundException;
+import org.apache.sis.internal.referencing.PositionalAccuracyConstant;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.crs.DefaultCompoundCRS;
 import org.apache.sis.referencing.crs.DefaultDerivedCRS;
 import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.CRS;
 import org.apache.sis.io.wkt.WKTFormat;
 
 import static org.apache.sis.internal.referencing.Formulas.LINEAR_TOLERANCE;
@@ -70,7 +72,7 @@ import static org.apache.sis.test.Assert.*;
  * Contrarily to {@link CoordinateOperationRegistryTest}, tests in this class are run without
EPSG geodetic dataset.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.7
  * @module
  */
@@ -529,7 +531,7 @@ public final strictfp class CoordinateOperationFinderTest extends MathTransformT
      * @see <a href="https://issues.apache.org/jira/browse/SIS-364">SIS-364</a>
      */
     static String AGD66() {
-        return "PROJCS[“AGD66 / AMG zone 49”,"
+        return "PROJCS[“AGD66 / AMG zone 49”, "
                 + "GEOGCS[“AGD66”, "
                 +   "DATUM[“Australian_Geodetic_Datum_1966”, "
                 +     "SPHEROID[“Australian National Spheroid”,6378160, 298.25, AUTHORITY[“EPSG”,“7003”]],"
@@ -550,6 +552,74 @@ public final strictfp class CoordinateOperationFinderTest extends MathTransformT
     }
 
     /**
+     * Tests a transformation between two CRS for which no direct bursa-wolf parameters are
defined.
+     * However a transformation should still be possible indirectly, through WGS 84.
+     *
+     * @throws ParseException if a CRS used in this test can not be parsed.
+     * @throws FactoryException if the operation can not be created.
+     * @throws TransformException if an error occurred while converting the test points.
+     */
+    @Test
+    public void testIndirectDatumShift() throws ParseException, FactoryException, TransformException
{
+        final CoordinateReferenceSystem sourceCRS = parse(
+                "PROJCS[“RGF93 / Lambert-93”, "
+                + "GEOGCS[“RGF93”, "
+                +   "DATUM[“Reseau Geodesique Francais 1993”, "
+                +     "SPHEROID[“GRS 1980”, 6378137, 298.257222101], "
+                +     "TOWGS84[0,0,0,0,0,0,0]], "
+                +     "PRIMEM[“Greenwich”,0], "
+                +     "UNIT[“degree”, 0.0174532925199433]], "
+                +   "PROJECTION[“Lambert_Conformal_Conic_2SP”], "
+                +   "PARAMETER[“standard_parallel_1”, 49], "
+                +   "PARAMETER[“standard_parallel_2”, 44], "
+                +   "PARAMETER[“latitude_of_origin”, 46.5], "
+                +   "PARAMETER[“central_meridian”, 3], "
+                +   "PARAMETER[“false_easting”, 700000], "
+                +   "PARAMETER[“false_northing”, 6600000], "
+                +   "UNIT[“metre”,1], "
+                +   "AUTHORITY[“EPSG”,“2154”]]");
+
+        final CoordinateReferenceSystem targetCRS = parse(
+                "PROJCS[“Amersfoort / RD New”, "
+                + "GEOGCS[“Amersfoort”, "
+                +   "DATUM[“Amersfoort”, "
+                +     "SPHEROID[“Bessel 1841”, 6377397.155, 299.1528128], "
+                +     "TOWGS84[565.417, 50.3319, 465.552, -0.398957, 0.343988, -1.8774, 4.0725]],
"
+                +     "PRIMEM[“Greenwich”,0], "
+                +     "UNIT[“degree”,0.0174532925199433]], "
+                +   "PROJECTION[“Oblique_Stereographic”], "
+                +   "PARAMETER[“latitude_of_origin”, 52.15616055555555], "
+                +   "PARAMETER[“central_meridian”, 5.38763888888889], "
+                +   "PARAMETER[“scale_factor”, 0.9999079], "
+                +   "PARAMETER[“false_easting”, 155000], "
+                +   "PARAMETER[“false_northing”, 463000], "
+                +   "UNIT[“metre”,1], "
+                +   "AUTHORITY[“EPSG”,“28992”]]");
+        /*
+         * Transform a point as a way to verify that a datum shift is applied.
+         * If no datum shift is applied, the point will be at 191 metres from
+         * expected value.
+         */
+        final CoordinateOperation operation = finder.createOperation(sourceCRS, targetCRS);
+        tolerance = LINEAR_TOLERANCE;
+        transform = operation.getMathTransform();
+        verifyTransform(new double[] {926713.702, 7348947.026},
+                        new double[] {220798.684,  577583.801});        // With datum shift
through WGS84.
+                        //            220762.487,  577396.040           // Without datum
shift.
+        validate();
+        /*
+         * The accuracy should tell that the datum shift is indirect (through WGS 84).
+         * However the value may differ depending on whether EPSG database has been
+         * used or not, because it depends on whether the datum have been completed
+         * with domain of validity.
+         */
+        final double accuracy = CRS.getLinearAccuracy(operation);
+        if (accuracy != PositionalAccuracyConstant.UNKNOWN_ACCURACY) {
+            assertEquals("accuracy", PositionalAccuracyConstant.INDIRECT_SHIFT_ACCURACY,
accuracy, STRICT);
+        }
+    }
+
+    /**
      * Tests that an exception is thrown on attempt to grab a transformation between incompatible
vertical CRS.
      *
      * @throws FactoryException if an exception other than the expected one occurred.


Mime
View raw message