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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 47e1f0fbf9 Add a test case for avoiding a regression in the 
transformations from NAD27 to WGS84. 
https://issues.apache.org/jira/browse/SIS-638
47e1f0fbf9 is described below

commit 47e1f0fbf99e82b64c89273c6d2b0eaf5f741084
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Oct 28 12:17:34 2026 +0100

    Add a test case for avoiding a regression in the transformations from NAD27 
to WGS84.
    https://issues.apache.org/jira/browse/SIS-638
---
 .../operation/CoordinateOperationRegistry.java     |  4 +-
 .../operation/DefaultConcatenatedOperation.java    | 23 +++---
 .../operation/CoordinateOperationRegistryTest.java | 85 ++++++++++++++++++----
 .../test/org/apache/sis/test/LoggingWatcher.java   | 17 +++++
 4 files changed, 105 insertions(+), 24 deletions(-)

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 ffe66b1401..b963d0f8d0 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
@@ -540,7 +540,7 @@ class CoordinateOperationRegistry {
                      * If at least one operation remains, we are done.
                      */
                     if (decompose.source | decompose.target) {
-                        for (int i=operations.size(); --i >= 0;) {
+                        for (int i = operations.size(); --i >= 0;) {
                             CoordinateOperation operation = operations.get(i);
                             operation = propagateVertical(sourceCRS, 
targetCRS, operation, decompose);
                             if (operation != null) {
@@ -849,7 +849,7 @@ class CoordinateOperationRegistry {
     private CoordinateOperation[] getSteps(final ConcatenatedOperation 
operation, final boolean inverse)
             throws NoninvertibleTransformException, FactoryException
     {
-        final var steps = 
operation.getOperations().toArray(CoordinateOperation[]::new);
+        final CoordinateOperation[] steps = 
operation.getOperations().toArray(CoordinateOperation[]::new);
         CoordinateReferenceSystem previous = operation.getSourceCRS();
         for (int i=0; i<steps.length; i++) {
             final CoordinateOperation step = steps[i];
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
index 8006da489e..da605cb36f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
@@ -217,14 +217,14 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
      * <h4>How coordinate operation accuracy is determined</h4>
      * If {@code setAccuracy} is {@code true}, then this method copies 
accuracy information found in the single
      * {@link Transformation} instance. This method ignores instances of other 
kinds for the following reason:
-     * some {@link Conversion} instances declare an accuracy, which is 
typically close to zero. If a concatenated
-     * operation contains such conversion together with a transformation with 
unknown accuracy, then we do not want
-     * to declare "0 meter" as the concatenated operation accuracy; it would 
be a false information.
+     * some {@link Conversion} instances declare an accuracy, which is 
typically close to zero.
+     * If a concatenated operation contains such conversion together with a 
transformation with unknown accuracy,
+     * we do not want to declare "0 meter" as the concatenated operation 
accuracy; it would be a false information.
      * Another reason is that a concatenated operation typically contains an 
arbitrary number of conversions,
      * but only one transformation. So considering only transformations 
usually means to pickup only one operation
      * in the given {@code operations} list, which make things clearer.
      *
-     * <h5>Note</h5>
+     * <h5>Compliance with <abbr>ISO</abbr> standard</h5>
      * According ISO 19111, the accuracy attribute is allowed only for 
transformations. However, this restriction
      * is not enforced everywhere. For example, the EPSG database declares an 
accuracy of 0 meter for conversions,
      * which is conceptually exact. In this class we are departing from strict 
interpretation of the specification
@@ -304,12 +304,14 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
             } else {
                 /*
                  * We can skip identity transforms only if the operation is a 
conversion.
-                 * We do not want to skip the tests for transformations, 
because there is
-                 * may a change of datum and we want to keep the positional 
accuracy metadata.
-                 *
-                 * Note: operation (source, target) may be in reverse order, 
but it should be taken as metadata.
+                 * We do not want to skip the step for transformations, 
because it may be
+                 * a change of datum and we want to keep the positional 
accuracy metadata.
+                 * We also need to ensure compatible source and target CRSs, 
otherwise the
+                 * `verifyStepChaining(…)` method may declare the operation 
chain invalid.
                  */
-                if (!(step.isIdentity() && step instanceof Conversion)) {
+                if (!(step.isIdentity() && op instanceof Conversion
+                        && Utilities.deepEquals(source, target, 
ComparisonMode.APPROXIMATE)))
+                {
                     flattened.add(op);
                 }
                 previous = target;          // For next iteration cycle.
@@ -345,6 +347,9 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
 
     /**
      * Verifies if a step of a concatenated operation can be chained after the 
previous step.
+     * The source <abbr>CRS</abbr> should be equivalent to the target 
<abbr>CRS</abbr> of the
+     * previous step, but this method accepts also swapped (<var>target</var>, 
<var>source</var>)
+     * <abbr>CRS</abbr> pair.
      *
      * @param  properties  user-specified properties (for the locale of error 
message), or {@code null} if none.
      * @param  step        zero-based index of the operation step, used only 
in case an exception it thrown.
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 0af8b32bad..f75463460f 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
@@ -27,7 +27,9 @@ import org.opengis.referencing.operation.SingleOperation;
 import org.opengis.referencing.operation.ConcatenatedOperation;
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.internal.shared.Formulas;
@@ -42,6 +44,8 @@ import org.junit.jupiter.api.TestInstance;
 import static org.junit.jupiter.api.Assertions.*;
 import org.junit.jupiter.api.parallel.Execution;
 import org.junit.jupiter.api.parallel.ExecutionMode;
+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 static org.apache.sis.test.TestCase.assumeConnectionToEPSG;
@@ -51,7 +55,7 @@ import org.opengis.metadata.Identifier;
 
 
 /**
- * Tests {@link CoordinateOperationRegistry} using the EPSG geodetic dataset.
+ * Tests {@link CoordinateOperationRegistry} using the <abbr>EPSG</abbr> 
geodetic dataset.
  * If no EPSG geodetic dataset is available in the running environment, then 
tests are skipped.
  * For tests without the need of an EPSG database, see {@link 
CoordinateOperationFinderTest}.
  *
@@ -63,9 +67,13 @@ import org.opengis.metadata.Identifier;
  *       that does not go through WGS84. Furthermore, since the EPSG database 
defines (λ,φ) axis order in
  *       addition to the usual (φ,λ) order for the target CRS, this tests 
allows us to verify we can find
  *       this operation despite different axis order.</li>
+ *   <li>All operations between <q>WGS 84</q> (EPSG:4326) and <q>NAD27</q> 
(EPSG:4267).
+ *       This is the opposite direction of the operations defined in the 
<abbr>EPSG</abbr> database.
+ *       <abbr>SIS</abbr> should nevertheless be able to create them by 
creating the inverses of the
+ *       operations from EPSG:4267 to EPSG:4326. We merely verifies that no 
exception is thrown.</li>
  * </ul>
  *
- * The operations are tested with various axis order and dimension in source 
and target CRS.
+ * The operations are tested with various axis order and dimension in source 
and target <abbr>CRS</abbr>s.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -73,6 +81,12 @@ import org.opengis.metadata.Identifier;
 @Execution(ExecutionMode.SAME_THREAD)
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase {
+    /**
+     * A JUnit extension for listening to log events.
+     */
+    @RegisterExtension
+    public final LoggingWatcher loggings;
+
     /**
      * The transformation factory to use for testing.
      */
@@ -101,6 +115,7 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
      * @throws FactoryException if an error occurred while creating the 
factory to be tested.
      */
     public CoordinateOperationRegistryTest() throws ParseException, 
FactoryException {
+        loggings = new LoggingWatcher(AbstractCoordinateOperation.LOGGER);
         crsFactory = CRS.getAuthorityFactory("EPSG");
         assumeConnectionToEPSG(crsFactory instanceof 
CoordinateOperationAuthorityFactory);
         factory = new DefaultCoordinateOperationFactory();
@@ -120,6 +135,7 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
     @BeforeEach
     public void reset() {
         super.reset();
+        loggings.clear();
     }
 
     /**
@@ -143,6 +159,7 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
 
     /**
      * Tests <q>NTF (Paris) to WGS 84 (1)</q> operation with source and target 
CRS conform to EPSG definitions.
+     * This operation implies a longitude rotation followed by a geocentric 
translation in the geographic domain.
      *
      * @throws ParseException if a CRS used in this test cannot be parsed.
      * @throws FactoryException if the operation cannot be created.
@@ -175,12 +192,13 @@ public final class CoordinateOperationRegistryTest 
extends MathTransformTestCase
         verifyTransform(new double[] {54.271680278,  0.098269657},      // in 
grads east of Paris
                         new double[] {48.844443528,  2.424952028});     // in 
degrees east of Greenwich
         validate();
+        loggings.assertNoUnexpectedLog();
     }
 
     /**
-     * Tests <q>NTF (Paris) to WGS 84 (1)</q> operation with normalized source 
and target CRS.
-     * {@link CoordinateOperationRegistry} should be able to find the 
operation despite the difference
-     * in axis order an units.
+     * Tests <q>NTF (Paris) to WGS 84 (1)</q> operation with normalized source 
and target <abbr>CRS</abbr>s.
+     * {@link CoordinateOperationRegistry} should be able to find the 
operation despite the difference in axis
+     * order an units.
      *
      * @throws ParseException if a CRS used in this test cannot be parsed.
      * @throws FactoryException if the operation cannot be created.
@@ -207,6 +225,7 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
         verifyTransform(new double[] {0.088442691, 48.844512250},      // in 
degrees east of Paris
                         new double[] {2.424952028, 48.844443528});     // in 
degrees east of Greenwich
         validate();
+        loggings.assertNoUnexpectedLog();
     }
 
     /**
@@ -236,6 +255,7 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
         verifyTransform(new double[] {2.424952028, 48.844443528},      // in 
degrees east of Greenwich
                         new double[] {0.098269657, 54.271680278});     // in 
grads east of Paris
         validate();
+        loggings.assertNoUnexpectedLog();
     }
 
     /**
@@ -270,11 +290,12 @@ public final class CoordinateOperationRegistryTest 
extends MathTransformTestCase
         verifyTransform(new double[] {54.271680278,  0.098269657, 20.00},      
// in grads east of Paris
                         new double[] {48.844443528,  2.424952028, 63.15});     
// in degrees east of Greenwich
         validate();
+        loggings.assertNoUnexpectedLog();
     }
 
     /**
-     * Tests <q>NTF (Paris) to WGS 84 (1)</q> operation with three-dimensional 
source and target CRS
-     * having different axis order and units than the ones declared in the 
EPSG dataset.
+     * Tests <q>NTF (Paris) to WGS 84 (1)</q> operation with three-dimensional 
source and target <abbr>CRS</abbr>s
+     * having different axis order and units than the ones declared in the 
<abbr>EPSG</abbr> geodetic dataset.
      *
      * @throws ParseException if a CRS used in this test cannot be parsed.
      * @throws FactoryException if the operation cannot be created.
@@ -304,6 +325,7 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
         verifyTransform(new double[] {0.088442691, 48.844512250, 20.00},      
// in degrees east of Paris
                         new double[] {2.424952028, 48.844443528, 63.15});     
// in degrees east of Greenwich
         validate();
+        loggings.assertNoUnexpectedLog();
     }
 
     /**
@@ -313,11 +335,11 @@ public final class CoordinateOperationRegistryTest 
extends MathTransformTestCase
      * @param  isEPSG  {@code true} if the coordinate operation is expected to 
contain EPSG identifiers.
      */
     private static void verifyNTF(final CoordinateOperation operation, final 
String domain, final boolean isEPSG) {
-        assertInstanceOf(ConcatenatedOperation.class, operation, "Operation 
should have two steps.");
-        final List<? extends CoordinateOperation> steps = 
((ConcatenatedOperation) operation).getOperations();
+        final List<? extends CoordinateOperation> steps =
+                assertInstanceOf(ConcatenatedOperation.class, operation, 
"Operation should have two steps.").getOperations();
         assertEquals(2, steps.size(), "Operation should have two steps.");
-        final SingleOperation step1 = (SingleOperation) steps.get(0);
-        final SingleOperation step2 = (SingleOperation) steps.get(1);
+        final SingleOperation step1 = assertInstanceOf(SingleOperation.class, 
steps.get(0));
+        final SingleOperation step2 = assertInstanceOf(SingleOperation.class, 
steps.get(1));
         if (isEPSG) {
             assertEpsgNameAndIdentifierEqual("NTF (Paris) to WGS 84 (1)", 
8094, operation);
             assertEpsgNameAndIdentifierEqual("NTF (Paris)",               
4807, operation.getSourceCRS());
@@ -366,7 +388,7 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
      * instead of EPSG:5489. Both are <q>RGAF09</q>, but the former use 
(longitude, latitude) axis
      * order instead of the usual (latitude, longitude) order. The source CRS 
stay fixed to EPSG:4625.
      *
-     * @throws FactoryException if an error occurred while creating a CRS or 
operation.
+     * @throws FactoryException if an error occurred while creating a 
<abbr>CRS</abbr> or an operation.
      */
     @Test
     public void testFindDespiteDifferentAxisOrder() throws FactoryException {
@@ -381,7 +403,7 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
         targetCRS = crsFactory.createGeographicCRS("EPSG:7086");
         operation = createOperation(sourceCRS, targetCRS);
         assertEpsgNameWithoutIdentifierEqual("Martinique 1938 to RGAF09 (1)", 
operation);
-        final ParameterValueGroup p = ((SingleOperation) 
operation).getParameterValues();
+        final ParameterValueGroup p = assertInstanceOf(SingleOperation.class, 
operation).getParameterValues();
         /*
          * Values below are copied from EPSG geodetic dataset 9.1. They may 
need
          * to be adjusted if a future version of EPSG dataset modify those 
values.
@@ -394,5 +416,42 @@ public final class CoordinateOperationRegistryTest extends 
MathTransformTestCase
         assertEquals( -0.8837, p.parameter("Z-axis rotation")   
.doubleValue());
         assertEquals( 14.1012, p.parameter("Scale difference")  
.doubleValue());
         assertEquals(  0.1,    CRS.getLinearAccuracy(operation));
+        loggings.assertNoUnexpectedLog();
+    }
+
+    /**
+     * 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.
+     * <abbr>SIS</abbr> should nevertheless be able to create them by creating 
the inverses of the
+     * operations from EPSG:4267 to EPSG:4326.
+     *
+     * @throws FactoryException if an error occurred while creating a 
<abbr>CRS</abbr> or an operation.
+     * @throws NoninvertibleTransformException if a transform cannot be 
inverted.
+     *
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-638";>SIS-638</a>
+     */
+    @Test
+    public void testInverseOperations() throws FactoryException, 
NoninvertibleTransformException {
+        registry.stopAtFirst = false;
+        CoordinateReferenceSystem source = 
crsFactory.createGeographicCRS("EPSG:4326");
+        CoordinateReferenceSystem target = 
crsFactory.createGeographicCRS("EPSG:4267");
+        List<CoordinateOperation> operations = 
registry.createOperations(source, target);
+        /*
+         * The EPSG database version 13.101 contains 78 operations from 
EPSG:4267 to EPSG:4326,
+         * but many of them require some NADCON or NTv2 files which may not be 
available on the
+         * machine running this test. However, 30 operations do not require 
any file and should
+         * be created in all cases.
+         */
+        final int count = operations.size();
+        assertTrue(count >= 30, () -> "Found " + count + " operations, 
expected more than that.");
+        for (CoordinateOperation operation : operations) {
+            final String name = operation.getName().getCode();
+            transform = operation.getMathTransform();
+            MathTransform inverse = transform.inverse();
+            assertNotEquals(transform, inverse, name);
+            assertSame(transform, inverse.inverse(), name);
+        }
+        loggings.skipLogsWhile((message) -> message.contains("NADCON") || 
message.contains("NTv2"));
+        loggings.assertNoUnexpectedLog();
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/LoggingWatcher.java 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/LoggingWatcher.java
index 5bcf2367f8..6e46d82825 100644
--- 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/LoggingWatcher.java
+++ 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/LoggingWatcher.java
@@ -23,6 +23,7 @@ import java.util.LinkedList;
 import java.util.ConcurrentModificationException;
 import java.util.Objects;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 import java.util.logging.Filter;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -298,6 +299,22 @@ public final class LoggingWatcher implements 
BeforeEachCallback, AfterEachCallba
         messages.add(new Message(formatter.formatMessage(record), 
record.getThrown()));
     }
 
+    /**
+     * Skips the next logs until the given condition become false.
+     * The filter receive a lot message in argument and returns whether that 
log should be skipped.
+     *
+     * @param  filter  a predicate which, given a log message, returns whether 
to skip that log.
+     */
+    public synchronized void skipLogsWhile(final Predicate<String> filter) {
+        Message message;
+        while ((message = messages.peek()) != null) {
+            if (!filter.test(message.text)) break;
+            if (messages.remove() != message) {
+                throw new ConcurrentModificationException();
+            }
+        }
+    }
+
     /**
      * Skips the next log messages if it contains all the given keywords.
      * This method is used instead of {@link 
#assertNextLogContains(String...)} when a log message may or

Reply via email to