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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new b8b11bda55 Fix a wrong detecting of coordinate operation type. The 
previous algortihm was based on whether an accuracy is specified, but got 
confused by the fact that datum ensembles also have accuracy. This commit also 
clarify the case of datum shifts without parameters.
b8b11bda55 is described below

commit b8b11bda559426573572b3d808f0d507a1c061e9
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Sep 4 19:10:19 2025 +0200

    Fix a wrong detecting of coordinate operation type.
    The previous algortihm was based on whether an accuracy is specified,
    but got confused by the fact that datum ensembles also have accuracy.
    This commit also clarify the case of datum shifts without parameters.
---
 .../apache/sis/console/MetadataCommandTest.java    |   1 +
 .../sis/referencing/cs/CoordinateSystems.java      |   3 +
 .../sis/referencing/datum/DatumOrEnsemble.java     |  17 +-
 .../apache/sis/referencing/internal/Resources.java |   5 +
 .../sis/referencing/internal/Resources.properties  |   1 +
 .../referencing/internal/Resources_fr.properties   |   1 +
 .../apache/sis/referencing/operation/CRSPair.java  |   2 +-
 .../operation/CoordinateOperationFinder.java       | 232 +++++++++++++++------
 .../operation/CoordinateOperationRegistry.java     | 108 ++++++----
 .../operation/CoordinateOperationFinderTest.java   |  10 +-
 .../DefaultCoordinateOperationFactoryTest.java     |   2 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |  15 +-
 .../sis/util/resources/Vocabulary.properties       |   3 +-
 .../sis/util/resources/Vocabulary_fr.properties    |   3 +-
 14 files changed, 270 insertions(+), 133 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MetadataCommandTest.java
 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MetadataCommandTest.java
