This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 25e567e3789ad63bbba46495444a75132eef6ac3
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Nov 5 16:49:04 2026 +0100

    Fix a case where SIS did not find the geocentric translation defined in the 
EPSG database.
---
 .../referencing/factory/IdentifiedObjectSet.java   | 20 ++---
 .../sis/referencing/factory/package-info.java      |  2 +-
 .../referencing/factory/sql/EPSGDataAccess.java    |  2 +
 .../operation/CoordinateOperationFinder.java       |  2 +-
 .../operation/CoordinateOperationRegistry.java     | 93 +++++++++++++++-------
 .../operation/CoordinateOperationFinderTest.java   | 22 ++---
 .../operation/CoordinateOperationRegistryTest.java | 59 ++++++++++++++
 7 files changed, 146 insertions(+), 54 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
index 3658f309c1..1d676e0d98 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
@@ -377,8 +377,8 @@ public class IdentifiedObjectSet<T extends 
IdentifiedObject> extends AbstractSet
 
     /**
      * Returns an iterator over the objects in this set. If the iteration 
encounter any kind of
-     * {@link FactoryException} other than {@link NoSuchIdentifierException}, 
then the exception
-     * will be re-thrown as an unchecked {@link BackingStoreException}.
+     * {@link FactoryException} other than {@linkplain #isRecoverableFailure 
recoverable failures},
+     * then the exception will be re-thrown as an unchecked {@link 
BackingStoreException}.
      *
      * <p>This iterator is <strong>not</strong> thread safe – iteration should 
be done in a single thread.
      * However, the iterator is robust to concurrent changes in {@code 
IdentifiedObjectSet} during iteration.</p>
@@ -468,7 +468,7 @@ public class IdentifiedObjectSet<T extends 
IdentifiedObject> extends AbstractSet
      */
     public void resolve(int n) throws FactoryException {
         if (n > 0) try {
-            for (final Iterator<T> it=iterator(); it.hasNext(); it.next()) {
+            for (final Iterator<T> it = iterator(); it.hasNext(); it.next()) {
                 if (--n == 0) {
                     break;
                 }
@@ -485,10 +485,8 @@ public class IdentifiedObjectSet<T extends 
IdentifiedObject> extends AbstractSet
      * <ol>
      *   <li>An identifier allocated by the authority given by
      *       <code>{@linkplain #factory}.{@linkplain 
GeodeticAuthorityFactory#getAuthority() getAuthority()}</code>.</li>
-     *   <li>The first {@linkplain 
org.apache.sis.referencing.AbstractIdentifiedObject#getIdentifiers() object 
identifier},
-     *       regardless its authority.</li>
-     *   <li>The first {@linkplain 
org.apache.sis.referencing.AbstractIdentifiedObject#getName() object name},
-     *       regardless its authority.</li>
+     *   <li>The first {@linkplain IdentifiedObject#getIdentifiers() object 
identifier}, regardless its authority.</li>
+     *   <li>The first {@linkplain IdentifiedObject#getName() object name}, 
regardless its authority.</li>
      * </ol>
      *
      * Subclasses may override this method if they want to use a different 
identifiers.
@@ -524,15 +522,13 @@ public class IdentifiedObjectSet<T extends 
IdentifiedObject> extends AbstractSet
     /**
      * Returns {@code true} if the specified exception should be handled as a 
recoverable failure.
      * This method is invoked during the iteration process if the factory 
failed to create some objects.
-     * If this method returns {@code true} for the given exception, then the 
exception will be logged
-     * at {@link Level#WARNING}. If this method returns {@code false}, then 
the exception will be re-thrown
-     * as a {@link BackingStoreException}.
+     * If this method returns {@code true} for the given exception, then the 
exception will be logged at {@link Level#WARNING}.
+     * If this method returns {@code false}, then the exception will be 
re-thrown as a {@link BackingStoreException}.
      *
      * <p>The default implementation applies the following rules:</p>
      * <ul>
      *   <li>If {@link NoSuchAuthorityCodeException}, returns {@code false} 
since failure to find a code declared
-     *       in the collection would be an inconsistency. Note that this 
exception is a subtype of
-     *       {@code NoSuchIdentifierException}, so it must be tested before 
the last case below.</li>
+     *       in the collection would be an inconsistency.</li>
      *   <li>If {@link NoSuchIdentifierException}, returns {@code true} 
because this exception is caused
      *       by an attempt to create a parameterized transform for an 
unimplemented operation.</li>
      *   <li>If {@link MissingFactoryResourceException}, returns {@code 
true}.</li>
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/package-info.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/package-info.java
index 249d07862b..aa0e7d5750 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/package-info.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/package-info.java
@@ -56,7 +56,7 @@
  * </table>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.6
+ * @version 1.8
  * @since SIS 0.6
  */
 package org.apache.sis.referencing.factory;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index 80758d6ce5..914df7cc07 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -3689,6 +3689,8 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         /*
          * Before to return the set, tests the creation of 1 object in order 
to report early (i.e. now)
          * any problems with SQL statements. Remaining operations will be 
created only when first needed.
+         * Do nothing if only metadata are requested because `set.resolve(1)` 
would create an instance of
+         * `DeferredCoordinateOperation`, which does not really answer the 
question.
          */
         if (!Semaphores.METADATA_ONLY.get()) {
             set.resolve(1);
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index 1a7b9032ce..d6e2f42efe 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
@@ -117,7 +117,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.7
+ * @version 1.8
  *
  * @see 
DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, 
CoordinateReferenceSystem, CoordinateOperationContext)
  *
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index 5e583753f5..f5ad1087b4 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
@@ -383,6 +383,66 @@ class CoordinateOperationRegistry {
         return codes;
     }
 
+    /**
+     * Returns whether it is presumed easy for {@link IdentifiedObjectFinder} 
to perform a full search
+     * (ignoring axis order) for the given CRS. An important criterion is to 
avoid all CRS containing
+     * a coordinate operation (in particular projected CRS), because they are 
difficult to search.
+     *
+     * <p>We allow extensive search of geographic CRS because the EPSG 
database has some geographic CRS
+     * defined with both (longitude, latitude) and (latitude, longitude) axis 
order. But operations may
+     * be defined for only one axis order. So if the user specified the CRS 
with (longitude, latitude)
+     * axis order, we want to check also the coordinate operations using 
(latitude, longitude) axis order
+     * because they may be the only ones available.</p>
+     */
+    private static boolean isEasySearch(final CoordinateReferenceSystem crs) {
+        if (crs instanceof DerivedCRS) {
+            return false;
+        }
+        if (crs instanceof CompoundCRS) {
+            for (CoordinateReferenceSystem c : ((CompoundCRS) 
crs).getComponents()) {
+                if (!isEasySearch(c)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns a copy of the given list where the first element (if it exists) 
is a fully constructed operation.
+     * This method tries to resolve {@link DeferredCoordinateOperation} 
instances in iteration order until one
+     * element can be fully constructed. All remaining elements are left 
deferred. The purpose is to determine
+     * whether the collection is effectively empty because of missing 
resources such as <abbr>NADCON</abbr> grids.
+     *
+     * @param  authoritatives  the operations returned by the authority 
factory.
+     * @return a copy of {@code authoritatives} where the first element, if 
present, is resolved.
+     */
+    private List<CoordinateOperation> resolveFirst(final 
Collection<CoordinateOperation> authoritatives)
+            throws FactoryException
+    {
+        if (Containers.isNullOrEmpty(authoritatives)) {
+            return List.of();
+        }
+        final var resolved = new 
ArrayList<CoordinateOperation>(authoritatives.size());
+        for (CoordinateOperation operation : authoritatives) {
+            try {
+                if (resolved.isEmpty() && operation instanceof 
DeferredCoordinateOperation) {
+                    assert Semaphores.METADATA_ONLY.get();
+                    try {
+                        Semaphores.METADATA_ONLY.clear();
+                        operation = ((DeferredCoordinateOperation) 
operation).create();
+                    } finally {
+                        Semaphores.METADATA_ONLY.set();
+                    }
+                }
+                resolved.add(operation);
+            } catch (MissingFactoryResourceException e) {
+                log(null, e);
+            }
+        }
+        return resolved;
+    }
+
     /**
      * Strategies for decomposing a three-dimensional CRS into components that 
may be present in EPSG database.
      * The database may have no entries for a coordinate operation between a 
pair of 3D CRS with (x,y,z) axes,
@@ -448,31 +508,6 @@ class CoordinateOperationRegistry {
         }
     }
 
-    /**
-     * Returns whether it is presumed easy for {@link IdentifiedObjectFinder} 
to perform a full search
-     * (ignoring axis order) for the given CRS. An important criterion is to 
avoid all CRS containing
-     * a coordinate operation (in particular projected CRS), because they are 
difficult to search.
-     *
-     * <p>We allow extensive search of geographic CRS because the EPSG 
database has some geographic CRS
-     * defined with both (longitude, latitude) and (latitude, longitude) axis 
order. But operations may
-     * be defined for only one axis order. So if the user specified the CRS 
with (longitude, latitude)
-     * axis order, we want to check also the coordinate operations using 
(latitude, longitude) axis order
-     * because they may be the only ones available.</p>
-     */
-    private static boolean isEasySearch(final CoordinateReferenceSystem crs) {
-        if (crs instanceof DerivedCRS) {
-            return false;
-        }
-        if (crs instanceof CompoundCRS) {
-            for (CoordinateReferenceSystem c : ((CompoundCRS) 
crs).getComponents()) {
-                if (!isEasySearch(c)) {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
     /**
      * Finds or infers operations for conversions or transformations between 
two coordinate reference systems.
      * {@code CoordinateOperationRegistry} implements the <i>late-binding</i> 
approach (see definition
@@ -589,7 +624,7 @@ class CoordinateOperationRegistry {
     {
         final List<String> sources = findCode(sourceCRS); if 
(sources.isEmpty()) return null;
         final List<String> targets = findCode(targetCRS); if 
(targets.isEmpty()) return null;
-        final List<CoordinateOperation> operations = new ArrayList<>();
+        final var operations = new ArrayList<CoordinateOperation>();
         boolean foundDirectOperations = false;
         boolean useDeprecatedOperations = false;
         for (final String sourceID : sources) {
@@ -616,8 +651,8 @@ class CoordinateOperationRegistry {
                 try {
                     Collection<CoordinateOperation> authoritatives;
                     try {
-                        authoritatives = 
registry.createFromCoordinateReferenceSystemCodes(sourceID, targetID);
-                        final boolean inverse = 
Containers.isNullOrEmpty(authoritatives);
+                        authoritatives = 
resolveFirst(registry.createFromCoordinateReferenceSystemCodes(sourceID, 
targetID));
+                        final boolean inverse = authoritatives.isEmpty();
                         if (inverse) {
                             /*
                              * No operation from `source` to `target` 
available. But maybe there is an inverse operation.
@@ -1102,7 +1137,7 @@ class CoordinateOperationRegistry {
                                                   final Decomposition 
decompose)
             throws NoninvertibleTransformException, FactoryException
     {
-        final List<CoordinateOperation> operations = new ArrayList<>();
+        final var operations = new ArrayList<CoordinateOperation>();
         if (operation instanceof ConcatenatedOperation) {
             operations.addAll(Arrays.asList(getSteps((ConcatenatedOperation) 
operation, false)));
         } else {
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
index c957485e7d..701fb81855 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
@@ -217,7 +217,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
          * We could mix WKT 1 and WKT 2 elements (SIS allows that), but we 
nevertheless use WKT 1 for the whole
          * string as a matter of principle.
          */
-        final GeographicCRS sourceCRS = (GeographicCRS) parse(
+        final GeographicCRS sourceCRS = assertInstanceOf(GeographicCRS.class, 
parse(
                 "GEOGCS[“NAD27”,\n" +
                 "  DATUM[“North American Datum 1927”,\n" +
                 "    SPHEROID[“Clarke 1866”, 6378206.4, 294.9786982138982],\n" 
+
@@ -226,7 +226,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
                 "  UNIT[“degree”, 0.017453292519943295],\n" +
                 "  AXIS[“Latitude (φ)”, NORTH],\n" +
                 "  AXIS[“Longitude (λ)”, EAST],\n" +
-                "  AUTHORITY[“EPSG”, “4267”]]");
+                "  AUTHORITY[“EPSG”, “4267”]]"));
 
         testGeocentricTranslationInGeographicDomain("Geocentric translations 
(geog2D domain)", sourceCRS, CommonCRS.WGS84.geographic());
         testGeocentricTranslationInGeographicDomain("Geocentric translations 
(geog3D domain)", sourceCRS, CommonCRS.WGS84.geographic3D());
@@ -242,7 +242,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
      */
     @Test
     public void testGeocentricTranslationInGeographic3D() throws 
ParseException, FactoryException, TransformException {
-        final GeographicCRS sourceCRS = (GeographicCRS) parse(
+        final GeographicCRS sourceCRS = assertInstanceOf(GeographicCRS.class, 
parse(
                 "GeographicCRS[“NAD27”,\n" +
                 "  Datum[“North American Datum 1927”,\n" +
                 "    Ellipsoid[“Clarke 1866”, 6378206.4, 
294.9786982138982],\n" +
@@ -250,7 +250,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
                 "  CS[ellipsoidal, 3],\n" +
                 "    Axis[“Latitude (φ)”, NORTH, Unit[“degree”, 
0.017453292519943295]],\n" +
                 "    Axis[“Longitude (λ)”, EAST, Unit[“degree”, 
0.017453292519943295]],\n" +
-                "    Axis[“Height (h)”, UP, Unit[“m”, 1]]]");
+                "    Axis[“Height (h)”, UP, Unit[“m”, 1]]]"));
 
         testGeocentricTranslationInGeographicDomain("Geocentric translations 
(geog3D domain)",
                 sourceCRS, CommonCRS.WGS84.geographic3D());
@@ -464,7 +464,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
         assertEquals("TM", operation.getName().getCode());
         assertInstanceOf(Conversion.class, operation);
 
-        final ParameterValueGroup param = ((SingleOperation) 
operation).getParameterValues();
+        final ParameterValueGroup param = 
assertInstanceOf(SingleOperation.class, operation).getParameterValues();
         assertEquals(6370997, param.parameter("semi_major"        
).doubleValue());
         assertEquals(6370997, param.parameter("semi_minor"        
).doubleValue());
         assertEquals(     50, 
param.parameter("latitude_of_origin").doubleValue());
@@ -549,7 +549,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
     }
 
     /**
-     * Tests a transformation between two CRS for which no direct bursa-wolf 
parameters are defined.
+     * Tests a transformation between two <abbr>CRS</abbr>s 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 cannot be parsed.
@@ -738,7 +738,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
      */
     @Test
     public void testGeographic4D_to_2D() throws FactoryException, 
TransformException {
-        // NOTE: make sure that the 'sourceCRS' below is not equal to any 
other 'sourceCRS' created in this class.
+        // NOTE: make sure that the `sourceCRS` below is not equal to any 
other `sourceCRS` created in this class.
         final CompoundCRS   sourceCRS = compound("Test4D", 
CommonCRS.WGS84.geographic3D(), CommonCRS.Temporal.UNIX.crs());
         final GeographicCRS targetCRS = CommonCRS.WGS84.geographic();
         final CoordinateOperation operation = 
finder().createOperation(sourceCRS, targetCRS);
@@ -782,7 +782,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
         assertEquals("Axis changes", operation.getName().getCode());
         assertInstanceOf(Conversion.class, operation);
 
-        final ParameterValueGroup parameters = ((SingleOperation) 
operation).getParameterValues();
+        final ParameterValueGroup parameters = 
assertInstanceOf(SingleOperation.class, operation).getParameterValues();
         assertEquals("Geographic3D to 2D conversion", 
parameters.getDescriptor().getName().getCode());
         assertTrue(parameters.values().isEmpty());
 
@@ -823,7 +823,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
         assertEquals("Axis changes", operation.getName().getCode());
         assertInstanceOf(Conversion.class, operation);
 
-        final ParameterValueGroup parameters = ((SingleOperation) 
operation).getParameterValues();
+        final ParameterValueGroup parameters = 
assertInstanceOf(SingleOperation.class, operation).getParameterValues();
         assertEquals("Geographic2D to 3D conversion", 
parameters.getDescriptor().getName().getCode());
         assertEquals(0, parameters.parameter("height").doubleValue());
 
@@ -894,7 +894,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
      */
     @Test
     public void testGeographic4D_to_EllipsoidalHeight() throws 
FactoryException, TransformException {
-        // NOTE: make sure that the 'sourceCRS' below is not equal to any 
other 'sourceCRS' created in this class.
+        // NOTE: make sure that the `sourceCRS` below is not equal to any 
other `sourceCRS` created in this class.
         final CompoundCRS sourceCRS = compound("Test4D", 
CommonCRS.WGS84.geographic3D(), CommonCRS.Temporal.JULIAN.crs());
         final VerticalCRS targetCRS = CommonCRS.Vertical.ELLIPSOIDAL.crs();
         final CoordinateOperation operation = 
finder().createOperation(sourceCRS, targetCRS);
@@ -1002,7 +1002,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
      */
     @Test
     public void testGeographic3D_to_4D() throws FactoryException, 
TransformException {
-        // NOTE: make sure that the 'sourceCRS' below is not equal to any 
other 'sourceCRS' created in this class.
+        // NOTE: make sure that the `sourceCRS` below is not equal to any 
other `sourceCRS` created in this class.
         final CompoundCRS sourceCRS = compound("Test3D", 
CommonCRS.WGS84.geographic(),   CommonCRS.Temporal.UNIX.crs());
         final CompoundCRS targetCRS = compound("Test4D", 
CommonCRS.WGS84.geographic3D(), CommonCRS.Temporal.MODIFIED_JULIAN.crs());
         final CoordinateOperation operation = 
finder().createOperation(sourceCRS, targetCRS);
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationRegistryTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationRegistryTest.java
index f75463460f..17fe6ab0c0 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationRegistryTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationRegistryTest.java
@@ -48,6 +48,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
 import org.apache.sis.test.LoggingWatcher;
 import org.apache.sis.referencing.operation.transform.MathTransformTestCase;
 import static 
org.apache.sis.referencing.Assertions.assertEpsgNameAndIdentifierEqual;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import static org.apache.sis.test.TestCase.assumeConnectionToEPSG;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -419,6 +420,64 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
         loggings.assertNoUnexpectedLog();
     }
 
+    /**
+     * Tests an operation requiring datum shifts. The <abbr>EPSG</abbr> 
database contains coordinate operations
+     * for the forward and reverse operations, but one of them is defined with 
geocentric translation while the
+     * other is defined with datum shift grids. The latter may not be present, 
in which case SIS should fallback
+     * on the inverse of the geocentric translation (a previous SIS version 
did not, causing a 355 meters error).
+     *
+     * @throws ParseException if a CRS used in this test cannot be parsed.
+     * @throws FactoryException if an error occurred while creating a 
<abbr>CRS</abbr> or an operation.
+     * @throws TransformException if an error occurred while converting the 
test points.
+     */
+    @Test
+    public void testDatumShift() throws ParseException, FactoryException, 
TransformException {
+        final CoordinateReferenceSystem sourceCRS = 
CommonCRS.WGS84.normalizedGeographic();
+        final CoordinateReferenceSystem targetCRS = parse(
+                "ProjectedCRS[“NTF Lambert II étendu”,\n" +
+                // Intentionally not the authoritative base for checking that 
the search works anyway.
+                "  BaseGeogCRS[“GCS NTF”,\n" +
+                "    Datum[“NTF”, Ellipsoid[“Clarke_1880_IGN”, 6378249.2, 
293.46602]],\n" +
+                "    Unit[“Degree”, 0.0174532925199433]],\n" +
+                "  Conversion[“Lambert zone II”,\n" +
+                "    Method[“Lambert Conformal Conic”],\n" +
+                "    Parameter[“False Easting”, 600000],\n" +
+                "    Parameter[“False Northing”, 2200000],\n" +
+                "    Parameter[“Central_Meridian”, 2.3372291667],\n" +
+                "    Parameter[“Standard Parallel 1”, 45.8989188889],\n" +  // 
Using degrees instead of grad.
+                "    Parameter[“Standard Parallel 2”, 47.6960144444],\n" +
+                "    Parameter[“Latitude Of Origin”,  46.8]],\n" +
+                "  Unit[“Meter”, 1],\n" +
+                "  Id[“EPSG”, “27582”]]");      // For helping SIS to find the 
authoritative definition faster.
+        /*
+         * Despite the fact that `sourceCRS` is not conform to EPSG 
authoritative definition,
+         * SIS should be able to find the correct definition (equivalent to 
above definition)
+         * and find a datum shift with an accuracy of 2 meters or less 
(compared to 3000 m if
+         * the datum shift is not found).
+         */
+        final CoordinateOperation forward = CRS.findOperation(sourceCRS, 
targetCRS, null);
+        final CoordinateOperation reverse = CRS.findOperation(targetCRS, 
sourceCRS, null);
+        assertTrue((tolerance = CRS.getLinearAccuracy(reverse)) <= 2, () -> 
"Forward accuracy: " + tolerance);
+        assertTrue((tolerance = CRS.getLinearAccuracy(forward)) <= 2, () -> 
"Reverse accuracy: " + tolerance);
+        /*
+         * Transform a building corner near Toulon (La Garde). We keep the 
tolerance declared by the operation.
+         * This is another way to test if a datum shift is applied (if not, 
the error is about 355 meters).
+         */
+        transform = forward.getMathTransform();
+        verifyTransform(new double[] {5.9808769, 43.1338320},   // Point to 
transform.
+                        new double[] {896966.00, 1799238.19});  // Expected 
values.
+        /*
+         * Verify the consistency between the two transforms: forward followed 
by reverse.
+         */
+        transform = MathTransforms.concatenate(forward.getMathTransform(), 
reverse.getMathTransform());
+        verifyTransform(new double[] {5.9808769, 43.1338320},
+                        new double[] {5.9808769, 43.1338320});
+
+        transform = MathTransforms.concatenate(reverse.getMathTransform(), 
forward.getMathTransform());
+        verifyTransform(new double[] {896966.00, 1799238.19},
+                        new double[] {896966.00, 1799238.19});
+    }
+
     /**
      * Tests operations from <q>WGS 84</q> (EPSG:4326) to <q>NAD27</q> 
(EPSG:4267).
      * This is the opposite direction of the operations defined in the 
<abbr>EPSG</abbr> database.

Reply via email to