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 2c396c8526 Make use, or prepare for the use, of `DatumEnsemble` in 
`CoordinateOperationFinder`. The following pattern:
2c396c8526 is described below

commit 2c396c852690acc967efb5d20bdadf3176e846a9
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Jul 19 17:20:17 2024 +0200

    Make use, or prepare for the use, of `DatumEnsemble` in 
`CoordinateOperationFinder`.
    The following pattern:
    
        if (equalsIgnoreMetadata(source.getDatum(), target.getDatum())) {...}
    
    is replaced by
    
        if (PseudoDatum.ofOperation(source, target).isPresent()) {...}
    
    The `ofOperation` result is used for determining the accuracy of the 
coordinate operation.
---
 .../sis/referencing/MultiRegisterOperations.java   |   2 +-
 .../apache/sis/referencing/datum/PseudoDatum.java  | 266 +++++++++++++++------
 .../operation/CoordinateOperationFinder.java       |  71 ++++--
 .../operation/CoordinateOperationRegistry.java     |   9 +-
 .../DefaultCoordinateOperationFactory.java         |  23 +-
 5 files changed, 247 insertions(+), 124 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java
index 8eed014ccc..4b46c7a044 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java
@@ -348,7 +348,7 @@ public class MultiRegisterOperations extends 
AbstractFactory implements Register
             return false;
         }
         for (int i=0; i<n; i++) {
-            if (PseudoDatum.getOperationAccuracy(sources.get(i), 
targets.get(i)).isEmpty()) {
+            if (PseudoDatum.getDatumOrEnsemble(sources.get(i), 
targets.get(i)).isEmpty()) {
                 return false;
             }
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/PseudoDatum.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/PseudoDatum.java
index 87c9fe4f43..75ac98142e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/PseudoDatum.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/PseudoDatum.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.referencing.datum;
 
+import java.util.ArrayDeque;
 import java.util.Set;
 import java.util.Collection;
 import java.util.Iterator;
@@ -39,7 +40,6 @@ import org.apache.sis.util.LenientComparable;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.GeodeticException;
-import org.apache.sis.referencing.internal.PositionalAccuracyConstant;
 
 
 /**
@@ -198,89 +198,136 @@ public abstract class PseudoDatum<D extends Datum> 
implements Datum, LenientComp
     }
 
     /**
-     * Returns the datum of the given <abbr>CRS</abbr> if presents, or the 
datum ensemble otherwise.
-     * This is an alternative to the {@code of(…)} methods when the caller 
does not need to view the
-     * object as a datum.
+     * 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 none of the <var>source</var>
+     * or <var>target</var> datum ensembles met that criterion, then this 
method returns an empty value.
+     * A non-empty value means that it is okay, for low accuracy requirements, 
to ignore the datum shift.
      *
-     * @param  crs  the <abbr>CRS</abbr> from which to get the datum or 
ensemble, or {@code null}.
-     * @return the datum if present, or the datum ensemble otherwise, or 
{@code null}.
-     */
-    public static IdentifiedObject getDatumOrEnsemble(final SingleCRS crs) {
-        if (crs == null) return null;
-        final Datum datum = crs.getDatum();
-        if (datum != null) {
-            if (datum instanceof PseudoDatum<?>) {
-                return ((PseudoDatum) datum).ensemble;
-            }
-            return datum;
-        }
-        return crs.getDatumEnsemble();
+     * @param  source  the source <abbr>CRS</abbr> of a coordinate operation.
+     * @param  target  the target <abbr>CRS</abbr> of a coordinate operation.
+     * @return datum or pseudo-datum of the coordinate operation result if it 
is okay to ignore datum shift.
+     */
+    public static Optional<GeodeticDatum> ofOperation(final GeodeticCRS 
source, final GeodeticCRS target) {
+        return ofOperation(source, source.getDatum(),
+                           target, target.getDatum(),
+                           Geodetic::new);
     }
 
     /**
-     * Returns the inaccuracy that would have an operation using datum 
ensembles.
-     * This method makes the following choice:
+     * Returns the datum or pseudo-datum of the result of an operation between 
the given vertical <abbr>CRS</abbr>s.
+     * See {@link #ofOperation(GeodeticCRS, GeodeticCRS)} for more information.
      *
-     * <ul>
-     *   <li>If the two reference systems are associated to the same datum, 
returns an arbitrary value.</li>
-     *   <li>Otherwise, if the datum of one <abbr>CRS</abbr> is a member of 
the datum ensemble of the other
-     *       <abbr>CRS</abbr>, returns the ensemble accuracy.</li>
-     *   <li>OTherwise, if the datum ensemble of one <abbr>CRS</abbr> is fully 
contained in the datum ensemble
-     *       of the other <abbr>CRS</abbr>, returns the accuracy of the larger 
ensemble.</li>
-     *   <li>Otherwise, returns an empty value.</li>
-     * </ul>
+     * @param  source  the source <abbr>CRS</abbr> of a coordinate operation.
+     * @param  target  the target <abbr>CRS</abbr> of a coordinate operation.
+     * @return datum or pseudo-datum of the coordinate operation result if it 
is okay to ignore datum shift.
+     */
+    public static Optional<VerticalDatum> ofOperation(final VerticalCRS 
source, final VerticalCRS target) {
+        return ofOperation(source, source.getDatum(),
+                           target, target.getDatum(),
+                           Vertical::new);
+    }
+
+    /**
+     * Returns the datum or pseudo-datum of the result of an operation between 
the given temporal <abbr>CRS</abbr>s.
+     * See {@link #ofOperation(GeodeticCRS, GeodeticCRS)} for more information.
      *
-     * An empty value means that the two <abbr>CRS</abbr> are not compatible 
according the datum and datum ensemble
-     * properties. However, a transformation path may exist in a geodetic 
registry such as <abbr>EPSG</abbr>.
+     * @param  source  the source <abbr>CRS</abbr> of a coordinate operation.
+     * @param  target  the target <abbr>CRS</abbr> of a coordinate operation.
+     * @return datum or pseudo-datum of the coordinate operation result if it 
is okay to ignore datum shift.
+     */
+    public static Optional<TemporalDatum> ofOperation(final TemporalCRS 
source, final TemporalCRS target) {
+        return ofOperation(source, source.getDatum(),
+                           target, target.getDatum(),
+                           Time::new);
+    }
+
+    /**
+     * Returns the datum or pseudo-datum of the result of an operation between 
the given parametric <abbr>CRS</abbr>s.
+     * See {@link #ofOperation(GeodeticCRS, GeodeticCRS)} for more information.
      *
-     * @param  source  the first <abbr>CRS</abbr> for which to compare the 
datum.
-     * @param  target  the second <abbr>CRS</abbr> for which to compare the 
datum.
-     * @return a non-null value if it is okay, for low accuracy requirements, 
to ignore the datum shift.
-     */
-    public static Optional<PositionalAccuracy> getOperationAccuracy(final 
SingleCRS source, final SingleCRS target) {
-        final Datum sourceDatum = source.getDatum();
-        final Datum targetDatum = target.getDatum();
-        if (sourceDatum != null && targetDatum != null && 
Utilities.equalsIgnoreMetadata(sourceDatum, targetDatum)) {
-            return Optional.of(PositionalAccuracyConstant.SAME_DATUM_ENSEMBLE);
+     * @param  source  the source <abbr>CRS</abbr> of a coordinate operation.
+     * @param  target  the target <abbr>CRS</abbr> of a coordinate operation.
+     * @return datum or pseudo-datum of the coordinate operation result if it 
is okay to ignore datum shift.
+     */
+    public static Optional<ParametricDatum> ofOperation(final ParametricCRS 
source, final ParametricCRS target) {
+        return ofOperation(source, source.getDatum(),
+                           target, target.getDatum(),
+                           Parametric::new);
+    }
+
+    /**
+     * Returns the datum or pseudo-datum of the result of an operation between 
the given engineering <abbr>CRS</abbr>s.
+     * See {@link #ofOperation(GeodeticCRS, GeodeticCRS)} for more information.
+     *
+     * @param  source  the source <abbr>CRS</abbr> of a coordinate operation.
+     * @param  target  the target <abbr>CRS</abbr> of a coordinate operation.
+     * @return datum or pseudo-datum of the coordinate operation result if it 
is okay to ignore datum shift.
+     */
+    public static Optional<EngineeringDatum> ofOperation(final EngineeringCRS 
source, final EngineeringCRS target) {
+        return ofOperation(source, source.getDatum(),
+                           target, target.getDatum(),
+                           Engineering::new);
+    }
+
+    /**
+     * Returns the datum or pseudo-datum 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 a 
pseudo-datum for the largest ensemble which
+     * fully contains the datum or datum ensemble of the other 
<abbr>CRS</abbr>. If none of the <var>source</var>
+     * or <var>target</var> datum ensembles met that criterion, then this 
method returns an empty value.
+     * A non-empty value means that it is okay, for low accuracy requirements, 
to ignore the datum shift.
+     *
+     * @param  source       the source <abbr>CRS</abbr> of a coordinate 
operation.
+     * @param  sourceDatum  the datum of the source <abbr>CRS</abbr>.
+     * @param  target       the target <abbr>CRS</abbr> of a coordinate 
operation.
+     * @param  targetDatum  the datum of the target <abbr>CRS</abbr>.
+     * @param  constructor  function to invoke for wrapping a datum ensemble 
in a pseudo-datum.
+     * @return datum or pseudo-datum of the coordinate operation result if it 
is okay to ignore datum shift.
+     */
+    @SuppressWarnings("unchecked")          // Casts are safe because callers 
know the method signature of <D>.
+    private static <C extends SingleCRS, D extends Datum, R extends 
IdentifiedObject> Optional<R> ofOperation(
+            final C source, final R sourceDatum,
+            final C target, final R targetDatum,
+            final Function<DatumEnsemble<D>, R> constructor)
+    {
+        if (sourceDatum != null && Utilities.equalsIgnoreMetadata(sourceDatum, 
targetDatum)) {
+            return Optional.of(targetDatum);
         }
-        DatumEnsemble<?> sourceEnsemble;
-        DatumEnsemble<?> targetEnsemble;
-        PositionalAccuracy accuracy;
-        if ((accuracy = accuracyIfMember(sourceDatum, targetEnsemble = 
target.getDatumEnsemble())) != null ||
-            (accuracy = accuracyIfMember(targetDatum, sourceEnsemble = 
source.getDatumEnsemble())) != null ||
-            (sourceEnsemble == null || targetEnsemble == null))
+        DatumEnsemble<D> sourceEnsemble;
+        DatumEnsemble<D> targetEnsemble;
+        DatumEnsemble<D> selected;
+        if ((isMember(selected = targetEnsemble = (DatumEnsemble<D>) 
target.getDatumEnsemble(), sourceDatum)) ||
+            (isMember(selected = sourceEnsemble = (DatumEnsemble<D>) 
source.getDatumEnsemble(), targetDatum)))
         {
-            return Optional.of(accuracy);
-        }
-        var sources = sourceEnsemble.getMembers();
-        var targets = targetEnsemble.getMembers();
-        if (sources.size() > targets.size()) {
-            var tmp = targets;          // Want as if transforming from 
smaller ensemble to larger ensemble.
-            targets = sources;
-            sources = tmp;
-
-            var te = targetEnsemble;
-            targetEnsemble = sourceEnsemble;
-            sourceEnsemble = te;
+            return Optional.of(constructor.apply(selected));
         }
-        final Datum[] remaining = sources.toArray(Datum[]::new);
-        int count = remaining.length;
-        for (final Datum member : targets) {
-            for (int i=0; i<count; i++) {
-                if (Utilities.equalsIgnoreMetadata(member, remaining[i])) {
-                    System.arraycopy(remaining, i+1, remaining, i, --count - 
i);
-                    if (count == 0) {
-                        /*
-                         * Found all members of the smaller ensemble. Take the 
accuracy
-                         * of the larger ensemble, as it contains the smaller 
ensemble.
-                         */
-                        if ((accuracy = targetEnsemble.getEnsembleAccuracy()) 
== null &&
-                            (accuracy = sourceEnsemble.getEnsembleAccuracy()) 
== null) {
-                             accuracy = 
PositionalAccuracyConstant.SAME_DATUM_ENSEMBLE;
+        if (sourceEnsemble != null && targetEnsemble != null) {
+            selected = targetEnsemble;
+            Collection<D> large = targetEnsemble.getMembers();
+            Collection<D> small = sourceEnsemble.getMembers();
+            if (small.size() > large.size()) {
+                selected = sourceEnsemble;
+                var t = large;
+                large = small;
+                small = t;
+            }
+            small = new ArrayDeque<>(small);
+            for (final Datum member : large) {
+                final Iterator<D> it = small.iterator();
+                while (it.hasNext()) {
+                    if (Utilities.equalsIgnoreMetadata(member, it.next())) {
+                        it.remove();
+                        if (small.isEmpty()) {
+                            /*
+                             * Found all members of the smaller ensemble. Take 
the larger ensemble,
+                             * as it contains both ensembles and should have 
conservative accuracy.
+                             */
+                            return Optional.of(constructor.apply(selected));
                         }
-                        return Optional.of(accuracy);
+                        break;      // For removing only the first match.
                     }
-                    break;      // For removing only the first match.
                 }
             }
         }
@@ -288,23 +335,84 @@ public abstract class PseudoDatum<D extends Datum> 
implements Datum, LenientComp
     }
 
     /**
-     * If the given datum is a member of the given ensemble, returns the 
ensemble accuracy.
-     * Otherwise, returns {@code null}.
+     * Returns whether the given datum is a member of the given ensemble.
      *
      * @param  datum     the datum to test, or {@code null}.
      * @param  ensemble  the ensemble to test, or {@code null}.
-     * @return a non-null value if the datum is a member of the given ensemble.
+     * @return whether the ensemble contains the given datum.
      */
-    private static PositionalAccuracy accuracyIfMember(final Datum datum, 
final DatumEnsemble<?> ensemble) {
+    private static boolean isMember(final DatumEnsemble<?> ensemble, final 
IdentifiedObject datum) {
         if (ensemble != null) {
             for (final Datum member : ensemble.getMembers()) {
                 if (Utilities.equalsIgnoreMetadata(datum, member)) {
-                    PositionalAccuracy accuracy = 
ensemble.getEnsembleAccuracy();
-                    return (accuracy != null) ? accuracy : 
PositionalAccuracyConstant.SAME_DATUM_ENSEMBLE;
+                    return true;
                 }
             }
         }
-        return null;
+        return false;
+    }
+
+    /**
+     * 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 none of the 
<var>source</var> or <var>target</var>
+     * datum ensembles met that criterion, then this method returns an empty 
value.
+     * A non-empty value means that it is okay, for low accuracy requirements, 
to ignore the datum shift.
+     *
+     * <p>This is an alternative to the {@code ofOperation(…)} methods when 
the caller does not need to view
+     * the returned object as a datum.</p>
+     *
+     * @param  source  the source <abbr>CRS</abbr> of a coordinate operation, 
or {@code null}.
+     * @param  target  the target <abbr>CRS</abbr> of a coordinate operation, 
or {@code null}.
+     * @return datum or datum ensemble of the coordinate operation result if 
it is okay to ignore datum shift.
+     */
+    public static Optional<IdentifiedObject> getDatumOrEnsemble(final 
SingleCRS source, final SingleCRS target) {
+        if (source == null) return 
Optional.ofNullable(getDatumOrEnsemble(target));
+        if (target == null) return 
Optional.ofNullable(getDatumOrEnsemble(source));
+        return ofOperation(source, source.getDatum(),
+                           target, target.getDatum(),
+                           (ensemble) -> ensemble);
+    }
+
+    /**
+     * Returns the datum of the given <abbr>CRS</abbr> if presents, or the 
datum ensemble otherwise.
+     * This is an alternative to the {@code of(…)} methods when the caller 
does not need to view the
+     * returned object as a datum.
+     *
+     * @param  crs  the <abbr>CRS</abbr> from which to get the datum or 
ensemble, or {@code null}.
+     * @return the datum if present, or the datum ensemble otherwise, or 
{@code null}.
+     */
+    public static IdentifiedObject getDatumOrEnsemble(final SingleCRS crs) {
+        if (crs == null) return null;
+        final Datum datum = crs.getDatum();
+        if (datum != null) {
+            if (datum instanceof PseudoDatum<?>) {
+                return ((PseudoDatum) datum).ensemble;
+            }
+            return datum;
+        }
+        return crs.getDatumEnsemble();
+    }
+
+    /**
+     * If the given object is a datum ensemble or a wrapper for a datum 
ensemble, returns its accuracy.
+     * This method recognizes the {@link DatumEnsemble} and {@link 
PseudoDatum} types.
+     *
+     * @param  object  the object from which to get the ensemble accuracy, or 
{@code null}.
+     * @return the datum ensemble accuracy if the given object is a datum 
ensemble or a wrapper.
+     * @throws NullPointerException if the given object should provide an 
accuracy but didn't.
+     */
+    public static Optional<PositionalAccuracy> getEnsembleAccuracy(final 
IdentifiedObject object) {
+        final DatumEnsemble<?> ensemble;
+        if (object instanceof DatumEnsemble<?>) {
+            ensemble = (DatumEnsemble<?>) object;
+        } else if (object instanceof PseudoDatum<?>) {
+            ensemble = ((PseudoDatum<?>) object).ensemble;
+        } else {
+            return Optional.empty();
+        }
+        return Optional.of(ensemble.getEnsembleAccuracy());     // Intentional 
NullPointerException if this property is null.
     }
 
     /**
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 a01cbe8868..9df0ff8833 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
@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.ListIterator;
+import java.util.Optional;
 import java.time.Duration;
 import javax.measure.Unit;
 import javax.measure.IncommensurableException;
@@ -64,11 +65,11 @@ import 
org.apache.sis.referencing.operation.provider.Geographic3Dto2D;
 import org.apache.sis.referencing.operation.provider.GeographicToGeocentric;
 import org.apache.sis.referencing.operation.provider.GeocentricToGeographic;
 import org.apache.sis.referencing.operation.provider.GeocentricAffine;
+import org.apache.sis.util.Utilities;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.DoubleDouble;
 import org.apache.sis.util.resources.Vocabulary;
-import static org.apache.sis.util.Utilities.equalsIgnoreMetadata;
 
 
 /**
@@ -233,8 +234,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
     {
         ArgumentChecks.ensureNonNull("sourceCRS", sourceCRS);
         ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
-        if (equalsIgnoreMetadata(sourceCRS, targetCRS)) try {
-            return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, 
targetCRS,
+        if (Utilities.equalsIgnoreMetadata(sourceCRS, targetCRS)) try {
+            return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, 
targetCRS, null,
                             
CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(),
                                                                
targetCRS.getCoordinateSystem())));
         } catch (IllegalArgumentException | IncommensurableException e) {
@@ -339,14 +340,16 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         ////                                                                   
     ////
         
////////////////////////////////////////////////////////////////////////////////
         if (sourceCRS instanceof SingleCRS && targetCRS instanceof SingleCRS) {
-            final IdentifiedObject sourceDatum = 
PseudoDatum.getDatumOrEnsemble((SingleCRS) sourceCRS);
-            final IdentifiedObject targetDatum = 
PseudoDatum.getDatumOrEnsemble((SingleCRS) targetCRS);
-            if (equalsIgnoreMetadata(sourceDatum, targetDatum)) try {
+            final Optional<IdentifiedObject> datumOrEnsemble =
+                    PseudoDatum.getDatumOrEnsemble((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,
+                                
PseudoDatum.getEnsembleAccuracy(datumOrEnsemble.get()).orElse(null),
                                 
CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(),
                                                                    
targetCRS.getCoordinateSystem())));
             } catch (IllegalArgumentException | IncommensurableException e) {
@@ -539,7 +542,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
          */
         Identifier identifier;
         boolean isGeographicToGeocentric = false;
-        if (equalsIgnoreMetadata(sourceDatum, targetDatum)) {
+        final Optional<GeodeticDatum> commonDatum = 
PseudoDatum.ofOperation(sourceCRS, targetCRS);
+        if (commonDatum.isPresent()) {
             final boolean isGeocentricToGeographic;
             isGeographicToGeocentric = (sourceCS instanceof EllipsoidalCS && 
targetCS instanceof CartesianCS);
             isGeocentricToGeographic = (sourceCS instanceof CartesianCS && 
targetCS instanceof EllipsoidalCS);
@@ -563,7 +567,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
             }
         }
         /*
-         * Conceptually, all transformations below could done by first 
converting from the source coordinate
+         * Conceptually, all transformations below could be done by first 
converting from source coordinate
          * system to geocentric Cartesian coordinates (X,Y,Z), apply an affine 
transform represented by the
          * datum shift matrix, then convert from the (X′,Y′,Z′) coordinates to 
the target coordinate system.
          * However, there are two exceptions to this path:
@@ -693,10 +697,12 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
          * specified datum.
          */
         final Map<String, Object> properties = properties(identifier);
-        if (datumShift instanceof AnnotatedMatrix) {
-            
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, new 
PositionalAccuracy[] {
-                ((AnnotatedMatrix) datumShift).accuracy
-            });
+        PositionalAccuracy accuracy = 
commonDatum.flatMap(PseudoDatum::getEnsembleAccuracy).orElse(null);
+        if (accuracy == null && datumShift instanceof AnnotatedMatrix) {
+            accuracy = ((AnnotatedMatrix) datumShift).accuracy;
+        }
+        if (accuracy != null) {
+            
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, accuracy);
         }
         return asList(createFromMathTransform(properties, sourceCRS, 
targetCRS, transform, method, parameters, null));
     }
@@ -740,7 +746,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         CoordinateSystem interpolationCS = 
interpolationCRS.getCoordinateSystem();
         if (!(interpolationCS instanceof EllipsoidalCS)) {
             final EllipsoidalCS cs = 
CommonCRS.WGS84.geographic3D().getCoordinateSystem();
-            if (!equalsIgnoreMetadata(interpolationCS, cs)) {
+            if (!Utilities.equalsIgnoreMetadata(interpolationCS, cs)) {
                 final GeographicCRS stepCRS = factorySIS.crsFactory
                         .createGeographicCRS(derivedFrom(sourceCRS), 
sourceCRS.getDatum(), sourceCRS.getDatumEnsemble(), cs);
                 step1 = createOperation(sourceCRS, 
toAuthorityDefinition(GeographicCRS.class, stepCRS));
@@ -765,12 +771,12 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         final boolean isEllipsoidalHeight;      // Whether heightCRS is okay 
or need to be recreated.
         VerticalCRS heightCRS = targetCRS;      // First candidate, will be 
replaced if it doesn't fit.
         VerticalCS  heightCS  = heightCRS.getCoordinateSystem();
-        if (equalsIgnoreMetadata(heightCS.getAxis(0), expectedAxis)) {
+        if (Utilities.equalsIgnoreMetadata(heightCS.getAxis(0), expectedAxis)) 
{
             isEllipsoidalHeight = 
ReferencingUtilities.isEllipsoidalHeight(PseudoDatum.of(heightCRS));
         } else {
             heightCRS = CommonCRS.Vertical.ELLIPSOIDAL.crs();
             heightCS  = heightCRS.getCoordinateSystem();
-            isEllipsoidalHeight = equalsIgnoreMetadata(heightCS.getAxis(0), 
expectedAxis);
+            isEllipsoidalHeight = 
Utilities.equalsIgnoreMetadata(heightCS.getAxis(0), expectedAxis);
             if (!isEllipsoidalHeight) {
                 heightCS = toAuthorityDefinition(VerticalCS.class, 
factorySIS.csFactory
                         .createVerticalCS(derivedFrom(heightCS), 
expectedAxis));
@@ -800,7 +806,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         final Matrix matrix = Matrices.createZero(tgtDim + 1, srcDim + 1);
         matrix.setElement(0,      i,      1);                                  
     // Scale factor for height.
         matrix.setElement(tgtDim, srcDim, 1);                                  
     // Always 1 for affine transform.
-        step2 = createFromAffineTransform(AXIS_CHANGES, interpolationCRS, 
heightCRS, matrix);
+        step2 = createFromAffineTransform(AXIS_CHANGES, interpolationCRS, 
heightCRS, null, matrix);
         return asList(concatenate(step1, step2, step3));
     }
 
@@ -824,10 +830,10 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
                                                             final VerticalCRS 
targetCRS)
             throws FactoryException
     {
-        final VerticalDatum sourceDatum = PseudoDatum.of(sourceCRS);
-        final VerticalDatum targetDatum = PseudoDatum.of(targetCRS);
-        if (!equalsIgnoreMetadata(sourceDatum, targetDatum)) {
-            throw new OperationNotFoundException(notFoundMessage(sourceDatum, 
targetDatum));
+        final Optional<VerticalDatum> commonDatum = 
PseudoDatum.ofOperation(sourceCRS, targetCRS);
+        if (commonDatum.isEmpty()) {
+            throw new 
OperationNotFoundException(notFoundMessage(PseudoDatum.getDatumOrEnsemble(sourceCRS),
+                                                                 
PseudoDatum.getDatumOrEnsemble(targetCRS)));
         }
         final VerticalCS sourceCS = sourceCRS.getCoordinateSystem();
         final VerticalCS targetCS = targetCRS.getCoordinateSystem();
@@ -837,7 +843,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         } catch (IllegalArgumentException | IncommensurableException 
exception) {
             throw new OperationNotFoundException(notFoundMessage(sourceCRS, 
targetCRS), exception);
         }
-        return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, 
targetCRS, matrix));
+        return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, 
targetCRS,
+                
PseudoDatum.getEnsembleAccuracy(commonDatum.get()).orElse(null), matrix));
     }
 
     /**
@@ -849,6 +856,14 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
      * But a list is returned because the same step may be implemented by 
different operation methods. Only one element
      * in the returned list should be selected (usually the first one).</p>
      *
+     * @todo The current version performs only unit conversion and change of 
datum epoch,
+     *       assuming that everything else is equivalent. This is probably not 
sufficient,
+     *       as the relationship between two temporal datum may be non-linear.
+     *       We should invoke {@link PseudoDatum#ofOperation(TemporalCRS, 
TemporalCRS)}
+     *       for checking if the datum are equivalent, but doing so require 
that we remove
+     *       the origin attribute from {@link TemporalDatum}.
+     *       This change has been proposed to <abbr>OGC</abbr>.
+     *
      * @param  sourceCRS  input coordinate reference system.
      * @param  targetCRS  output coordinate reference system.
      * @return a coordinate operation from {@code sourceCRS} to {@code 
targetCRS}.
@@ -889,7 +904,7 @@ 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, matrix));
+        return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, 
targetCRS, null, matrix));
     }
 
     /**
@@ -955,7 +970,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
                 CompoundCRS crs = 
factorySIS.crsFactory.createCompoundCRS(derivedFrom(sourceCRS), stepComponents);
                 stepSourceCRS = 
toAuthorityDefinition(CoordinateReferenceSystem.class, crs);
             }
-            operation = createFromAffineTransform(AXIS_CHANGES, sourceCRS, 
stepSourceCRS, select);
+            operation = createFromAffineTransform(AXIS_CHANGES, sourceCRS, 
stepSourceCRS, null, select);
         }
         /*
          * For each sub-operation, create a PassThroughOperation for the 
(stepSourceCRS → stepTargetCRS) operation.
@@ -1016,7 +1031,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
             final Matrix m = SubOperationInfo.createConstantOperation(infos, 
stepComponents.length,
                     stepSourceCRS.getCoordinateSystem().getDimension(),
                         targetCRS.getCoordinateSystem().getDimension());
-            operation = concatenate(operation, 
createFromAffineTransform(CONSTANTS, stepSourceCRS, targetCRS, m));
+            operation = concatenate(operation, 
createFromAffineTransform(CONSTANTS, stepSourceCRS, targetCRS, null, m));
         }
         return asList(operation);
     }
@@ -1042,6 +1057,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
      * @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.
      * @return the conversion or transformation.
      * @throws FactoryException if the operation cannot be created.
@@ -1049,11 +1065,16 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
     private CoordinateOperation createFromAffineTransform(final Identifier     
           name,
                                                           final 
CoordinateReferenceSystem sourceCRS,
                                                           final 
CoordinateReferenceSystem targetCRS,
+                                                          final 
PositionalAccuracy        accuracy,
                                                           final Matrix         
           matrix)
             throws FactoryException
     {
         final MathTransform transform  = 
factorySIS.getMathTransformFactory().createAffineTransform(matrix);
-        return createFromMathTransform(properties(name), sourceCRS, targetCRS, 
transform, null, null, null);
+        final Map<String, Object> properties = properties(name);
+        if (accuracy != null) {
+            
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, accuracy);
+        }
+        return createFromMathTransform(properties, sourceCRS, targetCRS, 
transform, null, null, null);
     }
 
     /**
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 c8bf82dacf..35a95e6a39 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
@@ -36,7 +36,6 @@ import org.opengis.util.NoSuchIdentifierException;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.extent.Extent;
 import org.opengis.metadata.citation.Citation;
-import org.opengis.metadata.quality.PositionalAccuracy;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.referencing.IdentifiedObject;
@@ -1249,9 +1248,9 @@ class CoordinateOperationRegistry {
         final Map<String,Object> properties = new HashMap<>(4);
         properties.put(CoordinateOperation.NAME_KEY, name);
         if ((name == DATUM_SHIFT) || (name == ELLIPSOID_CHANGE)) {
-            
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, new 
PositionalAccuracy[] {
-                      (name == DATUM_SHIFT) ? 
PositionalAccuracyConstant.DATUM_SHIFT_APPLIED
-                                            : 
PositionalAccuracyConstant.DATUM_SHIFT_OMITTED});
+            
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY,
+                    (name == DATUM_SHIFT) ? 
PositionalAccuracyConstant.DATUM_SHIFT_APPLIED
+                                          : 
PositionalAccuracyConstant.DATUM_SHIFT_OMITTED);
         }
         return properties;
     }
@@ -1330,7 +1329,7 @@ class CoordinateOperationRegistry {
          * source and target CRS) are compatible with the specified ones, then 
that operation is returned as-is.
          */
         if (transform instanceof CoordinateOperation) {
-            final CoordinateOperation operation = (CoordinateOperation) 
transform;
+            final var operation = (CoordinateOperation) transform;
             if (Objects.equals(operation.getSourceCRS(),     sourceCRS) &&
                 Objects.equals(operation.getTargetCRS(),     targetCRS) &&
                 Objects.equals(operation.getMathTransform(), transform) &&
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
index 40c87d28a8..9a09d6cb98 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
@@ -19,6 +19,8 @@ package org.apache.sis.referencing.operation;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.List;
+import java.util.ArrayDeque;
+import java.util.Iterator;
 import java.util.Objects;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterValueGroup;
@@ -45,7 +47,6 @@ import org.apache.sis.referencing.privy.CoordinateOperations;
 import org.apache.sis.referencing.privy.ReferencingFactoryContainer;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Classes;
-import org.apache.sis.util.Utilities;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.collection.WeakHashSet;
@@ -407,22 +408,16 @@ public class DefaultCoordinateOperationFactory extends 
AbstractFactory implement
     private static boolean isConversion(final CoordinateReferenceSystem 
sourceCRS,
                                         final CoordinateReferenceSystem 
targetCRS)
     {
-        List<SingleCRS> components = CRS.getSingleComponents(sourceCRS);
-        int n = components.size();                      // Number of remaining 
datum from sourceCRS to verify.
-        final IdentifiedObject[] datum = new IdentifiedObject[n];
-        for (int i=0; i<n; i++) {
-            datum[i] = PseudoDatum.getDatumOrEnsemble(components.get(i));
-        }
-        components = CRS.getSingleComponents(targetCRS);
-next:   for (int i=components.size(); --i >= 0;) {
-            final IdentifiedObject d = 
PseudoDatum.getDatumOrEnsemble(components.get(i));
-            for (int j=n; --j >= 0;) {
-                if (Utilities.equalsIgnoreMetadata(d, datum[j])) {
-                    System.arraycopy(datum, j+1, datum, j, --n - j);    // 
Remove the datum from the list.
+        final var components = new 
ArrayDeque<>(CRS.getSingleComponents(sourceCRS));
+next:   for (SingleCRS component : CRS.getSingleComponents(targetCRS)) {
+            final Iterator<SingleCRS> it = components.iterator();
+            while (it.hasNext()) {
+                if (PseudoDatum.getDatumOrEnsemble(component, 
it.next()).isPresent()) {
+                    it.remove();
                     continue next;
                 }
             }
-            return false;                               // Datum from 
`targetCRS` not found in `sourceCRS`.
+            return false;       // Datum from `targetCRS` not found in 
`sourceCRS`.
         }
         return true;
     }


Reply via email to