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.