index a2563cb880..6debe66ea7 100644
--- 
a/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MetadataCommandTest.java
+++ 
b/endorsed/src/org.apache.sis.console/test/org/apache/sis/console/MetadataCommandTest.java
@@ -80,6 +80,7 @@ public final class MetadataCommandTest extends 
TestCaseWithLogs {
         verifyNetCDF("<?xml", test.outputBuffer.toString());
         loggings.skipNextLogIfContains("EPSG:6019");    // Warning about 
deprecated EPSG code for datum.
         loggings.skipNextLogIfContains("EPSG:4019");    // Warning about 
deprecated EPSG code for CRS.
+        loggings.skipNextLogIfContains("EPSG:6019");    // In case logs are 
not in above order.
         loggings.assertNoUnexpectedLog();
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
index 61f84e9ee0..825261842a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
@@ -346,6 +346,9 @@ next:   for (final CoordinateSystem cs : targets) {
                                           final CoordinateSystem targetCS)
             throws IllegalArgumentException, IncommensurableException
     {
+        if (sourceCS == targetCS) {     // Quick optimization for a common 
case.
+            return Matrices.createIdentity(sourceCS.getDimension() + 1);
+        }
         if (!Classes.implementSameInterfaces(sourceCS.getClass(), 
targetCS.getClass(), CoordinateSystem.class)) {
             // Above line was a relatively cheap test. Try the more expensive 
test below only if necessary.
             if (!hasAllTargetTypes(sourceCS, targetCS)) {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
index 820ce1cfcc..89ab1e4124 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
@@ -113,9 +113,9 @@ public final class DatumOrEnsemble extends Static {
 
     /**
      * Returns the datum or ensemble of a coordinate operation from 
<var>source</var> to <var>target</var>.
-     * If the two given coordinate reference systems are associated to the 
same datum, then this method returns
-     * the <var>target</var> datum. Otherwise, this method returns the largest 
ensemble which fully contains the
-     * datum or datum ensemble of the other <abbr>CRS</abbr>.
+     * If the two given coordinate reference systems are associated to equal 
datum (ignoring metadata),
+     * then this method returns the <var>target</var> datum. Otherwise, this 
method returns
+     * the largest ensemble which fully contains the datum or datum ensemble 
of the other <abbr>CRS</abbr>.
      * That largest common ensemble is interpreted as the new target of the 
operation result.
      * If none of the <var>source</var> or <var>target</var> datum ensembles 
met the above criteria,
      * then this method returns an empty value.
@@ -222,9 +222,9 @@ public final class DatumOrEnsemble extends Static {
 
     /**
      * Returns the datum or pseudo-datum of the result of an operation between 
the given geodetic <abbr>CRS</abbr>s.
-     * If the two given coordinate reference systems are associated to the 
same datum, then this method returns
-     * the <var>target</var> datum. Otherwise, this method returns a 
pseudo-datum for the largest ensemble which
-     * fully contains the datum or datum ensemble of the other 
<abbr>CRS</abbr>.
+     * If the two given coordinate reference systems are associated to equal 
(ignoring metadata) datum,
+     * then this method returns the <var>target</var> datum. Otherwise, this 
method returns a pseudo-datum
+     * for the largest ensemble which fully contains the datum or datum 
ensemble of the other <abbr>CRS</abbr>.
      * That largest common ensemble is interpreted as the new target of the 
operation result.
      * If none of the <var>source</var> or <var>target</var> datum ensembles 
met the above criteria,
      * then this method returns an empty value.
@@ -329,7 +329,8 @@ public final class DatumOrEnsemble extends Static {
         DatumEnsemble<D> targetEnsemble;
         DatumEnsemble<D> selected;
         if ((isMember(selected = targetEnsemble = (DatumEnsemble<D>) 
targetCRS.getDatumEnsemble(), sourceDatum)) ||
-            (isMember(selected = sourceEnsemble = (DatumEnsemble<D>) 
sourceCRS.getDatumEnsemble(), targetDatum)))
+            (isMember(selected = sourceEnsemble = (DatumEnsemble<D>) 
sourceCRS.getDatumEnsemble(), targetDatum)) ||
+            (sourceEnsemble != null && sourceEnsemble == targetEnsemble))     
// Optimization for a common case.
         {
             return Optional.of(constructor.apply(selected));
         }
@@ -372,7 +373,7 @@ public final class DatumOrEnsemble extends Static {
      * @return whether the ensemble contains the given datum.
      */
     private static boolean isMember(final DatumEnsemble<?> ensemble, final 
IdentifiedObject datum) {
-        if (ensemble != null) {
+        if (ensemble != null && datum != null) {
             for (final Datum member : ensemble.getMembers()) {
                 if (Utilities.equalsIgnoreMetadata(datum, member)) {
                     return true;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
index 52da6a75c9..624fad4f24 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
@@ -203,6 +203,11 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short CoordinateOperationNotFound_2 = 13;
 
+        /**
+         * No information about how to change from datum “{0}” to “{1}”.
+         */
+        public static final short DatumChangeNotFound_2 = 110;
+
         /**
          * Datum shift files are searched in the “{0}” directory.
          */
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
index 67efa76663..0d71f64351 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
@@ -75,6 +75,7 @@ ColinearAxisDirections_2          = Axis directions {0} and 
{1} are colinear.
 ConstantCoordinateValueRequired_1 = A constant value is required for the 
coordinate at index {0}.
 CoordinateOperationNotFound_2     = Coordinate operation from system 
\u201c{0}\u201d to \u201c{1}\u201d has not been found.
 DatumChangesDirectory_1           = Datum shift files are searched in the 
\u201c{0}\u201d directory.
+DatumChangeNotFound_2             = No information about how to change from 
datum \u201c{0}\u201d to \u201c{1}\u201d.
 DatumOriginShallBeDate            = Origin of temporal datum shall be a date.
 DuplicatedParameterName_4         = Name or alias for parameter 
\u201c{0}\u201d at index {1} conflict with name \u201c{2}\u201d at index {3}.
 DuplicatedSpatialComponents_1     = Compound coordinate reference systems 
cannot contain two {0,choice,1#horizontal|2#vertical} components.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
index eaf5433c7a..e7dd3786dd 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
@@ -80,6 +80,7 @@ ColinearAxisDirections_2          = Les directions 
d\u2019axes {0} et {1} sont c
 ConstantCoordinateValueRequired_1 = Une valeur constante est requise pour la 
coordonn\u00e9e \u00e0 l\u2019index {0}.
 CoordinateOperationNotFound_2     = L\u2019op\u00e9ration sur les 
coordonn\u00e9es du syst\u00e8me \u00ab\u202f{0}\u202f\u00bb vers 
\u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e.
 DatumChangesDirectory_1           = Les fichiers de changements de 
r\u00e9f\u00e9rentiel sont cherch\u00e9s dans le dossier 
\u00ab\u202f{0}\u202f\u00bb.
+DatumChangeNotFound_2             = Il n\u2019y a pas d\u2019information sur 
la fa\u00e7on de passer du r\u00e9f\u00e9rentiel \u00ab\u202f{0}\u202f\u00bb 
vers \u00ab\u202f{1}\u202f\u00bb.
 DatumOriginShallBeDate            = L\u2019origine d\u2019un 
r\u00e9f\u00e9rentiel temporel doit \u00eatre une date.
 DuplicatedParameterName_4         = Le nom ou un alias pour le param\u00e8tre 
\u00ab\u202f{0}\u202f\u00bb \u00e0 l\u2019index {1} duplique le nom 
\u00ab\u202f{2}\u202f\u00bb \u00e0 l\u2019index {3}.
 DuplicatedSpatialComponents_1     = Un syst\u00e8me de r\u00e9f\u00e9rence des 
coordonn\u00e9es ne peut pas contenir deux composantes 
{0,choice,1#horizontales|2#verticales}.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java
index cd06e050d8..19f1a1cb34 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java
@@ -97,7 +97,7 @@ final class CRSPair extends org.apache.sis.pending.jdk.Record 
{
                 cs = ((CoordinateReferenceSystem) 
object).getCoordinateSystem();
             }
             if (cs instanceof EllipsoidalCS) {
-                final StringBuilder sb = new StringBuilder(label);
+                final var sb = new StringBuilder(label);
                 sb.setLength(label.length() - suffix.length());
                 label = sb.append(((CoordinateSystem) 
cs).getDimension()).append('D').toString();
             }
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 ec8bc9e40a..44aa117e10 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
@@ -49,6 +49,7 @@ import org.apache.sis.referencing.privy.CoordinateOperations;
 import org.apache.sis.referencing.privy.EllipsoidalHeightCombiner;
 import org.apache.sis.referencing.privy.ReferencingUtilities;
 import org.apache.sis.referencing.internal.AnnotatedMatrix;
+import org.apache.sis.referencing.internal.PositionalAccuracyConstant;
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.datum.BursaWolfParameters;
@@ -330,20 +331,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         // │                Any single CRS  ↔  CRS of the same type            
     │
         // 
└────────────────────────────────────────────────────────────────────────┘
         if (sourceCRS instanceof SingleCRS && targetCRS instanceof SingleCRS) {
-            final Optional<IdentifiedObject> datumOrEnsemble =
-                    DatumOrEnsemble.ofTarget((SingleCRS) sourceCRS, 
(SingleCRS) targetCRS);
-            if (datumOrEnsemble.isPresent()) try {
-                /*
-                 * Because the CRS type is determined by the datum type 
(sometimes completed by the CS type),
-                 * having equivalent datum and compatible CS should be a 
sufficient criterion.
-                 */
-                return asList(createFromAffineTransform(AXIS_CHANGES, 
sourceCRS, targetCRS,
-                                
DatumOrEnsemble.getAccuracy(datumOrEnsemble.get()).orElse(null),
-                                
CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(),
-                                                                   
targetCRS.getCoordinateSystem())));
-            } catch (IllegalArgumentException | IncommensurableException e) {
-                throw new FactoryException(notFoundMessage(sourceCRS, 
targetCRS), e);
-            }
+            return createOperationStepFallback((SingleCRS) sourceCRS, 
(SingleCRS) targetCRS);
         }
         // 
┌────────────────────────────────────────────────────────────────────────┐
         // │                        Compound  ↔  various CRS                   
     │
@@ -509,25 +497,37 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         final GeodeticDatum targetDatum = DatumOrEnsemble.asDatum(targetCRS);
         /*
          * Find the type of operation depending on whether there is a change 
of geodetic reference frame (datum).
-         * The `DATUM_SHIFT` and `ELLIPSOID_CHANGE` identifiers mean that 
there is a datum change, and all other
-         * identifiers mean that the coordinate operation is only a change of 
coordinate system type (Ellipsoidal
-         * ↔ Cartesian ↔ Spherical), axis swapping and unit conversions.
+         * The `DATUM_SHIFT`, `UNSPECIFIED_DATUM_CHANGE` and 
`SAME_DATUM_ENSEMBLE` identifiers mean that there is
+         * a datum change, and all other identifiers mean that the coordinate 
operation is only a change of
+         * coordinate system type (Ellipsoidal ↔ Cartesian ↔ Spherical), axis 
swapping and unit conversions.
          */
+        Identifier typeOfChange;
         final Matrix datumShift;
-        final Identifier identifier;
         final MathTransform transform;
         ParameterValueGroup parameters;
         final Optional<OperationMethod> method;
-        final Optional<GeodeticDatum> commonDatum = 
DatumOrEnsemble.asTargetDatum(sourceCRS, targetCRS);
-        if (commonDatum.isPresent()) {
+        final Optional<GeodeticDatum> finalDatum;
+        final boolean sameDatumOrEnsemble;
+        if (Utilities.equalsIgnoreMetadata(sourceDatum, targetDatum)) {
+            finalDatum = Optional.empty();
+            sameDatumOrEnsemble = true;
+            typeOfChange = (sourceCS instanceof EllipsoidalCS)
+                        == (targetCS instanceof EllipsoidalCS) ? AXIS_CHANGES 
: GEOCENTRIC_CONVERSION;
+        } else {
+            finalDatum = DatumOrEnsemble.asTargetDatum(sourceCRS, targetCRS);
+            sameDatumOrEnsemble = finalDatum.isPresent();
+            typeOfChange = sameDatumOrEnsemble ? SAME_DATUM_ENSEMBLE : 
UNSPECIFIED_DATUM_CHANGE;
+        }
+        if (sameDatumOrEnsemble) {
             /*
              * Coordinate system change (including change in the number of 
dimensions) without datum shift.
              * May contain the addition of ellipsoidal height or spherical 
radius, which need an ellipsoid.
              */
-            final boolean isGeographic = (sourceCS instanceof EllipsoidalCS);
-            identifier = isGeographic != (targetCS instanceof EllipsoidalCS) ? 
GEOCENTRIC_CONVERSION : AXIS_CHANGES;
             final var builder = 
factorySIS.getMathTransformFactory().builder(Constants.COORDINATE_SYSTEM_CONVERSION);
-            final var ellipsoid = (isGeographic ? sourceDatum : 
targetDatum).getEllipsoid();
+            Ellipsoid ellipsoid = targetDatum.getEllipsoid();
+            if (ellipsoid == null) {
+                ellipsoid = sourceDatum.getEllipsoid();
+            }
             builder.setSourceAxes(sourceCS, ellipsoid);
             builder.setTargetAxes(targetCS, ellipsoid);
             transform  = builder.create();
@@ -554,9 +554,13 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
              *    - [Abridged] Molodensky          (as an approximation of 
geocentric translation)
              *    - Identity                       (if the desired accuracy is 
so large than we can skip datum shift)
              */
-            datumShift = (sourceDatum instanceof DefaultGeodeticDatum) ?
-                    ((DefaultGeodeticDatum) 
sourceDatum).getPositionVectorTransformation(targetDatum, areaOfInterest) : 
null;
-            identifier = (datumShift != null) ? DATUM_SHIFT : ELLIPSOID_CHANGE;
+            if (sourceDatum instanceof DefaultGeodeticDatum) {
+                final var impl = (DefaultGeodeticDatum) sourceDatum;
+                datumShift = impl.getPositionVectorTransformation(targetDatum, 
areaOfInterest);
+                if (datumShift != null) typeOfChange = DATUM_SHIFT;
+            } else {
+                datumShift   = null;
+            }
             var builder = new 
MathTransformContext(factorySIS.getMathTransformFactory(), sourceDatum, 
targetDatum);
             builder.setSourceAxes(sourceCS, sourceDatum.getEllipsoid());
             builder.setTargetAxes(targetCS, targetDatum.getEllipsoid());
@@ -576,15 +580,23 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
          * The indirect path uses a third datum (typically WGS84) as an 
intermediate between the two
          * specified datum.
          */
-        final Map<String, Object> properties = properties(identifier);
-        PositionalAccuracy accuracy = 
commonDatum.flatMap(DatumOrEnsemble::getAccuracy).orElse(null);
-        if (accuracy == null && datumShift instanceof AnnotatedMatrix) {
-            accuracy = ((AnnotatedMatrix) datumShift).accuracy;
-        }
-        if (accuracy != null) {
-            
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, accuracy);
+        final Map<String, Object> properties = properties(typeOfChange);
+        PositionalAccuracy accuracy = 
finalDatum.flatMap(DatumOrEnsemble::getAccuracy).orElseGet(() ->
+            (datumShift instanceof AnnotatedMatrix) ? ((AnnotatedMatrix) 
datumShift).accuracy : null
+        );
+        final Class<? extends SingleOperation> type;
+        if (isDatumChange(typeOfChange)) {
+            type = Transformation.class;
+            if (accuracy == null) {
+                accuracy = (typeOfChange == UNSPECIFIED_DATUM_CHANGE)
+                     ? PositionalAccuracyConstant.DATUM_SHIFT_OMITTED
+                     : PositionalAccuracyConstant.DATUM_SHIFT_APPLIED;
+            }
+        } else {
+            type = Conversion.class;
         }
-        return asList(createFromMathTransform(properties, sourceCRS, 
targetCRS, transform, method.orElse(null), parameters, null));
+        properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, 
accuracy);
+        return asList(createFromMathTransform(properties, sourceCRS, 
targetCRS, transform, method.orElse(null), parameters, type));
     }
 
     /**
@@ -681,7 +693,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
          * It is not the job of this block to perform unit conversions.
          * Unit conversions, if needed, are done by `step3` computed in above 
block.
          *
-         * The "Geographic3DtoVertical.txt" file in the provider package is a 
reminder.
+         * The "Geographic3DtoVertical.md" file in the `provider` package is a 
reminder.
          * If this policy is changed, that file should be edited accordingly.
          */
         final int srcDim = interpolationCS.getDimension();                     
     // Should always be 3.
@@ -713,10 +725,19 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
                                                             final VerticalCRS 
targetCRS)
             throws FactoryException
     {
-        final Optional<VerticalDatum> commonDatum = 
DatumOrEnsemble.asTargetDatum(sourceCRS, targetCRS);
-        if (commonDatum.isEmpty()) {
-            throw new 
OperationNotFoundException(notFoundMessage(DatumOrEnsemble.of(sourceCRS),
-                                                                 
DatumOrEnsemble.of(targetCRS)));
+        final VerticalDatum sourceDatum = DatumOrEnsemble.asDatum(sourceCRS);
+        final VerticalDatum targetDatum = DatumOrEnsemble.asDatum(targetCRS);
+        final Optional<VerticalDatum> finalDatum;
+        final Identifier typeOfChange;
+        if (Utilities.equalsIgnoreMetadata(sourceDatum, targetDatum)) {
+            finalDatum   = Optional.empty();
+            typeOfChange = AXIS_CHANGES;
+        } else {
+            finalDatum = DatumOrEnsemble.asTargetDatum(sourceCRS, targetCRS);
+            if (finalDatum.isEmpty()) {
+                throw new 
OperationNotFoundException(datumChangeNotFound(sourceDatum, targetDatum));
+            }
+            typeOfChange = SAME_DATUM_ENSEMBLE;
         }
         final VerticalCS sourceCS = sourceCRS.getCoordinateSystem();
         final VerticalCS targetCS = targetCRS.getCoordinateSystem();
@@ -726,8 +747,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         } catch (IllegalArgumentException | IncommensurableException 
exception) {
             throw new OperationNotFoundException(notFoundMessage(sourceCRS, 
targetCRS), exception);
         }
-        return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, 
targetCRS,
-                DatumOrEnsemble.getAccuracy(commonDatum.get()).orElse(null), 
matrix));
+        // TODO: should we replace `targetCRS` if `finalDatum` is not equal to 
`targetDatum`?
+        return asList(createFromAffineTransform(typeOfChange, sourceCRS, 
targetCRS, finalDatum, matrix));
     }
 
     /**
@@ -758,6 +779,16 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
     {
         final TemporalDatum sourceDatum = DatumOrEnsemble.asDatum(sourceCRS);
         final TemporalDatum targetDatum = DatumOrEnsemble.asDatum(targetCRS);
+        final Optional<TemporalDatum> finalDatum;
+        final Identifier typeOfChange;
+        if (Utilities.equalsIgnoreMetadata(sourceDatum, targetDatum)) {
+            finalDatum   = Optional.empty();
+            typeOfChange = AXIS_CHANGES;
+        } else {
+            finalDatum   = DatumOrEnsemble.asTargetDatum(sourceCRS, targetCRS);
+            typeOfChange = finalDatum.isPresent() ? SAME_DATUM_ENSEMBLE : 
DATUM_SHIFT;
+            // We do not default to `UNSPECIFIED_DATUM_CHANGE` because the 
change is not completely unspecified.
+        }
         final TimeCS sourceCS = sourceCRS.getCoordinateSystem();
         final TimeCS targetCS = targetCRS.getCoordinateSystem();
         /*
@@ -787,7 +818,58 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         final int translationColumn = matrix.getNumCol() - 1;           // 
Paranoiac check: should always be 1.
         final var translation = DoubleDouble.of(matrix.getNumber(0, 
translationColumn), true);
         matrix.setNumber(0, translationColumn, translation.add(epochShift));
-        return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, 
targetCRS, null, matrix));
+        // TODO: should we replace `targetCRS` if `finalDatum` is not equal to 
`targetDatum`?
+        return asList(createFromAffineTransform(typeOfChange, sourceCRS, 
targetCRS, finalDatum, matrix));
+    }
+
+    /**
+     * Creates an operation between two coordinate reference systems having no 
specialized method.
+     * The two given <abbr>CRS</abbr> should be of the same type. This is not 
verified directly by
+     * this method. However, because the <abbr>CRS</abbr> type is determined 
by the datum type
+     * (sometimes completed by the <abbr>CS</abbr> type), having equivalent 
datum and compatible
+     * <abbr>CS</abbr> should be a sufficient criterion for saying that the 
<abbr>CRS</abbr> are
+     * of the same type.
+     *
+     * <p>This method should be invoked as in last resort only.</p>
+     *
+     * <h4>Implementation type</h4>
+     * The method body is a pattern repeated in most {@code 
createOperationStep(…)} methods of this class.
+     * Except that in other methods, the logic is interleaved with more 
complex checks for datum changes.
+     * Understanding the code of this method can help to understand the code 
of other methods.
+     *
+     * @param  sourceCRS  input coordinate reference system.
+     * @param  targetCRS  output coordinate reference system.
+     * @return a coordinate operation from {@code sourceCRS} to {@code 
targetCRS}.
+     * @throws FactoryException if the operation cannot be constructed.
+     */
+    private List<CoordinateOperation> createOperationStepFallback(final 
SingleCRS sourceCRS,
+                                                                  final 
SingleCRS targetCRS)
+            throws FactoryException
+    {
+        final IdentifiedObject sourceDatum = DatumOrEnsemble.of(sourceCRS);
+        final IdentifiedObject targetDatum = DatumOrEnsemble.of(targetCRS);
+        final Optional<IdentifiedObject> finalDatum;
+        final Identifier typeOfChange;
+        if (Utilities.equalsIgnoreMetadata(sourceDatum, targetDatum)) {
+            finalDatum   = Optional.empty();
+            typeOfChange = AXIS_CHANGES;
+        } else {
+            finalDatum = DatumOrEnsemble.ofTarget(sourceCRS, targetCRS);
+            if (finalDatum.isEmpty()) {
+                throw new 
OperationNotFoundException(datumChangeNotFound(sourceDatum, targetDatum));
+            }
+            typeOfChange = SAME_DATUM_ENSEMBLE;
+        }
+        final CoordinateSystem sourceCS = sourceCRS.getCoordinateSystem();
+        final CoordinateSystem targetCS = targetCRS.getCoordinateSystem();
+        final Matrix matrix;
+        try {
+            matrix = CoordinateSystems.swapAndScaleAxes(sourceCS, targetCS);
+        } catch (IllegalArgumentException | IncommensurableException 
exception) {
+            throw new OperationNotFoundException(notFoundMessage(sourceCRS, 
targetCRS), exception);
+        }
+        // TODO: should we replace `targetCRS` if `finalDatum` is not equal to 
`targetDatum`?
+        return asList(createFromAffineTransform(typeOfChange, sourceCRS, 
targetCRS, finalDatum, matrix));
     }
 
     /**
@@ -944,31 +1026,37 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
 
     /**
      * Creates a coordinate operation from a matrix, which usually describes 
an affine transform.
-     * A default {@link OperationMethod} object is given to this transform. In 
the special case
-     * where the {@code name} identifier is {@link #DATUM_SHIFT} or {@link 
#ELLIPSOID_CHANGE},
+     * A default {@link OperationMethod} object is given to the returned 
operation. In the special case
+     * where {@code typeOfChange} {@linkplain #isDatumChange(Identifier) is 
for a datum change},
      * the operation will be a {@link Transformation} instance instead of 
{@link Conversion}.
      *
-     * @param  name       the identifier for the operation to be created.
-     * @param  sourceCRS  the source coordinate reference system.
-     * @param  targetCRS  the target coordinate reference system.
-     * @param  accuracy   the positional accuracy, or {@code null} if 
unspecified.
-     * @param  matrix     the matrix which describe an affine transform 
operation.
+     * @param  typeOfChange  the identifier for the operation to be created.
+     * @param  sourceCRS     the source coordinate reference system.
+     * @param  targetCRS     the target coordinate reference system.
+     * @param  finalDatum    the ensemble that determines the operation 
accuracy, or {@code null} if none.
+     * @param  matrix        the matrix which describe an affine transform 
operation.
      * @return the conversion or transformation.
      * @throws FactoryException if the operation cannot be created.
      */
-    private CoordinateOperation createFromAffineTransform(final Identifier     
           name,
-                                                          final 
CoordinateReferenceSystem sourceCRS,
-                                                          final 
CoordinateReferenceSystem targetCRS,
-                                                          final 
PositionalAccuracy        accuracy,
-                                                          final Matrix         
           matrix)
-            throws FactoryException
+    private CoordinateOperation createFromAffineTransform(
+            final Identifier typeOfChange,
+            final CoordinateReferenceSystem sourceCRS,
+            final CoordinateReferenceSystem targetCRS,
+            final Optional<? extends IdentifiedObject> finalDatum,
+            final Matrix matrix) throws FactoryException
     {
-        final MathTransform transform  = 
factorySIS.getMathTransformFactory().createAffineTransform(matrix);
-        final Map<String, Object> properties = properties(name);
-        if (accuracy != null) {
-            
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, accuracy);
+        final MathTransform transform = 
factorySIS.getMathTransformFactory().createAffineTransform(matrix);
+        final Map<String, Object> properties = properties(typeOfChange);
+        if (finalDatum != null) {
+            finalDatum.flatMap(DatumOrEnsemble::getAccuracy)
+                    .ifPresent((accuracy) -> 
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, 
accuracy));
+        }
+        // Note: we cannot rely on whether the accuracy was non-null for 
determining the type.
+        Class<? extends CoordinateOperation> type = Conversion.class;
+        if (isDatumChange(typeOfChange)) {
+            type = Transformation.class;
         }
-        return createFromMathTransform(properties, sourceCRS, targetCRS, 
transform, null, null, null);
+        return createFromMathTransform(properties, sourceCRS, targetCRS, 
transform, null, null, type);
     }
 
     /**
@@ -1033,11 +1121,14 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         }
         if (main instanceof SingleOperation) {
             final SingleOperation op = (SingleOperation) main;
-            final MathTransform mt = 
factorySIS.getMathTransformFactory().createConcatenatedTransform(mt1, mt2);
-            main = createFromMathTransform(new 
HashMap<>(IdentifiedObjects.getProperties(main)),
-                   sourceCRS, targetCRS, mt, op.getMethod(), 
op.getParameterValues(),
-                   (main instanceof Transformation) ? Transformation.class :
-                   (main instanceof Conversion) ? Conversion.class : 
SingleOperation.class);
+            main = createFromMathTransform(
+                    new HashMap<>(IdentifiedObjects.getProperties(main)),
+                    sourceCRS,
+                    targetCRS,
+                    
factorySIS.getMathTransformFactory().createConcatenatedTransform(mt1, mt2),
+                    op.getMethod(),
+                    op.getParameterValues(),
+                    typeOf(op));
         } else {
             main = factory.createConcatenatedOperation(defaultName(sourceCRS, 
targetCRS), step1, step2);
         }
@@ -1112,6 +1203,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
      * Returns {@code true} if a coordinate operation of the given name can be 
hidden
      * in the list of operations. Note that the {@code MathTransform} will 
still take
      * the operation in account however.
+     *
+     * @see #isDatumChange(Identifier)
      */
     private static boolean canHide(final Identifier id) {
         return (id == AXIS_CHANGES) || (id == IDENTITY);
@@ -1142,8 +1235,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         identifierOfStepCRS.put(newID, oldID);
         identifierOfStepCRS.put(oldID, count);
 
-        final Map<String,Object> properties = new HashMap<>(4);
-        properties.put(IdentifiedObject.NAME_KEY, newID);
+        final Map<String,Object> properties = properties(newID);
         properties.put(IdentifiedObject.REMARKS_KEY, 
Vocabulary.formatInternational(Vocabulary.Keys.DerivedFrom_1, label(object)));
         return properties;
     }
@@ -1161,7 +1253,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
      * CoordinateReferenceSystem)} method contract.
      */
     private static List<CoordinateOperation> asList(final CoordinateOperation 
operation) {
-        final List<CoordinateOperation> operations = new ArrayList<>(1);
+        final var operations = new ArrayList<CoordinateOperation>(1);
         operations.add(operation);
         return operations;
     }
@@ -1173,8 +1265,10 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
      * @param  source  the source CRS.
      * @param  target  the target CRS.
      * @return a default error message.
+     *
+     * @see #datumChangeNotFound(IdentifiedObject, IdentifiedObject)
      */
-    private String notFoundMessage(final IdentifiedObject source, final 
IdentifiedObject target) {
+    private String notFoundMessage(final CoordinateReferenceSystem source, 
final CoordinateReferenceSystem target) {
         return 
resources().getString(Resources.Keys.CoordinateOperationNotFound_2, 
label(source), label(target));
     }
 
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 3c45c52ffc..446a972e0b 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
@@ -127,20 +127,30 @@ class CoordinateOperationRegistry {
     /**
      * The identifier for a transformation which is a datum shift without 
Bursa-Wolf parameters.
      * Only the changes in ellipsoid axis-length are taken in account.
-     * Such "ellipsoid shifts" are approximations and may have 1 kilometre 
error.
+     * Such "Unspecified datum change" are approximations and may have 1 
kilometre error.
      *
+     * @see #isDatumChange(Identifier)
      * @see org.apache.sis.referencing.datum.BursaWolfParameters
      * @see PositionalAccuracyConstant#DATUM_SHIFT_OMITTED
      */
-    static final Identifier ELLIPSOID_CHANGE = 
createIdentifier(Vocabulary.Keys.EllipsoidChange);
+    static final Identifier UNSPECIFIED_DATUM_CHANGE = 
createIdentifier(Vocabulary.Keys.UnspecifiedDatumChange);
 
     /**
      * The identifier for a transformation which is a datum shift.
      *
+     * @see #isDatumChange(Identifier)
      * @see PositionalAccuracyConstant#DATUM_SHIFT_APPLIED
      */
     static final Identifier DATUM_SHIFT = 
createIdentifier(Vocabulary.Keys.DatumShift);
 
+    /**
+     * The identifier for a transformation between two members of the same 
datum ensemble.
+     * The accuracy is specified by the datum ensemble.
+     *
+     * @see #isDatumChange(Identifier)
+     */
+    static final Identifier SAME_DATUM_ENSEMBLE = 
createIdentifier(Vocabulary.Keys.SameDatumEnsemble);
+
     /**
      * The identifier for a geocentric conversion.
      */
@@ -158,6 +168,17 @@ class CoordinateOperationRegistry {
         return new NamedIdentifier(Citations.SIS, 
Vocabulary.formatInternational(key));
     }
 
+    /**
+     * Returns whether the given identifier is one of the pre-defined values 
used for datum changes.
+     *
+     * @see CoordinateOperationFinder#canHide(Identifier)
+     */
+    static boolean isDatumChange(final Identifier typeOfChange) {
+        return typeOfChange == DATUM_SHIFT
+            || typeOfChange == UNSPECIFIED_DATUM_CHANGE
+            || typeOfChange == SAME_DATUM_ENSEMBLE;
+    }
+
     /**
      * The object to use for finding authority codes, or {@code null} if none.
      * An instance is fetched at construction time from the {@link #registry} 
if possible.
@@ -739,15 +760,7 @@ class CoordinateOperationRegistry {
         final OperationMethod method = 
InverseOperationMethod.create(op.getMethod(), factorySIS);
         final Map<String,Object> properties = properties(INVERSE_OPERATION);
         InverseOperationMethod.properties(op, properties);
-        /*
-         * Find a hint about whether the coordinate operation is a 
transformation or a conversion,
-         * but do not set any conversion subtype. In particular, do not 
specify a Projection type,
-         * because the inverse of a Projection does not implement the 
Projection interface.
-         */
-        Class<? extends CoordinateOperation> type = null;
-        if (op instanceof Transformation)  type = Transformation.class;
-        else if (op instanceof Conversion) type = Conversion.class;
-        inverse = createFromMathTransform(properties, targetCRS, sourceCRS, 
transform, method, null, type);
+        inverse = createFromMathTransform(properties, targetCRS, sourceCRS, 
transform, method, null, typeOf(op));
         AbstractCoordinateOperation.setCachedInverse(op, inverse);
         return inverse;
     }
@@ -975,15 +988,7 @@ class CoordinateOperationRegistry {
         if (Utilities.equalsApproximately(sourceCRS, crs = 
operation.getSourceCRS())) sourceCRS = crs;
         if (Utilities.equalsApproximately(targetCRS, crs = 
operation.getTargetCRS())) targetCRS = crs;
         final Map<String,Object> properties = new 
HashMap<>(derivedFrom(operation));
-        /*
-         * Determine whether the operation to create is a Conversion or a 
Transformation
-         * (could also be a Conversion subtype like Projection, but this is 
less important).
-         * We want the GeoAPI interface, not the implementation class.
-         * The most reliable way is to ask to the 
`AbstractOperation.getInterface()` method,
-         * but this is SIS-specific. The fallback uses reflection.
-         */
-        properties.put(CoordinateOperations.OPERATION_TYPE_KEY,
-                ReferencingUtilities.getInterface(CoordinateOperation.class, 
operation));
+        properties.put(CoordinateOperations.OPERATION_TYPE_KEY, 
typeOf(operation));
         /*
          * Reuse the same operation method, but we may need to change its 
number of dimension.
          * For example the "Affine" set of parameters depend on the number of 
dimensions.
@@ -1233,10 +1238,10 @@ class CoordinateOperationRegistry {
 
     /**
      * Returns the specified identifier in a map to be given to coordinate 
operation constructors.
-     * In the special case where the {@code name} identifier is {@link 
#DATUM_SHIFT} or {@link #ELLIPSOID_CHANGE},
-     * the map will contain extra information like positional accuracy.
+     * If the coordinate operation is a transformation, then it is caller's 
responsibility to set
+     * the {@value CoordinateOperation#COORDINATE_OPERATION_ACCURACY_KEY} 
property.
      *
-     * <h4>Note</h4>
+     * <h4>Missing properties</h4>
      * In the datum shift case, an operation version is mandatory but unknown 
at this time.
      * However, we noticed that the EPSG database does not always define a 
version neither.
      * Consequently, the Apache SIS implementation relaxes the rule requiring 
an operation
@@ -1246,13 +1251,8 @@ class CoordinateOperationRegistry {
      * @return a modifiable map containing the given name. Callers can put 
other entries in this map.
      */
     static Map<String,Object> properties(final Identifier name) {
-        final Map<String,Object> properties = new HashMap<>(4);
+        final var properties = new HashMap<String,Object>(4);
         properties.put(CoordinateOperation.NAME_KEY, name);
-        if ((name == DATUM_SHIFT) || (name == ELLIPSOID_CHANGE)) {
-            
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY,
-                    (name == DATUM_SHIFT) ? 
PositionalAccuracyConstant.DATUM_SHIFT_APPLIED
-                                          : 
PositionalAccuracyConstant.DATUM_SHIFT_OMITTED);
-        }
         return properties;
     }
 
@@ -1296,7 +1296,7 @@ class CoordinateOperationRegistry {
      *   <li>If the given {@code type} is null, then this method infers the 
type from whether the given properties
      *       specify and accuracy or not. If those properties were created by 
the {@link #properties(Identifier)}
      *       method, then the operation will be a {@link Transformation} 
instance instead of {@link Conversion} if
-     *       the {@code name} identifier was {@link #DATUM_SHIFT} or {@link 
#ELLIPSOID_CHANGE}.</li>
+     *       the {@code name} identifier {@linkplain 
#isDatumChange(Identifier) is for a datum change}.</li>
      *
      *   <li>If the given {@code method} is {@code null}, then infer an 
operation method by inspecting the given transform.
      *       The transform needs to implement the {@link 
org.apache.sis.parameter.Parameterized} interface in order to allow
@@ -1340,15 +1340,6 @@ class CoordinateOperationRegistry {
                 return operation;
             }
         }
-        /*
-         * If the operation type was not explicitly specified, infers it from 
whether an accuracy is specified
-         * or not. In principle, only transformations has an accuracy 
property; conversions do not. This policy
-         * is applied by the properties(Identifier) method in this class.
-         */
-        if (type == null) {
-            type = 
properties.containsKey(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY)
-                    ? Transformation.class : Conversion.class;
-        }
         /*
          * The operation method is mandatory. If the user did not provided 
one, we need to infer it ourselves.
          * If we fail to infer an OperationMethod, let it to null - the 
exception will be thrown by the factory.
@@ -1372,13 +1363,32 @@ class CoordinateOperationRegistry {
         if (parameters != null) {
             properties.put(CoordinateOperations.PARAMETERS_KEY, parameters);
         }
-        properties.put(CoordinateOperations.OPERATION_TYPE_KEY, type);
-        if (Conversion.class.isAssignableFrom(type) && transform.isIdentity()) 
{
-            properties.replace(IdentifiedObject.NAME_KEY, AXIS_CHANGES, 
IDENTITY);
+        if (type != null) {
+            properties.put(CoordinateOperations.OPERATION_TYPE_KEY, type);
+            if (Conversion.class.isAssignableFrom(type) && 
transform.isIdentity()) {
+                properties.replace(IdentifiedObject.NAME_KEY, AXIS_CHANGES, 
IDENTITY);
+            }
         }
         return factorySIS.createSingleOperation(properties, sourceCRS, 
targetCRS, null, method, transform);
     }
 
+    /**
+     * Returns the GeoAPI interface (not the implementation class) of the 
given operation.
+     * This mostly for whether the operation is a {@link Conversion} or {@link 
Transformation}.
+     * Legacy GeoAPI types such as {@code Projection} are intentionally 
omitted, in part because
+     * the inverse of a {@code Projection} does not implement the same 
interface.
+     *
+     * @param  op  the coordinate operation for which to identify the type.
+     * @return the operation type, or {@code null} if not one of the types of 
interest for this factory.
+     *
+     * @see AbstractCoordinateOperation#getInterface()
+     */
+    static Class<? extends CoordinateOperation> typeOf(final 
CoordinateOperation op) {
+        if (op instanceof Transformation) return Transformation.class;
+        if (op instanceof Conversion)     return Conversion.class;
+        return null;
+    }
+
     /**
      * {@return the localized resources for error messages}.
      */
@@ -1395,6 +1405,20 @@ class CoordinateOperationRegistry {
         return CRSPair.label(object, locale);
     }
 
+    /**
+     * Returns an error message for missing information about datum change.
+     * This is used for the construction of {@link OperationNotFoundException}.
+     *
+     * @param  source  the source datum or datum ensemble.
+     * @param  target  the target datum or datum ensemble.
+     * @return a default error message.
+     */
+    final String datumChangeNotFound(final IdentifiedObject source, final 
IdentifiedObject target) {
+        return resources().getString(Resources.Keys.DatumChangeNotFound_2,
+                IdentifiedObjects.getDisplayName(source, locale),
+                IdentifiedObjects.getDisplayName(target, locale));
+    }
+
     /**
      * Logs an unexpected but ignorable exception. This method pretends that 
the logging
      * come from {@link CoordinateOperationFinder} since this is the public 
API which
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 85d7fed458..e8488c69c5 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
@@ -627,9 +627,9 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
     }
 
     /**
-     * Tests a conversion of the temporal axis. We convert 1899-12-31 from a 
CRS having its epoch at 1970-1-1
+     * Tests a cha,ge of the temporal axis. We convert 1899-12-31 from a CRS 
having its epoch at 1970-1-1
      * to another CRS having its epoch at 1858-11-17, so the new value shall 
be approximately 41 years
-     * after the new epoch. This conversion also implies a change of units 
from seconds to days.
+     * after the new epoch. This operation also implies a change of units from 
seconds to days.
      *
      * @throws FactoryException if the operation cannot be created.
      * @throws TransformException if an error occurred while converting the 
test points.
@@ -641,8 +641,8 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
         final CoordinateOperation operation = 
finder().createOperation(sourceCRS, targetCRS);
         assertSame(sourceCRS, operation.getSourceCRS());
         assertSame(targetCRS, operation.getTargetCRS());
-        assertEquals("Axis changes", operation.getName().getCode());
-        assertInstanceOf(Conversion.class, operation);
+        assertEquals("Datum shift", operation.getName().getCode());
+        assertInstanceOf(Transformation.class, operation);  // Transformation 
because there is a change of datum.
 
         transform = operation.getMathTransform();
         tolerance = 2E-12;
@@ -1080,7 +1080,7 @@ public final class CoordinateOperationFinderTest extends 
MathTransformTestCase {
         final CoordinateOperationFinder finder = finder();
         var e = assertThrows(OperationNotFoundException.class, () -> 
finder.createOperation(sourceCRS, targetCRS),
                 "Should not create operation between CRS of different datum.");
-        assertMessageContains(e, "A test CRS");
+        assertMessageContains(e, "Screen display", "Another device");
 
         final DefaultEngineeringCRS screenCRS = createEngineering("Screen 
display", AxisDirection.DISPLAY_UP);
         final CoordinateOperation operation = 
finder.createOperation(sourceCRS, screenCRS);
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactoryTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactoryTest.java
index c7ce93f6f1..b96ed6d376 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactoryTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactoryTest.java
@@ -267,7 +267,7 @@ public final class DefaultCoordinateOperationFactoryTest 
extends MathTransformTe
             assertEquals( 320,      p2.parameter("Z-axis 
translation").doubleValue());
             return true;
         } else {
-            assertSame(CoordinateOperationFinder.ELLIPSOID_CHANGE, 
steps.get(datumShiftIndex).getName());
+            assertSame(CoordinateOperationFinder.UNSPECIFIED_DATUM_CHANGE, 
steps.get(datumShiftIndex).getName());
             return false;
         }
     }
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
index 4bb8178aaf..5cef3e30e1 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
@@ -464,11 +464,6 @@ public class Vocabulary extends IndexedResourceBundle {
          */
         public static final short Ellipsoid = 72;
 
-        /**
-         * Ellipsoid change
-         */
-        public static final short EllipsoidChange = 73;
-
         /**
          * Ellipsoidal height
          */
@@ -1129,6 +1124,11 @@ public class Vocabulary extends IndexedResourceBundle {
          */
         public static final short RootMeanSquare = 177;
 
+        /**
+         * Same datum ensemble
+         */
+        public static final short SameDatumEnsemble = 279;
+
         /**
          * Sample dimensions
          */
@@ -1344,6 +1344,11 @@ public class Vocabulary extends IndexedResourceBundle {
          */
         public static final short Unspecified = 209;
 
+        /**
+         * Unspecified datum change
+         */
+        public static final short UnspecifiedDatumChange = 73;
+
         /**
          * Untitled
          */
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
index 5541020861..14dc57b6fa 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
@@ -97,7 +97,6 @@ Domain                  = Domain
 DublinJulian            = Dublin Julian
 EastBound               = East bound
 Ellipsoid               = Ellipsoid
-EllipsoidChange         = Ellipsoid change
 EllipsoidalHeight       = Ellipsoidal height
 EndDate                 = End date
 EndPoint                = End point
@@ -230,6 +229,7 @@ Result                  = Result
 Retry                   = Retry
 Root                    = Root
 RootMeanSquare          = Root Mean Square
+SameDatumEnsemble       = Same datum ensemble
 SampleDimensions        = Sample dimensions
 Scale                   = Scale
 Simplified              = Simplified
@@ -274,6 +274,7 @@ Unnamed_1               = Unnamed #{0}
 Unspecified             = Unspecified
 Untitled                = Untitled
 UnavailableContent      = Unavailable content.
+UnspecifiedDatumChange  = Unspecified datum change
 UpperBound              = Upper bound
 UserHome                = User home directory
 Value                   = Value
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
index c51e88a567..8294ba244a 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -104,7 +104,6 @@ Domain                  = Domaine
 DublinJulian            = Julien Dublin
 EastBound               = Limite est
 Ellipsoid               = Ellipso\u00efde
-EllipsoidChange         = Changement d\u2019ellipso\u00efde
 EllipsoidalHeight       = Hauteur ellipso\u00efdale
 EndDate                 = Date de fin
 EndPoint                = Point d\u2019arriv\u00e9
@@ -237,6 +236,7 @@ Result                  = R\u00e9sultat
 Retry                   = R\u00e9essayer
 Root                    = Racine
 RootMeanSquare          = Moyenne quadratique
+SameDatumEnsemble       = M\u00eame ensemble de r\u00e9f\u00e9rentiels
 SampleDimensions        = Dimensions d\u2019\u00e9chantillonnage
 Scale                   = \u00c9chelle
 Simplified              = Simplifi\u00e9
@@ -281,6 +281,7 @@ Unnamed_1               = Sans nom \u2116{0}
 Unspecified             = Non-sp\u00e9cifi\u00e9
 Untitled                = Sans titre
 UnavailableContent      = Contenu non-disponible.
+UnspecifiedDatumChange  = Changement de r\u00e9f\u00e9rentiel non 
sp\u00e9cifi\u00e9
 UpperBound              = Limite haute
 UserHome                = R\u00e9pertoire de l\u2019utilisateur
 Value                   = Valeur

Reply via email to