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

commit 3bbf94a0042b0a2e40bf1623eeda0bdd448eca0c
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Jul 17 12:50:56 2024 +0200

    Modification in the implementation strategy of datum ensemble:
    allow a datum ensemble to be viewed as a pseudo-datum.
    This is trick for reducing the amount of null checks.
---
 .../main/org/apache/sis/io/wkt/VerticalInfo.java   |   5 +-
 .../sis/referencing/AbstractIdentifiedObject.java  |   7 +-
 .../main/org/apache/sis/referencing/CommonCRS.java |  37 +-
 .../referencing/EllipsoidalHeightSeparator.java    |   4 +-
 .../sis/referencing/MultiRegisterOperations.java   |  31 +-
 .../apache/sis/referencing/crs/AbstractCRS.java    |  81 +--
 .../sis/referencing/crs/AbstractSingleCRS.java     | 314 +++++++++++
 .../sis/referencing/crs/DefaultDerivedCRS.java     |  22 +-
 .../sis/referencing/crs/DefaultEngineeringCRS.java |  46 +-
 .../sis/referencing/crs/DefaultGeocentricCRS.java  |   2 +-
 .../sis/referencing/crs/DefaultGeodeticCRS.java    |  44 +-
 .../sis/referencing/crs/DefaultGeographicCRS.java  | 102 ++--
 .../sis/referencing/crs/DefaultImageCRS.java       |  28 +-
 .../sis/referencing/crs/DefaultParametricCRS.java  |  46 +-
 .../sis/referencing/crs/DefaultProjectedCRS.java   |  23 +-
 .../sis/referencing/crs/DefaultTemporalCRS.java    |  71 +--
 .../sis/referencing/crs/DefaultVerticalCRS.java    |  46 +-
 .../referencing/datum/DefaultDatumEnsemble.java    |  14 +-
 .../apache/sis/referencing/datum/PseudoDatum.java  | 589 +++++++++++++++++++++
 .../operation/CoordinateOperationFinder.java       |  21 +-
 .../operation/CoordinateOperationRegistry.java     |  12 +-
 .../DefaultCoordinateOperationFactory.java         |   6 +-
 .../sis/referencing/privy/DefinitionVerifier.java  |   5 +-
 .../privy/EllipsoidalHeightCombiner.java           |   3 +-
 .../referencing/privy/GeodeticObjectBuilder.java   |  21 +-
 .../referencing/privy/ReferencingUtilities.java    |  44 +-
 .../sis/storage/geotiff/reader/CRSBuilder.java     |   5 +-
 .../sis/storage/geotiff/writer/GeoEncoder.java     |  65 ++-
 .../main/org/apache/sis/storage/csv/Store.java     |   9 +-
 .../org/apache/sis/gui/map/OperationFinder.java    |   5 +-
 .../main/org/apache/sis/gui/referencing/Utils.java |   6 +-
 31 files changed, 1219 insertions(+), 495 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/VerticalInfo.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/VerticalInfo.java
index a11f49dbec..5db9ddb99c 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/VerticalInfo.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/VerticalInfo.java
@@ -27,7 +27,7 @@ import org.opengis.referencing.cs.CSFactory;
 import org.opengis.referencing.cs.VerticalCS;
 import org.opengis.referencing.crs.CRSFactory;
 import org.opengis.referencing.crs.VerticalCRS;
-import org.opengis.referencing.datum.VerticalDatum;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.metadata.privy.AxisNames;
 import org.apache.sis.metadata.iso.extent.DefaultExtent;
 import org.apache.sis.metadata.iso.extent.DefaultVerticalExtent;
@@ -109,8 +109,7 @@ final class VerticalInfo {
      */
     final VerticalInfo resolve(final VerticalCRS crs) {
         if (crs != null) {
-            final VerticalDatum datum = crs.getDatum();
-            if (datum != null && datum.getRealizationMethod().orElse(null) == 
RealizationMethod.GEOID) {
+            if (PseudoDatum.of(crs).getRealizationMethod().orElse(null) == 
RealizationMethod.GEOID) {
                 return resolve(crs, crs.getCoordinateSystem().getAxis(0));
             }
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java
index 679b1ed56e..aefc4a31d1 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java
@@ -449,10 +449,13 @@ public class AbstractIdentifiedObject extends 
FormattableObject implements Ident
      *
      * <p>This constructor performs a shallow copy, i.e. the properties are 
not cloned.</p>
      *
-     * @param object  the object to shallow copy.
+     * @param  object  the object to shallow copy.
      */
     protected AbstractIdentifiedObject(final IdentifiedObject object) {
-        name        =          object.getName();
+        name = object.getName();
+        if (name == null) {
+            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.MissingValueForProperty_1, 
NAME_KEY));
+        }
         alias       = nonEmpty(object.getAlias()); // Favor null for empty set 
in case it is not Collections.EMPTY_SET
         identifiers = nonEmpty(object.getIdentifiers());
         domains     = nonEmpty(object.getDomains());
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
index b8331ff590..6d4457e1bd 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
@@ -67,11 +67,11 @@ import org.apache.sis.referencing.crs.DefaultGeocentricCRS;
 import org.apache.sis.referencing.crs.DefaultEngineeringCRS;
 import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
 import org.apache.sis.referencing.factory.UnavailableFactoryException;
-import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.referencing.operation.provider.TransverseMercator;
 import org.apache.sis.referencing.privy.ReferencingUtilities;
 import org.apache.sis.referencing.privy.Formulas;
 import org.apache.sis.referencing.internal.Resources;
+import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.system.SystemListener;
 import org.apache.sis.system.Modules;
 import org.apache.sis.util.OptionalCandidate;
@@ -501,7 +501,7 @@ public enum CommonCRS {
         }
         final Datum datum = single.getDatum();
         if (datum instanceof GeodeticDatum) {
-            final CommonCRS c = forDatum((GeodeticDatum) datum);
+            final CommonCRS c = forDatum((GeodeticDatum) datum, 
single.getDatumEnsemble());
             if (c != null) return c;
         }
         throw new IllegalArgumentException(Errors.format(
@@ -511,10 +511,11 @@ public enum CommonCRS {
     /**
      * Returns the {@code CommonCRS} enumeration value for the given datum, or 
{@code null} if none.
      *
-     * @param  datum  the datum to represent as an enumeration value, or 
{@code null}.
+     * @param  datum     the datum to represent as an enumeration value, or 
{@code null}.
+     * @param  ensemble  the datum ensemble to represent as an enumeration 
value, or {@code null}.
      * @return enumeration value for the given datum, or {@code null} if none.
      */
-    static CommonCRS forDatum(final GeodeticDatum datum) {
+    static CommonCRS forDatum(final GeodeticDatum datum, final 
DatumEnsemble<?> ensemble) {
         /*
          * First, try to search using only the EPSG code. This approach avoid 
initializing unneeded
          * geodetic objects (such initializations are costly if they require 
connection to the EPSG
@@ -531,7 +532,15 @@ public enum CommonCRS {
             }
         }
         for (final CommonCRS c : values()) {
-            if ((epsg != 0) ? c.datum == epsg : 
Utilities.equalsIgnoreMetadata(c.datum(), datum)) {
+            final boolean filter;
+            if (epsg != 0) {
+                filter = c.datum == epsg;
+            } else if (datum != null) {
+                filter = Utilities.equalsIgnoreMetadata(c.datum(), datum);
+            } else {
+                filter = Utilities.equalsIgnoreMetadata(c.datumEnsemble(), 
ensemble);
+            }
+            if (filter) {
                 return c;
             }
         }
@@ -2063,6 +2072,24 @@ public enum CommonCRS {
         public EngineeringDatum datum() {
             return datum;
         }
+
+        /**
+         * Returns {@code true} is the given <abbr>CRS</abbr> uses the datum 
identified by this enumeration value.
+         * The association may be direct through {@link SingleCRS#getDatum()}, 
or indirect throw at least one of
+         * the members of {@link SingleCRS#getDatumEnsemble()}.
+         *
+         * @param  crs  the CRS to compare against the datum of this 
enumeration value. May be {@code null}.
+         * @return whether the given <abbr>CRS</abbr> uses the datum, directly 
or indirectly.
+         * @since 1.5
+         */
+        public boolean datumUsedBy(final CoordinateReferenceSystem crs) {
+            for (final SingleCRS component : CRS.getSingleComponents(crs)) {
+                if (ReferencingUtilities.uses(component, datum)) {
+                    return true;
+                }
+            }
+            return false;
+        }
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
index e4ca8f5200..4dfad99d15 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
@@ -127,10 +127,10 @@ final class EllipsoidalHeightSeparator implements 
AxisFilter {
             }
             final CommonCRS ref = CommonCRS.WGS84;
             if 
(Utilities.equalsIgnoreMetadata(ref.geographic().getCoordinateSystem(), cs)) {
-                final CommonCRS c = CommonCRS.forDatum(datum);
+                final CommonCRS c = CommonCRS.forDatum(datum, ensemble);
                 if (c != null) return c.geographic();
             } else if 
(Utilities.equalsIgnoreMetadata(ref.normalizedGeographic().getCoordinateSystem(),
 cs)) {
-                final CommonCRS c = CommonCRS.forDatum(datum);
+                final CommonCRS c = CommonCRS.forDatum(datum, ensemble);
                 if (c != null) return c.normalizedGeographic();
             }
             return 
factory().createGeographicCRS(getPropertiesForModifiedCRS(crs), datum, 
ensemble, (EllipsoidalCS) cs);
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 25b4aaede9..9b8ebfef7e 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
@@ -38,11 +38,13 @@ import 
org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.CoordinateOperationFactory;
 import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
 import org.opengis.referencing.operation.MathTransformFactory;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.factory.GeodeticObjectFactory;
 import org.apache.sis.referencing.factory.MultiAuthoritiesFactory;
 import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException;
 import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
 import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+import org.apache.sis.util.Utilities;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.iso.AbstractFactory;
@@ -50,7 +52,6 @@ import org.apache.sis.util.iso.AbstractFactory;
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.referencing.RegisterOperations;
 import org.opengis.referencing.crs.SingleCRS;
-import org.opengis.referencing.crs.CompoundCRS;
 import org.apache.sis.referencing.privy.ReferencingUtilities;
 
 
@@ -342,22 +343,24 @@ public class MultiRegisterOperations extends 
AbstractFactory implements Register
     public boolean areMembersOfSameEnsemble(CoordinateReferenceSystem source, 
CoordinateReferenceSystem target)
             throws FactoryException
     {
-        if (source instanceof SingleCRS && target instanceof SingleCRS) {
-            return ReferencingUtilities.areMembersOfSameEnsemble((SingleCRS) 
source, (SingleCRS) target);
+        final List<SingleCRS> sources = CRS.getSingleComponents(source);
+        final List<SingleCRS> targets = CRS.getSingleComponents(target);
+        final int n = targets.size();
+        if (sources.size() != n) {
+            return false;
         }
-        if (source instanceof CompoundCRS && target instanceof CompoundCRS) {
-            final List<SingleCRS> sources = ((CompoundCRS) 
source).getSingleComponents();
-            final List<SingleCRS> targets = ((CompoundCRS) 
target).getSingleComponents();
-            final int n = targets.size();
-            if (sources.size() == n) {
-                for (int i=0; i<n; i++) {
-                    if 
(!ReferencingUtilities.areMembersOfSameEnsemble(sources.get(i), 
targets.get(i))) {
-                        return false;
-                    }
-                }
+        for (int i=0; i<n; i++) {
+            final var crs1 = sources.get(i);
+            final var crs2 = targets.get(i);
+            if 
(!(Utilities.equalsIgnoreMetadata(PseudoDatum.getDatumOrEnsemble(crs1),
+                                                 
PseudoDatum.getDatumOrEnsemble(crs2))
+                    || ReferencingUtilities.uses(crs1, crs2.getDatum())
+                    || ReferencingUtilities.uses(crs2, crs1.getDatum())))
+            {
+                return false;
             }
         }
-        return false;
+        return true;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractCRS.java
index 30f4513010..486ff17472 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractCRS.java
@@ -31,16 +31,13 @@ import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.referencing.AbstractReferenceSystem;
-import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.cs.AbstractCS;
 import org.apache.sis.referencing.cs.AxesConvention;
-import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.referencing.privy.WKTUtilities;
 import org.apache.sis.referencing.privy.ReferencingUtilities;
 import org.apache.sis.metadata.privy.ImplementationHelper;
 import org.apache.sis.io.wkt.Convention;
 import org.apache.sis.io.wkt.Formatter;
-import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.resources.Errors;
@@ -50,7 +47,6 @@ import org.opengis.metadata.Identifier;
 
 // Specific to the geoapi-4.0 branch:
 import org.opengis.referencing.crs.DerivedCRS;
-import org.opengis.referencing.datum.DatumEnsemble;
 import org.opengis.coordinate.MismatchedDimensionException;
 
 
@@ -96,13 +92,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
 @XmlType(name = "AbstractCRSType")
 @XmlRootElement(name = "AbstractCRS")
 @XmlSeeAlso({
-    AbstractDerivedCRS.class,
-    DefaultGeodeticCRS.class,
-    DefaultVerticalCRS.class,
-    DefaultTemporalCRS.class,
-    DefaultParametricCRS.class,
-    DefaultEngineeringCRS.class,
-    DefaultImageCRS.class,
+    AbstractSingleCRS.class,
     DefaultCompoundCRS.class
 })
 public class AbstractCRS extends AbstractReferenceSystem implements 
CoordinateReferenceSystem {
@@ -115,7 +105,7 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
      * The coordinate system.
      *
      * <p><b>Consider this field as final!</b>
-     * This field is modified only at unmarshalling time by {@link 
#setCoordinateSystem(String, CoordinateSystem)}</p>
+     * This field is modified only at unmarshalling time by {@link 
#setCoordinateSystem(String, CoordinateSystem)}.</p>
      *
      * @see #getCoordinateSystem()
      */
@@ -134,13 +124,13 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
 
     /**
      * Creates the value to assign to the {@link #forConvention} map by 
constructors.
+     * {@code this} instance will be the <abbr>CRS</abbr> to declare as the 
original one.
      *
-     * @param  original  the coordinate system to declare as the original one.
      * @return map to assign to the {@link #forConvention} field.
      */
-    private static EnumMap<AxesConvention,AbstractCRS> forConvention(final 
AbstractCRS original) {
+    private EnumMap<AxesConvention,AbstractCRS> forConvention() {
         var m = new EnumMap<AxesConvention,AbstractCRS>(AxesConvention.class);
-        m.put(AxesConvention.ORIGINAL, original);
+        m.put(AxesConvention.ORIGINAL, this);
         return m;
     }
 
@@ -186,7 +176,7 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
     public AbstractCRS(final Map<String,?> properties, final CoordinateSystem 
cs) {
         super(properties);
         coordinateSystem = Objects.requireNonNull(cs);
-        forConvention = forConvention(this);
+        forConvention = forConvention();
     }
 
     /**
@@ -211,36 +201,10 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
                 Errors.Keys.MismatchedDimension_3, "cs", expected, actual));
     }
 
-    /**
-     * Verifies the consistency between the datum and the ensemble.
-     * At least one of the {@code datum} and {@code ensemble} arguments shall 
be non-null.
-     *
-     * @param  datum       the datum, or {@code null} if the CRS is associated 
only to a datum ensemble.
-     * @param  ensemble    collection of reference frames which for low 
accuracy requirements may be considered to be
-     *                     insignificantly different from each other, or 
{@code null} if there is no such ensemble.
-     * @throws NullPointerException if both arguments are null.
-     * @throws IllegalArgumentException if the datum is not a member of the 
ensemble.
-     */
-    static <D extends Datum> void checkDatum(final D datum, final 
DatumEnsemble<D> ensemble) {
-        if (ensemble == null) {
-            ArgumentChecks.ensureNonNull("datum", datum);
-        } else if (datum != null) {
-            for (final D member : ensemble.getMembers()) {
-                if (Utilities.equalsIgnoreMetadata(datum, member)) {
-                    return;
-                }
-            }
-            throw new 
IllegalArgumentException(Resources.format(Resources.Keys.NotAMemberOfDatumEnsemble_2,
-                    IdentifiedObjects.getDisplayName(ensemble), 
IdentifiedObjects.getDisplayName(datum)));
-        } else {
-            ArgumentChecks.ensureNonEmpty("ensemble", ensemble.getMembers());
-        }
-    }
-
     /**
      * Creates a new CRS derived from the specified one, but with different 
axis order or unit.
      *
-     * @param original  the original coordinate system from which to derive a 
new one.
+     * @param original  the original CRS from which to derive a new one.
      * @param id        new identifier for this CRS, or {@code null} if none.
      * @param cs        coordinate system with new axis order or units of 
measurement.
      *
@@ -249,7 +213,7 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
     AbstractCRS(final AbstractCRS original, final Identifier id, final 
AbstractCS cs) {
         super(ReferencingUtilities.getPropertiesWithoutIdentifiers(original, 
(id == null) ? null : Map.of(IDENTIFIERS_KEY, id)));
         coordinateSystem = cs;
-        forConvention = cs.hasSameAxes(original.coordinateSystem) ? 
original.forConvention : forConvention(original);
+        forConvention = cs.hasSameAxes(original.coordinateSystem) ? 
original.forConvention : original.forConvention();
     }
 
     /**
@@ -267,7 +231,10 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
     protected AbstractCRS(final CoordinateReferenceSystem crs) {
         super(crs);
         coordinateSystem = crs.getCoordinateSystem();
-        forConvention = forConvention(this);
+        if (coordinateSystem == null) {
+            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.MissingValueForProperty_1, 
"coordinateSystem"));
+        }
+        forConvention = forConvention();
     }
 
     /**
@@ -318,10 +285,8 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
      * Returns the datum, or {@code null} if none.
      *
      * This property does not exist in {@code CoordinateReferenceSystem} 
interface — it is defined in the
-     * {@link SingleCRS} sub-interface instead. But Apache SIS does not define 
an {@code AbstractSingleCRS} class
-     * in order to simplify our class hierarchy, so we provide a datum getter 
in this class has a hidden property.
-     * Subclasses implementing {@code SingleCRS} (basically all SIS subclasses 
except {@link DefaultCompoundCRS})
-     * will override this method with public access and more specific return 
type.
+     * {@link SingleCRS} sub-interface instead. This method is defined here 
for the convenience of the
+     * {@link #formatTo(Formatter)} method implementation.
      *
      * @return the datum, or {@code null} if none.
      */
@@ -380,8 +345,8 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
     }
 
     /**
-     * Returns a coordinate reference system equivalent to this one but with 
axes rearranged according the given
-     * convention. If this CRS is already compatible with the given 
convention, then this method returns {@code this}.
+     * Returns a <abbr>CRS</abbr> equivalent to this one but with axes 
rearranged according the given convention.
+     * If this <abbr>CRS</abbr> is already compatible with the given 
convention, then this method returns {@code this}.
      *
      * @param  convention  the axes convention for which a coordinate 
reference system is desired.
      * @return a coordinate reference system compatible with the given 
convention (may be {@code this}).
@@ -434,16 +399,14 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
     @Override
     public boolean equals(final Object object, final ComparisonMode mode) {
         if (super.equals(object, mode)) {
-            final Datum datum = getDatum();
             switch (mode) {
                 case STRICT: {
-                    final AbstractCRS that = (AbstractCRS) object;
-                    return Objects.equals(datum, that.getDatum()) &&
-                           Objects.equals(coordinateSystem, 
that.coordinateSystem);
+                    final var that = (AbstractCRS) object;
+                    return Objects.equals(coordinateSystem, 
that.coordinateSystem);
                 }
                 default: {
-                    return Utilities.deepEquals(datum, (object instanceof 
SingleCRS) ? ((SingleCRS) object).getDatum() : null, mode) &&
-                           Utilities.deepEquals(getCoordinateSystem(), 
((CoordinateReferenceSystem) object).getCoordinateSystem(), mode);
+                    final var that = (CoordinateReferenceSystem) object;
+                    return Utilities.deepEquals(getCoordinateSystem(), 
that.getCoordinateSystem(), mode);
                 }
             }
         }
@@ -459,7 +422,7 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
      */
     @Override
     protected long computeHashCode() {
-        return super.computeHashCode() + Objects.hash(getDatum(), 
coordinateSystem);
+        return super.computeHashCode() + coordinateSystem.hashCode();
     }
 
     /**
@@ -590,7 +553,7 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
      */
     AbstractCRS() {
         super(org.apache.sis.referencing.privy.NilReferencingObject.INSTANCE);
-        forConvention = forConvention(this);
+        forConvention = forConvention();
         /*
          * The coordinate system is mandatory for SIS working. We do not 
verify its presence here
          * because the verification would have to be done in an 
`afterMarshal(…)` method and throwing
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractSingleCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractSingleCRS.java
new file mode 100644
index 0000000000..6cff7e3c74
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractSingleCRS.java
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.crs;
+
+import java.util.Map;
+import java.util.Objects;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlSeeAlso;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.opengis.metadata.Identifier;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.datum.Datum;
+import org.apache.sis.util.Utilities;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.cs.AbstractCS;
+import org.apache.sis.referencing.datum.PseudoDatum;
+import org.apache.sis.referencing.internal.Resources;
+import org.apache.sis.metadata.privy.ImplementationHelper;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.referencing.datum.DatumEnsemble;
+
+
+/**
+ * Base class of <abbr>CRS</abbr> associated to a datum.
+ *
+ * @param  <D>  the type of datum associated to this <abbr>CRS</abbr>.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ */
+@XmlType(name = "AbstractSingleCRSType")
+@XmlRootElement(name = "AbstractSingleCRS")
+@XmlSeeAlso({
+    AbstractDerivedCRS.class,
+    DefaultGeodeticCRS.class,
+    DefaultVerticalCRS.class,
+    DefaultTemporalCRS.class,
+    DefaultParametricCRS.class,
+    DefaultEngineeringCRS.class,
+    DefaultImageCRS.class
+})
+class AbstractSingleCRS<D extends Datum> extends AbstractCRS implements 
SingleCRS {
+    /**
+     * Serial number for inter-operability with different versions.
+     */
+    private static final long serialVersionUID = 2876221982955686798L;
+
+    /**
+     * The datum, or {@code null} if the <abbr>CRS</abbr> is associated only 
to a datum ensemble.
+     *
+     * <p><b>Consider this field as final!</b>
+     * This field is non-final only for construction convenience and for 
unmarshalling.</p>
+     *
+     * @see #getDatum()
+     */
+    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
+    private D datum;
+
+    /**
+     * Collection of reference frames which for low accuracy requirements may 
be considered to be
+     * insignificantly different from each other. May be {@code null} if there 
is no such ensemble.
+     *
+     * @see #getDatumEnsemble()
+     */
+    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
+    private final DatumEnsemble<D> ensemble;
+
+    /**
+     * Creates a coordinate reference system from the given properties, datum 
and coordinate system.
+     * At least one of the {@code datum} and {@code ensemble} arguments shall 
be non-null.
+     * The properties given in argument follow the same rules as for the
+     * {@linkplain AbstractReferenceSystem#AbstractReferenceSystem(Map) 
super-class constructor}.
+     *
+     * @param  properties  the properties to be given to the coordinate 
reference system.
+     * @param  datumType   GeoAPI interface of the datum or members of the 
datum ensemble.
+     * @param  datum       the datum, or {@code null} if the CRS is associated 
only to a datum ensemble.
+     * @param  ensemble    collection of reference frames which for low 
accuracy requirements may be considered to be
+     *                     insignificantly different from each other, or 
{@code null} if there is no such ensemble.
+     * @param  cs          the coordinate system.
+     */
+    AbstractSingleCRS(final Map<String,?> properties,
+                      final Class<D> datumType,
+                      final D datum,
+                      final DatumEnsemble<D> ensemble,
+                      final CoordinateSystem cs)
+    {
+        super(properties, cs);
+        /*
+         * If the given datum is actually a wrapper for a datum ensemble, 
unwrap the datum ensemble
+         * and verify the consistency. This class should never store 
`PseudoDatum` instances.
+         */
+        if (datum instanceof PseudoDatum<?>) {
+            @SuppressWarnings("unchecked")      // Type is verified below.
+            final var pseudo = (PseudoDatum<D>) datum;
+            final var member = pseudo.getInterface();
+            if (member != datumType) {
+                throw new 
IllegalArgumentException(Errors.forProperties(properties)
+                            .getString(Errors.Keys.IllegalArgumentClass_2, 
"datum",
+                                       PseudoDatum.class.getSimpleName() + '<' 
+ member.getSimpleName() + '>'));
+            }
+            if (ensemble == null) {
+                this.ensemble = pseudo.ensemble;
+            } else if (Utilities.equalsIgnoreMetadata(ensemble, 
pseudo.ensemble)) {
+                this.ensemble = ensemble;
+            } else {
+                throw new 
IllegalArgumentException(Errors.forProperties(properties)
+                            
.getString(Errors.Keys.IncompatiblePropertyValue_1, "pseudo-datum"));
+            }
+            ArgumentChecks.ensureNonEmpty((ensemble != null) ? "ensemble" : 
"pseudo-datum", this.ensemble.getMembers());
+        } else {
+            this.datum    = datum;
+            this.ensemble = ensemble;
+            checkDatum(properties);
+        }
+    }
+
+    /**
+     * Verifies the consistency between the datum and the ensemble.
+     * At least one of the {@link #datum} and {@link #ensemble} arguments 
shall be non-null.
+     *
+     * @param  properties  user-specified properties given at construction 
time, or {@code null} if none.
+     * @throws NullPointerException if both {@link #datum} and {@link 
#ensemble} are null.
+     * @throws IllegalArgumentException if the datum is not a member of the 
ensemble.
+     */
+    private void checkDatum(final Map<String,?> properties) {
+        if (ensemble == null) {
+            ArgumentChecks.ensureNonNull("datum", datum);
+        } else if (datum != null) {
+            for (final D member : ensemble.getMembers()) {
+                if (Utilities.equalsIgnoreMetadata(datum, member)) {
+                    return;
+                }
+            }
+            throw new 
IllegalArgumentException(Resources.forProperties(properties)
+                        .getString(Resources.Keys.NotAMemberOfDatumEnsemble_2,
+                                   IdentifiedObjects.getDisplayName(ensemble),
+                                   IdentifiedObjects.getDisplayName(datum)));
+        } else {
+            ArgumentChecks.ensureNonEmpty("ensemble", ensemble.getMembers());
+        }
+    }
+
+    /**
+     * Creates a new CRS derived from the specified one, but with different 
axis order or unit.
+     *
+     * @param original  the original CRS from which to derive a new one.
+     * @param id        new identifier for this CRS, or {@code null} if none.
+     * @param cs        coordinate system with new axis order or units of 
measurement.
+     */
+    AbstractSingleCRS(final AbstractSingleCRS<D> original, final Identifier 
id, final AbstractCS cs) {
+        super(original, id, cs);
+        datum    = original.datum;
+        ensemble = original.ensemble;
+    }
+
+    /**
+     * Constructs a new coordinate reference system with the same values as 
the specified one.
+     * This copy constructor provides a way to convert an arbitrary 
implementation into a SIS one
+     * or a user-defined one (as a subclass), usually in order to leverage 
some implementation-specific API.
+     *
+     * <p>This constructor performs a shallow copy, i.e. the properties are 
not cloned.</p>
+     *
+     * <h4>Type safety</h4>
+     * This constructor shall be invoked only by subclass constructors with a 
method signature where
+     * the <abbr>CRS</abbr> type is an interface with {@code getDatum()} and 
{@code getDatumEnsemble()}
+     * methods overridden with return type {@code <D>}.
+     *
+     * @param  crs  the coordinate reference system to copy.
+     */
+    @SuppressWarnings("unchecked")              // See "Type safety" in above 
Javadoc.
+    AbstractSingleCRS(final SingleCRS crs) {
+        super(crs);
+        datum = (D) crs.getDatum();
+        if (datum instanceof PseudoDatum<?>) {
+            throw new IllegalArgumentException(
+                    Errors.format(Errors.Keys.IllegalPropertyValueClass_2, 
"datum", PseudoDatum.class));
+        }
+        ensemble = (DatumEnsemble<D>) crs.getDatumEnsemble();
+        checkDatum(null);
+    }
+
+    /**
+     * Returns the GeoAPI interface implemented by this class.
+     * The default implementation returns {@code SingleCRS.class}.
+     * Subclasses implementing a more specific GeoAPI interface shall override 
this method.
+     *
+     * @return the coordinate reference system interface implemented by this 
class.
+     */
+    @Override
+    public Class<? extends SingleCRS> getInterface() {
+        return SingleCRS.class;
+    }
+
+    /**
+     * Returns the datum, or {@code null} if this <abbr>CRS</abbr> is 
associated only to a datum ensemble.
+     *
+     * @return the datum, or {@code null} if none.
+     */
+    @Override
+    public D getDatum() {
+        return datum;
+    }
+
+    /**
+     * Returns the datum ensemble, or {@code null} if none.
+     *
+     * @return the datum ensemble, or {@code null} if none.
+     */
+    @Override
+    public DatumEnsemble<D> getDatumEnsemble() {
+        return ensemble;
+    }
+
+    /**
+     * Compares this coordinate reference system with the specified object for 
equality.
+     *
+     * @param  object  the object to compare to {@code this}.
+     * @param  mode    whether to perform a strict or lenient comparison.
+     * @return {@code true} if both objects are equal.
+     * @hidden
+     */
+    @Override
+    public boolean equals(final Object object, final ComparisonMode mode) {
+        if (super.equals(object, mode)) {
+            switch (mode) {
+                case STRICT: {
+                    final var that = (AbstractSingleCRS<?>) object;
+                    return Objects.equals(datum, that.datum) && 
Objects.equals(ensemble, that.ensemble);
+                }
+                default: {
+                    final var that = (SingleCRS) object;
+                    return Utilities.deepEquals(getDatum(), that.getDatum(), 
mode) &&
+                           Utilities.deepEquals(getDatumEnsemble(), 
that.getDatumEnsemble(), mode);
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Invoked by {@code hashCode()} for computing the hash code when first 
needed.
+     *
+     * @return the hash code value. This value may change in any future Apache 
SIS version.
+     * @hidden
+     */
+    @Override
+    protected long computeHashCode() {
+        return super.computeHashCode() + Objects.hash(datum, ensemble);
+    }
+
+
+
+
+    /*
+     
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+     ┃                                                                         
         ┃
+     ┃                               XML support with JAXB                     
         ┃
+     ┃                                                                         
         ┃
+     ┃        The following methods are invoked by JAXB using reflection (even 
if       ┃
+     ┃        they are private) or are helpers for other methods invoked by 
JAXB.       ┃
+     ┃        Those methods can be safely removed if Geographic Markup 
Language         ┃
+     ┃        (GML) support is not needed.                                     
         ┃
+     ┃                                                                         
         ┃
+     
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+     */
+
+    /**
+     * Constructs a new object in which every attributes are set to a null 
value.
+     * <strong>This is not a valid object.</strong> This constructor is 
strictly
+     * reserved to JAXB, which will assign values to the fields using 
reflection.
+     */
+    AbstractSingleCRS() {
+        ensemble = null;
+        /*
+         * The coordinate system is mandatory for SIS working. We do not 
verify its presence here
+         * because the verification would have to be done in an 
`afterMarshal(…)` method and throwing
+         * an exception in that method causes the whole unmarshalling to fail. 
But the SC_CRS adapter
+         * does some verifications.
+         */
+    }
+
+    /**
+     * Sets the datum to the given value.
+     * This method is indirectly invoked by JAXB at unmarshalling time.
+     *
+     * @param  name  the property name, used only in case of error message to 
format. Can be null for auto-detect.
+     * @throws IllegalStateException if the datum has already been set.
+     */
+    final void setDatum(final String name, final D value) {
+        if (datum == null) {
+            datum = value;
+        } else {
+            ImplementationHelper.propertyAlreadySet(AbstractSingleCRS.class, 
"setDatum", name);
+        }
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultDerivedCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultDerivedCRS.java
index edf9df9a2f..75c85fc31c 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultDerivedCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultDerivedCRS.java
@@ -397,15 +397,33 @@ public class DefaultDerivedCRS extends AbstractDerivedCRS 
implements DerivedCRS
     }
 
     /**
-     * Returns the datum of the {@linkplain #getBaseCRS() base CRS}.
+     * Returns the datum of the base <abbr>CRS</abbr>.
+     * This property may be null if this <abbr>CRS</abbr> is related to an 
object
+     * identified only by a {@linkplain #getDatumEnsemble() datum ensemble}.
      *
-     * @return the datum of the base CRS.
+     * @return the datum of the {@linkplain #getBaseCRS() base CRS}, or {@code 
null} if this <abbr>CRS</abbr>
+     *         is related to an object identified only by a {@linkplain 
#getDatumEnsemble() datum ensemble}.
      */
     @Override
     public Datum getDatum() {
         return getBaseCRS().getDatum();
     }
 
+    /**
+     * Returns the datum ensemble of the base <abbr>CRS</abbr>.
+     * This property may be null if this <abbr>CRS</abbr> is related to an 
object
+     * identified only by a {@linkplain #getDatum() reference frame}.
+     *
+     * @return the datum ensemble of the {@linkplain #getBaseCRS() base CRS}, 
or {@code null} if this
+     *         <abbr>CRS</abbr> is related to an object identified only by a 
{@linkplain #getDatum() datum}.
+     *
+     * @since 1.5
+     */
+    @Override
+    public DatumEnsemble<?> getDatumEnsemble() {
+        return getBaseCRS().getDatumEnsemble();
+    }
+
     /**
      * Returns the CRS on which the conversion is applied.
      * This CRS defines the {@linkplain #getDatum() datum} of this CRS and (at 
least implicitly)
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultEngineeringCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultEngineeringCRS.java
index aaae14d88e..e08f83a07a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultEngineeringCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultEngineeringCRS.java
@@ -26,7 +26,6 @@ import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.datum.EngineeringDatum;
 import org.apache.sis.referencing.AbstractReferenceSystem;
 import org.apache.sis.referencing.cs.*;
-import org.apache.sis.metadata.privy.ImplementationHelper;
 import org.apache.sis.referencing.privy.WKTKeywords;
 import org.apache.sis.xml.bind.referencing.CS_CoordinateSystem;
 import org.apache.sis.io.wkt.Formatter;
@@ -81,31 +80,11 @@ import org.opengis.referencing.datum.DatumEnsemble;
     "datum"
 })
 @XmlRootElement(name = "EngineeringCRS")
-public class DefaultEngineeringCRS extends AbstractCRS implements 
EngineeringCRS {
+public class DefaultEngineeringCRS extends AbstractSingleCRS<EngineeringDatum> 
implements EngineeringCRS {
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = 6695541732063382701L;
-
-    /**
-     * The datum, or {@code null} if the CRS is associated only to a datum 
ensemble.
-     *
-     * <p><b>Consider this field as final!</b>
-     * This field is modified only at unmarshalling time by {@link 
#setDatum(EngineeringDatum)}</p>
-     *
-     * @see #getDatum()
-     */
-    @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
-    private EngineeringDatum datum;
-
-    /**
-     * Collection of reference frames which for low accuracy requirements may 
be considered to be
-     * insignificantly different from each other. May be {@code null} if there 
is no such ensemble.
-     *
-     * @see #getDatumEnsemble()
-     */
-    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
-    private final DatumEnsemble<EngineeringDatum> ensemble;
+    private static final long serialVersionUID = -5716016061569447341L;
 
     /**
      * Creates a coordinate reference system from the given properties, datum 
and coordinate system.
@@ -158,10 +137,7 @@ public class DefaultEngineeringCRS extends AbstractCRS 
implements EngineeringCRS
                                  final DatumEnsemble<EngineeringDatum> 
ensemble,
                                  final CoordinateSystem cs)
     {
-        super(properties, cs);
-        this.datum    = datum;
-        this.ensemble = ensemble;
-        checkDatum(datum, ensemble);
+        super(properties, EngineeringDatum.class, datum, ensemble, cs);
     }
 
     /**
@@ -181,8 +157,6 @@ public class DefaultEngineeringCRS extends AbstractCRS 
implements EngineeringCRS
      */
     private DefaultEngineeringCRS(final DefaultEngineeringCRS original, final 
AbstractCS cs) {
         super(original, null, cs);
-        datum    = original.datum;
-        ensemble = original.ensemble;
     }
 
     /**
@@ -198,9 +172,6 @@ public class DefaultEngineeringCRS extends AbstractCRS 
implements EngineeringCRS
      */
     protected DefaultEngineeringCRS(final EngineeringCRS crs) {
         super(crs);
-        datum    = crs.getDatum();
-        ensemble = crs.getDatumEnsemble();
-        checkDatum(datum, ensemble);
     }
 
     /**
@@ -245,7 +216,7 @@ public class DefaultEngineeringCRS extends AbstractCRS 
implements EngineeringCRS
     @Override
     @XmlElement(name = "engineeringDatum", required = true)
     public EngineeringDatum getDatum() {
-        return datum;
+        return super.getDatum();
     }
 
     /**
@@ -261,7 +232,7 @@ public class DefaultEngineeringCRS extends AbstractCRS 
implements EngineeringCRS
      */
     @Override
     public DatumEnsemble<EngineeringDatum> getDatumEnsemble() {
-        return ensemble;
+        return super.getDatumEnsemble();
     }
 
     /**
@@ -322,7 +293,6 @@ public class DefaultEngineeringCRS extends AbstractCRS 
implements EngineeringCRS
      * reserved to JAXB, which will assign values to the fields using 
reflection.
      */
     private DefaultEngineeringCRS() {
-        ensemble = null;
         /*
          * The datum and the coordinate system are mandatory for SIS working. 
We do not verify their presence
          * here because the verification would have to be done in an 
'afterMarshal(…)' method and throwing an
@@ -337,11 +307,7 @@ public class DefaultEngineeringCRS extends AbstractCRS 
implements EngineeringCRS
      * @see #getDatum()
      */
     private void setDatum(final EngineeringDatum value) {
-        if (datum == null) {
-            datum = value;
-        } else {
-            
ImplementationHelper.propertyAlreadySet(DefaultEngineeringCRS.class, 
"setDatum", "engineeringDatum");
-        }
+        setDatum("engineeringDatum", value);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeocentricCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeocentricCRS.java
index 7685aa9720..d33a7d34d8 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeocentricCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeocentricCRS.java
@@ -275,7 +275,7 @@ public class DefaultGeocentricCRS extends 
DefaultGeodeticCRS {
      */
     @Override
     public DatumEnsemble<GeodeticDatum> getDatumEnsemble() {
-        return ensemble;
+        return super.getDatumEnsemble();
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java
index 7f706ab223..c46575f586 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java
@@ -39,7 +39,6 @@ import org.apache.sis.referencing.privy.AxisDirections;
 import org.apache.sis.referencing.privy.WKTKeywords;
 import org.apache.sis.referencing.privy.WKTUtilities;
 import org.apache.sis.referencing.privy.ReferencingUtilities;
-import org.apache.sis.metadata.privy.ImplementationHelper;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.io.wkt.Convention;
 import org.apache.sis.io.wkt.Formatter;
@@ -71,31 +70,11 @@ import org.opengis.referencing.datum.DatumEnsemble;
     "datum"
 })
 @XmlRootElement(name = "GeodeticCRS")
-class DefaultGeodeticCRS extends AbstractCRS implements GeodeticCRS {   // If 
made public, see comment in getDatum().
+class DefaultGeodeticCRS extends AbstractSingleCRS<GeodeticDatum> implements 
GeodeticCRS {
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = -6205678223972395910L;
-
-    /**
-     * The datum, or {@code null} if the CRS is associated only to a datum 
ensemble.
-     *
-     * <p><b>Consider this field as final!</b>
-     * This field is modified only at unmarshalling time by {@link 
#setDatum(GeodeticDatum)}</p>
-     *
-     * @see #getDatum()
-     */
-    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
-    private GeodeticDatum datum;
-
-    /**
-     * Collection of reference frames which for low accuracy requirements may 
be considered to be
-     * insignificantly different from each other. May be {@code null} if there 
is no such ensemble.
-     *
-     * @see #getDatumEnsemble()
-     */
-    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
-    final DatumEnsemble<GeodeticDatum> ensemble;
+    private static final long serialVersionUID = -1634312292667977126L;
 
     /**
      * Creates a coordinate reference system from the given properties, datum 
and coordinate system.
@@ -115,10 +94,7 @@ class DefaultGeodeticCRS extends AbstractCRS implements 
GeodeticCRS {   // If ma
                        final DatumEnsemble<GeodeticDatum> ensemble,
                        final CoordinateSystem cs)
     {
-        super(properties, cs);
-        this.datum    = datum;
-        this.ensemble = ensemble;
-        checkDatum(datum, ensemble);
+        super(properties, GeodeticDatum.class, datum, ensemble, cs);
     }
 
     /**
@@ -127,8 +103,6 @@ class DefaultGeodeticCRS extends AbstractCRS implements 
GeodeticCRS {   // If ma
      */
     DefaultGeodeticCRS(final DefaultGeodeticCRS original, final Identifier id, 
final AbstractCS cs) {
         super(original, id, cs);
-        datum    = original.datum;
-        ensemble = original.ensemble;
     }
 
     /**
@@ -142,9 +116,6 @@ class DefaultGeodeticCRS extends AbstractCRS implements 
GeodeticCRS {   // If ma
      */
     protected DefaultGeodeticCRS(final GeodeticCRS crs) {
         super(crs);
-        datum    = crs.getDatum();
-        ensemble = crs.getDatumEnsemble();
-        checkDatum(datum, ensemble);
     }
 
     /**
@@ -184,7 +155,7 @@ class DefaultGeodeticCRS extends AbstractCRS implements 
GeodeticCRS {   // If ma
     @Override
     @XmlElement(name = "geodeticDatum", required = true)
     public GeodeticDatum getDatum() {
-        return datum;
+        return super.getDatum();
     }
 
     /**
@@ -329,7 +300,6 @@ class DefaultGeodeticCRS extends AbstractCRS implements 
GeodeticCRS {   // If ma
      * reserved to JAXB, which will assign values to the fields using 
reflection.
      */
     DefaultGeodeticCRS() {
-        ensemble = null;
         /*
          * The datum and the coordinate system are mandatory for SIS working. 
We do not verify their presence
          * here because the verification would have to be done in an 
`afterMarshal(…)` method and throwing an
@@ -344,11 +314,7 @@ class DefaultGeodeticCRS extends AbstractCRS implements 
GeodeticCRS {   // If ma
      * @see #getDatum()
      */
     private void setDatum(final GeodeticDatum value) {
-        if (datum == null) {
-            datum = value;
-        } else {
-            ImplementationHelper.propertyAlreadySet(DefaultGeodeticCRS.class, 
"setDatum", "geodeticDatum");
-        }
+        setDatum("geodeticDatum", value);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeographicCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeographicCRS.java
index 40ff61cca7..fd685ca0b9 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeographicCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeographicCRS.java
@@ -18,9 +18,8 @@ package org.apache.sis.referencing.crs;
 
 import java.util.Map;
 import java.util.Arrays;
-import java.util.Iterator;
+import java.util.NoSuchElementException;
 import jakarta.xml.bind.annotation.XmlTransient;
-import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.datum.Ellipsoid;
 import org.opengis.referencing.datum.PrimeMeridian;
 import org.opengis.referencing.datum.GeodeticDatum;
@@ -31,12 +30,11 @@ import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.referencing.GeodeticException;
-import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.ImmutableIdentifier;
 import org.apache.sis.referencing.AbstractReferenceSystem;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.AbstractCS;
-import org.apache.sis.util.resources.Errors;
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.measure.Longitude;
 import static org.apache.sis.util.privy.Constants.CRS;
@@ -252,6 +250,38 @@ public class DefaultGeographicCRS extends 
DefaultGeodeticCRS implements Geograph
         return GeographicCRS.class;
     }
 
+    /**
+     * Returns the prime meridian which is indirectly (through a datum) 
associated to this <abbr>CRS</abbr>.
+     * If the {@linkplain #getDatum() datum} is non-null, then this method 
returns the datum prime meridian.
+     * Otherwise, if all members of the {@linkplain #getDatumEnsemble() datum 
ensemble} use the same prime meridian,
+     * then this method returns that meridian.
+     *
+     * @return the prime meridian indirectly associated to this 
<abbr>CRS</abbr>.
+     * @throws NoSuchElementException if there is no datum and the ensemble 
does not contain at least one member.
+     * @throws GeodeticException if the prime meridian is not the same for all 
members of the datum ensemble.
+     *
+     * @since 1.5
+     */
+    public PrimeMeridian getPrimeMeridian() {
+        return PseudoDatum.of(this).getPrimeMeridian();
+    }
+
+    /**
+     * Returns the ellipsoid which is indirectly (through a datum) associated 
to this <abbr>CRS</abbr>.
+     * If the {@linkplain #getDatum() datum} is non-null, then this method 
returns the datum ellipsoid.
+     * Otherwise, if all members of the {@linkplain #getDatumEnsemble() datum 
ensemble} use the same ellipsoid,
+     * then this method returns that ellipsoid.
+     *
+     * @return the ellipsoid indirectly associated to this <abbr>CRS</abbr>.
+     * @throws NoSuchElementException if there is no datum and the ensemble 
does not contain at least one member.
+     * @throws GeodeticException if the ellipsoid is not the same for all 
members of the datum ensemble.
+     *
+     * @since 1.5
+     */
+    public Ellipsoid getEllipsoid() {
+        return PseudoDatum.of(this).getEllipsoid();
+    }
+
     /**
      * Returns the geodetic reference frame associated to this geographic CRS.
      * This property may be null if this <abbr>CRS</abbr> is related to an 
object
@@ -278,7 +308,7 @@ public class DefaultGeographicCRS extends 
DefaultGeodeticCRS implements Geograph
      */
     @Override
     public DatumEnsemble<GeodeticDatum> getDatumEnsemble() {
-        return ensemble;
+        return super.getDatumEnsemble();
     }
 
     /**
@@ -291,68 +321,6 @@ public class DefaultGeographicCRS extends 
DefaultGeodeticCRS implements Geograph
         return (EllipsoidalCS) super.getCoordinateSystem();
     }
 
-    /**
-     * Returns the ellipsoid which is indirectly (through a datum) associated 
to this <abbr>CRS</abbr>.
-     * If the {@linkplain #getDatum() datum} is non-null, then this method 
returns the datum ellipsoid.
-     * Otherwise, if all members of the {@linkplain #getDatumEnsemble() datum 
ensemble} use the same ellipsoid,
-     * then this method returns that ellipsoid.
-     *
-     * @return the ellipsoid indirectly associated to this <abbr>CRS</abbr>.
-     * @throws NullPointerException if an ellipsoid, which are mandatory in 
the context of geographic <abbr>CRS</abbr>, is null.
-     * @throws GeodeticException if the ellipsoid is not the same for all 
members of the datum ensemble.
-     *
-     * @since 1.5
-     */
-    public Ellipsoid getEllipsoid() {
-        final GeodeticDatum datum = super.getDatum();
-        if (datum != null) {
-            return datum.getEllipsoid();        // Has precedence regardless 
the value.
-        }
-        // If the datum is null, then the datum ensemble must be non-null.
-        final Iterator<GeodeticDatum> it = ensemble.getMembers().iterator();
-        final Ellipsoid ellipsoid = it.next().getEllipsoid();  // Mandatory
-        while (it.hasNext()) {
-            checkDatumConsistency(ellipsoid, it.next().getEllipsoid());
-        }
-        return ellipsoid;
-    }
-
-    /**
-     * Returns the prime meridian which is indirectly (through a datum) 
associated to this <abbr>CRS</abbr>.
-     * If the {@linkplain #getDatum() datum} is non-null, then this method 
returns the datum prime meridian.
-     * Otherwise, if all members of the {@linkplain #getDatumEnsemble() datum 
ensemble} use the same prime meridian,
-     * then this method returns that meridian.
-     *
-     * @return the prime meridian indirectly associated to this 
<abbr>CRS</abbr>.
-     * @throws NullPointerException if a prime meridian, which are mandatory, 
is null.
-     * @throws GeodeticException if the prime meridian is not the same for all 
members of the datum ensemble.
-     *
-     * @since 1.5
-     */
-    public PrimeMeridian getPrimeMeridian() {
-        final GeodeticDatum datum = super.getDatum();
-        if (datum != null) {
-            return datum.getPrimeMeridian();    // Has precedence regardless 
the value.
-        }
-        // If the datum is null, then the datum ensemble must be non-null.
-        final Iterator<GeodeticDatum> it = ensemble.getMembers().iterator();
-        final PrimeMeridian pm = it.next().getPrimeMeridian();  // Mandatory
-        while (it.hasNext()) {
-            checkDatumConsistency(pm, it.next().getPrimeMeridian());
-        }
-        return pm;
-    }
-
-    /**
-     * Ensures that the ellipsoid or prime meridian has the same value in all 
members of a datum ensemble.
-     */
-    private static void checkDatumConsistency(final IdentifiedObject expected, 
final IdentifiedObject actual) {
-        if (!expected.equals(actual)) {
-            throw new 
GeodeticException(Errors.format(Errors.Keys.NonUniformValue_2,
-                    IdentifiedObjects.getDisplayName(expected), 
IdentifiedObjects.getDisplayName(actual)));
-        }
-    }
-
     /**
      * {@inheritDoc}
      *
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultImageCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultImageCRS.java
index e9e58b45d8..885a1679bd 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultImageCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultImageCRS.java
@@ -17,7 +17,6 @@
 package org.apache.sis.referencing.crs;
 
 import java.util.Map;
-import java.util.Objects;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
@@ -27,7 +26,6 @@ import org.apache.sis.referencing.AbstractReferenceSystem;
 import org.apache.sis.referencing.privy.WKTKeywords;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.AbstractCS;
-import org.apache.sis.metadata.privy.ImplementationHelper;
 import org.apache.sis.io.wkt.Formatter;
 
 // Specific to the geoapi-4.0 branch:
@@ -69,21 +67,11 @@ import org.apache.sis.referencing.datum.DefaultImageDatum;
     "datum"
 })
 @XmlRootElement(name = "ImageCRS")
-public final class DefaultImageCRS extends AbstractCRS {
+public final class DefaultImageCRS extends 
AbstractSingleCRS<DefaultImageDatum> {
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = 7312452786096397847L;
-
-    /**
-     * The datum.
-     *
-     * <p><b>Consider this field as final!</b>
-     * This field is modified only at unmarshalling time.</p>
-     *
-     * @see #getDatum()
-     */
-    private DefaultImageDatum datum;
+    private static final long serialVersionUID = 7222610270977351462L;
 
     /**
      * Creates a coordinate reference system from the given properties, datum 
and coordinate system.
@@ -128,8 +116,7 @@ public final class DefaultImageCRS extends AbstractCRS {
                            final DefaultImageDatum datum,
                            final AffineCS cs)
     {
-        super(properties, cs);
-        this.datum = Objects.requireNonNull(datum);
+        super(properties, DefaultImageDatum.class, datum, null, cs);
     }
 
     /**
@@ -138,7 +125,6 @@ public final class DefaultImageCRS extends AbstractCRS {
      */
     private DefaultImageCRS(final DefaultImageCRS original, final AbstractCS 
cs) {
         super(original, null, cs);
-        datum = original.datum;
     }
 
     /**
@@ -149,7 +135,7 @@ public final class DefaultImageCRS extends AbstractCRS {
     @Override
     @XmlElement(name = "imageDatum", required = true)
     public DefaultImageDatum getDatum() {
-        return datum;
+        return super.getDatum();
     }
 
     /**
@@ -239,11 +225,7 @@ public final class DefaultImageCRS extends AbstractCRS {
      * @see #getDatum()
      */
     private void setDatum(final DefaultImageDatum value) {
-        if (datum == null) {
-            datum = value;
-        } else {
-            ImplementationHelper.propertyAlreadySet(DefaultImageCRS.class, 
"setDatum", "imageDatum");
-        }
+        setDatum("imageDatum", value);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultParametricCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultParametricCRS.java
index 0f37ee6d7b..adcee10299 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultParametricCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultParametricCRS.java
@@ -20,7 +20,6 @@ import java.util.Map;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 import jakarta.xml.bind.annotation.XmlType;
-import org.apache.sis.metadata.privy.ImplementationHelper;
 import org.apache.sis.referencing.privy.WKTKeywords;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.AbstractCS;
@@ -63,31 +62,11 @@ import org.opengis.referencing.datum.DatumEnsemble;
     "datum"
 })
 @XmlRootElement(name = "ParametricCRS")
-public class DefaultParametricCRS extends AbstractCRS implements ParametricCRS 
{
+public class DefaultParametricCRS extends AbstractSingleCRS<ParametricDatum> 
implements ParametricCRS {
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = 4013698133331342649L;
-
-    /**
-     * The datum, or {@code null} if the CRS is associated only to a datum 
ensemble.
-     *
-     * <p><b>Consider this field as final!</b>
-     * This field is modified only at unmarshalling time by {@link 
#setDatum(ParametricDatum)}</p>
-     *
-     * @see #getDatum()
-     */
-    @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
-    private ParametricDatum datum;
-
-    /**
-     * Collection of reference frames which for low accuracy requirements may 
be considered to be
-     * insignificantly different from each other. May be {@code null} if there 
is no such ensemble.
-     *
-     * @see #getDatumEnsemble()
-     */
-    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
-    private final DatumEnsemble<ParametricDatum> ensemble;
+    private static final long serialVersionUID = -5443671973122639841L;
 
     /**
      * Creates a coordinate reference system from the given properties, datum 
and coordinate system.
@@ -140,10 +119,7 @@ public class DefaultParametricCRS extends AbstractCRS 
implements ParametricCRS {
                                 final DatumEnsemble<ParametricDatum> ensemble,
                                 final ParametricCS cs)
     {
-        super(properties, cs);
-        this.datum    = datum;
-        this.ensemble = ensemble;
-        checkDatum(datum, ensemble);
+        super(properties, ParametricDatum.class, datum, ensemble, cs);
         checkDimension(1, 1, cs);
     }
 
@@ -164,8 +140,6 @@ public class DefaultParametricCRS extends AbstractCRS 
implements ParametricCRS {
      */
     private DefaultParametricCRS(final DefaultParametricCRS original, final 
AbstractCS cs) {
         super(original, null, cs);
-        datum    = original.datum;
-        ensemble = original.ensemble;
     }
 
     /**
@@ -181,9 +155,6 @@ public class DefaultParametricCRS extends AbstractCRS 
implements ParametricCRS {
      */
     protected DefaultParametricCRS(final ParametricCRS crs) {
         super(crs);
-        datum    = crs.getDatum();
-        ensemble = crs.getDatumEnsemble();
-        checkDatum(datum, ensemble);
     }
 
     /**
@@ -228,7 +199,7 @@ public class DefaultParametricCRS extends AbstractCRS 
implements ParametricCRS {
     @Override
     @XmlElement(name = "parametricDatum", required = true)
     public ParametricDatum getDatum() {
-        return datum;
+        return super.getDatum();
     }
 
     /**
@@ -244,7 +215,7 @@ public class DefaultParametricCRS extends AbstractCRS 
implements ParametricCRS {
      */
     @Override
     public DatumEnsemble<ParametricDatum> getDatumEnsemble() {
-        return ensemble;
+        return super.getDatumEnsemble();
     }
 
     /**
@@ -321,7 +292,6 @@ public class DefaultParametricCRS extends AbstractCRS 
implements ParametricCRS {
      * reserved to JAXB, which will assign values to the fields using 
reflection.
      */
     private DefaultParametricCRS() {
-        ensemble = null;
         /*
          * The datum and the coordinate system are mandatory for SIS working. 
We do not verify their presence
          * here because the verification would have to be done in an 
'afterMarshal(…)' method and throwing an
@@ -336,11 +306,7 @@ public class DefaultParametricCRS extends AbstractCRS 
implements ParametricCRS {
      * @see #getDatum()
      */
     private void setDatum(final ParametricDatum value) {
-        if (datum == null) {
-            datum = value;
-        } else {
-            
ImplementationHelper.propertyAlreadySet(DefaultParametricCRS.class, "setDatum", 
"parametricDatum");
-        }
+        setDatum("parametricDatum", value);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultProjectedCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultProjectedCRS.java
index abac077c16..d5af964f6f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultProjectedCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultProjectedCRS.java
@@ -42,6 +42,7 @@ import org.apache.sis.util.Workaround;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.coordinate.MismatchedDimensionException;
+import org.opengis.referencing.datum.DatumEnsemble;
 
 
 /**
@@ -209,15 +210,33 @@ public class DefaultProjectedCRS extends 
AbstractDerivedCRS implements Projected
     }
 
     /**
-     * Returns the datum of the {@linkplain #getBaseCRS() base CRS}.
+     * Returns the datum of the base <abbr>CRS</abbr>.
+     * This property may be null if this <abbr>CRS</abbr> is related to an 
object
+     * identified only by a {@linkplain #getDatumEnsemble() datum ensemble}.
      *
-     * @return the datum of the base CRS.
+     * @return the datum of the {@linkplain #getBaseCRS() base CRS}, or {@code 
null} if this <abbr>CRS</abbr>
+     *         is related to an object identified only by a {@linkplain 
#getDatumEnsemble() datum ensemble}.
      */
     @Override
     public GeodeticDatum getDatum() {
         return getBaseCRS().getDatum();
     }
 
+    /**
+     * Returns the datum ensemble of the base <abbr>CRS</abbr>.
+     * This property may be null if this <abbr>CRS</abbr> is related to an 
object
+     * identified only by a {@linkplain #getDatum() reference frame}.
+     *
+     * @return the datum ensemble of the {@linkplain #getBaseCRS() base CRS}, 
or {@code null} if this
+     *         <abbr>CRS</abbr> is related to an object identified only by a 
{@linkplain #getDatum() datum}.
+     *
+     * @since 1.5
+     */
+    @Override
+    public DatumEnsemble<GeodeticDatum> getDatumEnsemble() {
+        return getBaseCRS().getDatumEnsemble();
+    }
+
     /**
      * Returns the geographic CRS on which the map projection is applied.
      * This CRS defines the {@linkplain #getDatum() datum} of this CRS and (at 
least implicitly)
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
index b3fbfe6256..2d157f8e2b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.referencing.crs;
 
-import java.util.Iterator;
 import java.util.Map;
 import java.util.Date;
 import java.time.Instant;
@@ -38,12 +37,11 @@ import org.apache.sis.referencing.GeodeticException;
 import org.apache.sis.referencing.AbstractReferenceSystem;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.AbstractCS;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.privy.WKTKeywords;
-import org.apache.sis.metadata.privy.ImplementationHelper;
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.measure.Units;
 import org.apache.sis.math.Fraction;
-import org.apache.sis.util.resources.Errors;
 import static org.apache.sis.util.privy.Constants.NANOS_PER_SECOND;
 import static org.apache.sis.util.privy.Constants.MILLIS_PER_SECOND;
 
@@ -85,31 +83,11 @@ import org.opengis.referencing.datum.DatumEnsemble;
     "datum"
 })
 @XmlRootElement(name = "TemporalCRS")
-public class DefaultTemporalCRS extends AbstractCRS implements TemporalCRS {
+public class DefaultTemporalCRS extends AbstractSingleCRS<TemporalDatum> 
implements TemporalCRS {
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = 3000119849197222007L;
-
-    /**
-     * The datum, or {@code null} if the CRS is associated only to a datum 
ensemble.
-     *
-     * <p><b>Consider this field as final!</b>
-     * This field is modified only at unmarshalling time by {@link 
#setDatum(TemporalDatum)}</p>
-     *
-     * @see #getDatum()
-     */
-    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
-    private TemporalDatum datum;
-
-    /**
-     * Collection of reference frames which for low accuracy requirements may 
be considered to be
-     * insignificantly different from each other. May be {@code null} if there 
is no such ensemble.
-     *
-     * @see #getDatumEnsemble()
-     */
-    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
-    private final DatumEnsemble<TemporalDatum> ensemble;
+    private static final long serialVersionUID = 369537220141768472L;
 
     /**
      * Conversion factor from values in this CRS to values in seconds. We use 
{@link UnitConverter}
@@ -179,10 +157,7 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
                               final DatumEnsemble<TemporalDatum> ensemble,
                               final TimeCS cs)
     {
-        super(properties, cs);
-        this.datum    = datum;
-        this.ensemble = ensemble;
-        checkDatum(datum, ensemble);
+        super(properties, TemporalDatum.class, datum, ensemble, cs);
         checkDimension(1, 1, cs);
         initializeConverter();
     }
@@ -204,8 +179,6 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
      */
     private DefaultTemporalCRS(final DefaultTemporalCRS original, final 
AbstractCS cs) {
         super(original, null, cs);
-        datum    = original.datum;
-        ensemble = original.ensemble;
         initializeConverter();
     }
 
@@ -223,9 +196,6 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
     @SuppressWarnings("this-escape")
     protected DefaultTemporalCRS(final TemporalCRS crs) {
         super(crs);
-        datum    = crs.getDatum();
-        ensemble = crs.getDatumEnsemble();
-        checkDatum(datum, ensemble);
         initializeConverter();
     }
 
@@ -257,7 +227,7 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
     }
 
     /**
-     * Initialize the fields required for {@link #toInstant(double)} and 
{@link #toValue(Instant)} operations.
+     * Initializes the fields required for {@link #toInstant(double)} and 
{@link #toValue(Instant)} operations.
      */
     private void initializeConverter() {
         toSeconds = getUnit().getConverterTo(Units.SECOND);
@@ -302,7 +272,7 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
     @Override
     @XmlElement(name = "temporalDatum", required = true)
     public TemporalDatum getDatum() {
-        return datum;
+        return super.getDatum();
     }
 
     /**
@@ -318,7 +288,7 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
      */
     @Override
     public DatumEnsemble<TemporalDatum> getDatumEnsemble() {
-        return ensemble;
+        return super.getDatumEnsemble();
     }
 
     /**
@@ -366,19 +336,7 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
      * @since 1.5
      */
     public final Temporal getOrigin() {     // Must be final because invoked 
at construction time.
-        if (datum != null) {
-            return datum.getOrigin();       // Has precedence regardless the 
value.
-        }
-        // If the datum is null, then the datum ensemble must be non-null.
-        final Iterator<TemporalDatum> it = ensemble.getMembers().iterator();
-        final Temporal origin = it.next().getOrigin();
-        while (it.hasNext()) {
-            final Temporal actual = it.next().getOrigin();
-            if (!origin.equals(actual)) {
-                throw new 
GeodeticException(Errors.format(Errors.Keys.NonUniformValue_2, origin, actual));
-            }
-        }
-        return origin;
+        return PseudoDatum.of(this).getOrigin();
     }
 
     /**
@@ -575,7 +533,6 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
      * reserved to JAXB, which will assign values to the fields using 
reflection.
      */
     private DefaultTemporalCRS() {
-        ensemble = null;
         /*
          * The datum and the coordinate system are mandatory for SIS working. 
We do not verify their presence
          * here because the verification would have to be done in an 
'afterMarshal(…)' method and throwing an
@@ -590,13 +547,9 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
      * @see #getDatum()
      */
     private void setDatum(final TemporalDatum value) {
-        if (datum == null) {
-            datum = value;
-            if (super.getCoordinateSystem() != null) {
-                initializeConverter();
-            }
-        } else {
-            ImplementationHelper.propertyAlreadySet(DefaultVerticalCRS.class, 
"setDatum", "temporalDatum");
+        setDatum("temporalDatum", value);
+        if (super.getCoordinateSystem() != null) {
+            initializeConverter();
         }
     }
 
@@ -607,7 +560,7 @@ public class DefaultTemporalCRS extends AbstractCRS 
implements TemporalCRS {
      */
     private void setCoordinateSystem(final TimeCS cs) {
         setCoordinateSystem("timeCS", cs);
-        if (toSeconds == null && datum != null) {
+        if (toSeconds == null && super.getDatum() != null) {
             initializeConverter();
         }
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultVerticalCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultVerticalCRS.java
index cb11c46b70..41ef613856 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultVerticalCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultVerticalCRS.java
@@ -27,7 +27,6 @@ import org.apache.sis.referencing.AbstractReferenceSystem;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.AbstractCS;
 import org.apache.sis.referencing.privy.WKTKeywords;
-import org.apache.sis.metadata.privy.ImplementationHelper;
 import org.apache.sis.io.wkt.Formatter;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -64,31 +63,11 @@ import org.opengis.referencing.datum.DatumEnsemble;
     "datum"
 })
 @XmlRootElement(name = "VerticalCRS")
-public class DefaultVerticalCRS extends AbstractCRS implements VerticalCRS {
+public class DefaultVerticalCRS extends AbstractSingleCRS<VerticalDatum> 
implements VerticalCRS {
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = 3565878468719941800L;
-
-    /**
-     * The datum, or {@code null} if the CRS is associated only to a datum 
ensemble.
-     *
-     * <p><b>Consider this field as final!</b>
-     * This field is modified only at unmarshalling time by {@link 
#setDatum(VerticalDatum)}</p>
-     *
-     * @see #getDatum()
-     */
-    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
-    private VerticalDatum datum;
-
-    /**
-     * Collection of reference frames which for low accuracy requirements may 
be considered to be
-     * insignificantly different from each other. May be {@code null} if there 
is no such ensemble.
-     *
-     * @see #getDatumEnsemble()
-     */
-    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
-    private final DatumEnsemble<VerticalDatum> ensemble;
+    private static final long serialVersionUID = 5807645386129942811L;
 
     /**
      * Creates a coordinate reference system from the given properties, datum 
and coordinate system.
@@ -141,10 +120,7 @@ public class DefaultVerticalCRS extends AbstractCRS 
implements VerticalCRS {
                               final DatumEnsemble<VerticalDatum> ensemble,
                               final VerticalCS cs)
     {
-        super(properties, cs);
-        this.datum    = datum;
-        this.ensemble = ensemble;
-        checkDatum(datum, ensemble);
+        super(properties, VerticalDatum.class, datum, ensemble, cs);
         checkDimension(1, 1, cs);
     }
 
@@ -165,8 +141,6 @@ public class DefaultVerticalCRS extends AbstractCRS 
implements VerticalCRS {
      */
     private DefaultVerticalCRS(final DefaultVerticalCRS original, final 
AbstractCS cs) {
         super(original, null, cs);
-        datum    = original.datum;
-        ensemble = original.ensemble;
     }
 
     /**
@@ -182,9 +156,6 @@ public class DefaultVerticalCRS extends AbstractCRS 
implements VerticalCRS {
      */
     protected DefaultVerticalCRS(final VerticalCRS crs) {
         super(crs);
-        datum    = crs.getDatum();
-        ensemble = crs.getDatumEnsemble();
-        checkDatum(datum, ensemble);
     }
 
     /**
@@ -229,7 +200,7 @@ public class DefaultVerticalCRS extends AbstractCRS 
implements VerticalCRS {
     @Override
     @XmlElement(name = "verticalDatum", required = true)
     public VerticalDatum getDatum() {
-        return datum;
+        return super.getDatum();
     }
 
     /**
@@ -245,7 +216,7 @@ public class DefaultVerticalCRS extends AbstractCRS 
implements VerticalCRS {
      */
     @Override
     public DatumEnsemble<VerticalDatum> getDatumEnsemble() {
-        return ensemble;
+        return super.getDatumEnsemble();
     }
 
     /**
@@ -314,7 +285,6 @@ public class DefaultVerticalCRS extends AbstractCRS 
implements VerticalCRS {
      * reserved to JAXB, which will assign values to the fields using 
reflection.
      */
     private DefaultVerticalCRS() {
-        ensemble = null;
         /*
          * The datum and the coordinate system are mandatory for SIS working. 
We do not verify their presence
          * here because the verification would have to be done in an 
'afterMarshal(…)' method and throwing an
@@ -329,11 +299,7 @@ public class DefaultVerticalCRS extends AbstractCRS 
implements VerticalCRS {
      * @see #getDatum()
      */
     private void setDatum(final VerticalDatum value) {
-        if (datum == null) {
-            datum = value;
-        } else {
-            ImplementationHelper.propertyAlreadySet(DefaultVerticalCRS.class, 
"setDatum", "verticalDatum");
-        }
+        setDatum("verticalDatum", value);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultDatumEnsemble.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultDatumEnsemble.java
index 12b563e81c..0b03e1ba43 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultDatumEnsemble.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultDatumEnsemble.java
@@ -32,14 +32,14 @@ import org.apache.sis.referencing.privy.WKTKeywords;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.Utilities;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
  * Collection of datums which for low accuracy requirements may be considered
  * to be insignificantly different from each other.
  *
- * @author  OGC Topic 2 (for abstract model and documentation)
- * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
  *
  * @param <D> the type of datum contained in this ensemble.
@@ -158,10 +158,15 @@ public class DefaultDatumEnsemble<D extends Datum> 
extends AbstractIdentifiedObj
 
     /**
      * Verifies this ensemble. All members shall have the same conventional 
reference system.
+     * No member can be an instance of {@link PseudoDatum}.
      */
     private void validate() {
         IdentifiedObject rs = null;
         for (final D datum : members) {
+            if (datum instanceof PseudoDatum<?>) {
+                throw new IllegalArgumentException(
+                        Errors.format(Errors.Keys.IllegalPropertyValueClass_2, 
"members", PseudoDatum.class));
+            }
             final IdentifiedObject dr = datum.getConventionalRS().orElse(null);
             if (dr != null) {
                 if (rs == null) {
@@ -224,7 +229,10 @@ public class DefaultDatumEnsemble<D extends Datum> extends 
AbstractIdentifiedObj
      * @return {@code true} if both objects are equal.
      */
     @Override
-    public boolean equals(final Object object, final ComparisonMode mode) {
+    public boolean equals(Object object, final ComparisonMode mode) {
+        if (mode != ComparisonMode.STRICT && object instanceof PseudoDatum<?>) 
{
+            object = ((PseudoDatum<?>) object).ensemble;
+        }
         if (!super.equals(object, mode)) {
             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
new file mode 100644
index 0000000000..6d13d6200f
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/PseudoDatum.java
@@ -0,0 +1,589 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.datum;
+
+import java.util.Set;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import java.time.temporal.Temporal;
+import java.io.Serializable;
+import org.opengis.util.GenericName;
+import org.opengis.util.InternationalString;
+import org.opengis.metadata.Identifier;
+import org.opengis.referencing.IdentifiedObject;
+import org.opengis.referencing.ObjectDomain;
+import org.opengis.referencing.datum.*;
+import org.opengis.referencing.crs.*;
+import org.apache.sis.util.Utilities;
+import org.apache.sis.util.ComparisonMode;
+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;
+
+
+/**
+ * A datum ensemble viewed as if it was a single datum for the sake of 
simplicity.
+ * This pseudo-datum is a non-standard mechanism used by the Apache 
<abbr>SIS</abbr> implementation
+ * for handling datum and datum ensemble in a uniform way. For example, {@code 
PseudoDatum.of(crs)}
+ * allows to {@linkplain IdentifiedObjects#isHeuristicMatchForName compare the 
datum name} without
+ * the need to check which one of the {@code getDatum()} or {@code 
getDatumEnsemble()} methods
+ * returns a non-null value.
+ *
+ * <p>{@code PseudoDatum} instances should live only for a short time.
+ * They should not be stored as {@link SingleCRS} properties.
+ * If a {@code PseudoDatum} instances is given to the constructor of an Apache 
<abbr>SIS</abbr> class,
+ * the constructor will automatically unwraps the {@linkplain #ensemble}.</p>
+ *
+ * <h2>Default method implementations</h2>
+ * Unless otherwise specified in the Javadoc, all methods in this class 
delegate
+ * to the same method in the wrapper datum {@linkplain #ensemble}.
+ *
+ * <h2>Object comparisons</h2>
+ * The {@link #equals(Object)} method returns {@code true} only if the two 
compared objects are instances
+ * of the same class, which implies that the {@code Object} argument must be a 
{@code PseudoDatum}.
+ * The {@link #equals(Object, ComparisonMode)} method with a non-strict 
comparison mode compares
+ * the wrapped datum ensemble, which implies that:
+ *
+ * <ul>
+ *   <li>A pseudo-datum is never equal to a real datum, regardless the names 
and identifiers of the compared objects.</li>
+ *   <li>A pseudo-datum can be equal to another pseudo-datum or to a datum 
ensemble.</li>
+ * </ul>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ *
+ * @param <D> the type of datum contained in this ensemble.
+ *
+ * @since 1.5
+ */
+public abstract class PseudoDatum<D extends Datum> implements Datum, 
LenientComparable, Serializable {
+    /**
+     * For cross-versions compatibility.
+     */
+    private static final long serialVersionUID = 3889895625961827486L;
+
+    /**
+     * The datum ensemble wrapped by this pseudo-datum.
+     */
+    @SuppressWarnings("serial")     // Most SIS implementations are 
serializable.
+    public final DatumEnsemble<D> ensemble;
+
+    /**
+     * Creates a new pseudo-datum.
+     *
+     * @param ensemble the datum ensemble wrapped by this pseudo-datum.
+     */
+    protected PseudoDatum(final DatumEnsemble<D> ensemble) {
+        this.ensemble = Objects.requireNonNull(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
+     * 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();
+    }
+
+    /**
+     * Returns the datum or pseudo-datum of the given geodetic 
<abbr>CRS</abbr>.
+     * If the given <abbr>CRS</abbr> is associated to a non-null datum, then 
this method returns that datum.
+     * Otherwise, this method returns the <abbr>CRS</abbr> datum ensemble 
wrapped in a pseudo-datum.
+     * In the latter case, the pseudo-datum implementations of the {@link 
GeodeticDatum#getEllipsoid()}
+     * and {@link GeodeticDatum#getPrimeMeridian()} methods expect an 
ellipsoid or prime meridian which
+     * is the same for all {@linkplain #ensemble} members.
+     * If this condition does not hold, a {@link GeodeticException} will be 
thrown.
+     *
+     * @param  crs  the coordinate reference system for which to get the datum 
or datum ensemble.
+     * @return the datum or pseudo-datum of the given <abbr>CRS</abbr>.
+     * @throws NullPointerException if the given argument is {@code null},
+     *         or if both the datum and datum ensemble are null.
+     */
+    public static GeodeticDatum of(final GeodeticCRS crs) {
+        GeodeticDatum datum = crs.getDatum();
+        if (datum == null) {
+            datum = new PseudoDatum.Geodetic(crs.getDatumEnsemble());
+        }
+        return datum;
+    }
+
+    /**
+     * Returns the datum or pseudo-datum of the given vertical 
<abbr>CRS</abbr>.
+     * If the given <abbr>CRS</abbr> is associated to a non-null datum, then 
this method returns that datum.
+     * Otherwise, this method returns the <abbr>CRS</abbr> datum ensemble 
wrapped in a pseudo-datum.
+     *
+     * @param  crs  the coordinate reference system for which to get the datum 
or datum ensemble.
+     * @return the datum or pseudo-datum of the given <abbr>CRS</abbr>.
+     * @throws NullPointerException if the given argument is {@code null},
+     *         or if both the datum and datum ensemble are null.
+     */
+    public static VerticalDatum of(final VerticalCRS crs) {
+        VerticalDatum datum = crs.getDatum();
+        if (datum == null) {
+            datum = new PseudoDatum.Vertical(crs.getDatumEnsemble());
+        }
+        return datum;
+    }
+
+    /**
+     * Returns the datum or pseudo-datum of the given temporal 
<abbr>CRS</abbr>.
+     * If the given <abbr>CRS</abbr> is associated to a non-null datum, then 
this method returns that datum.
+     * Otherwise, this method returns the <abbr>CRS</abbr> datum ensemble 
wrapped in a pseudo-datum.
+     * In the latter case, the pseudo-datum implementations of the {@link 
TemporalDatum#getOrigin()}
+     * expects a temporal origin which is the same for all {@linkplain 
#ensemble} members.
+     * If this condition does not hold, a {@link GeodeticException} will be 
thrown.
+     *
+     * @param  crs  the coordinate reference system for which to get the datum 
or datum ensemble.
+     * @return the datum or pseudo-datum of the given <abbr>CRS</abbr>.
+     * @throws NullPointerException if the given argument is {@code null},
+     *         or if both the datum and datum ensemble are null.
+     */
+    public static TemporalDatum of(final TemporalCRS crs) {
+        TemporalDatum datum = crs.getDatum();
+        if (datum == null) {
+            datum = new PseudoDatum.Time(crs.getDatumEnsemble());
+        }
+        return datum;
+    }
+
+    /**
+     * Returns the datum or pseudo-datum of the given parametric 
<abbr>CRS</abbr>.
+     * If the given <abbr>CRS</abbr> is associated to a non-null datum, then 
this method returns that datum.
+     * Otherwise, this method returns the <abbr>CRS</abbr> datum ensemble 
wrapped in a pseudo-datum.
+     *
+     * @param  crs  the coordinate reference system for which to get the datum 
or datum ensemble.
+     * @return the datum or pseudo-datum of the given <abbr>CRS</abbr>.
+     * @throws NullPointerException if the given argument is {@code null},
+     *         or if both the datum and datum ensemble are null.
+     */
+    public static ParametricDatum of(final ParametricCRS crs) {
+        ParametricDatum datum = crs.getDatum();
+        if (datum == null) {
+            datum = new PseudoDatum.Parametric(crs.getDatumEnsemble());
+        }
+        return datum;
+    }
+
+    /**
+     * Returns the datum or pseudo-datum of the given engineering 
<abbr>CRS</abbr>.
+     * If the given <abbr>CRS</abbr> is associated to a non-null datum, then 
this method returns that datum.
+     * Otherwise, this method returns the <abbr>CRS</abbr> datum ensemble 
wrapped in a pseudo-datum.
+     *
+     * @param  crs  the coordinate reference system for which to get the datum 
or datum ensemble.
+     * @return the datum or pseudo-datum of the given <abbr>CRS</abbr>.
+     * @throws NullPointerException if the given argument is {@code null},
+     *         or if both the datum and datum ensemble are null.
+     */
+    public static EngineeringDatum of(final EngineeringCRS crs) {
+        EngineeringDatum datum = crs.getDatum();
+        if (datum == null) {
+            datum = new PseudoDatum.Engineering(crs.getDatumEnsemble());
+        }
+        return datum;
+    }
+
+    /**
+     * Returns the GeoAPI interface of the ensemble members.
+     * It should also be the interface implemented by this class.
+     *
+     * @return the GeoAPI interface of the ensemble members.
+     */
+    public abstract Class<D> getInterface();
+
+    /**
+     * Returns the primary name by which the datum ensemble is identified.
+     *
+     * @return {@code ensemble.getName()}.
+     * @hidden
+     */
+    @Override
+    public Identifier getName() {
+        return ensemble.getName();
+    }
+
+    /**
+     * Returns alternative names by which the datum ensemble is identified.
+     *
+     * @return {@code ensemble.getAlias()}.
+     * @hidden
+     */
+    @Override
+    public Collection<GenericName> getAlias() {
+        return ensemble.getAlias();
+    }
+
+    /**
+     * Returns an identifier which references elsewhere the datum ensemble 
information.
+     *
+     * @return {@code ensemble.getIdentifiers()}.
+     * @hidden
+     */
+    @Override
+    public Set<Identifier> getIdentifiers() {
+        return ensemble.getIdentifiers();
+    }
+
+    /**
+     * Returns the usage of the datum ensemble.
+     *
+     * @return {@code ensemble.getDomains()}.
+     * @hidden
+     */
+    @Override
+    public Collection<ObjectDomain> getDomains() {
+        return ensemble.getDomains();
+    }
+
+    /**
+     * Returns an anchor definition which is common to all members of the 
datum ensemble.
+     * If the value is not the same for all members (including the case where 
a member
+     * has an empty value), then this method returns an empty value.
+     *
+     * @return the common anchor definition, or empty if there is no common 
value.
+     */
+    @Override
+    public Optional<InternationalString> getAnchorDefinition() {
+        return getCommonOptionalValue(Datum::getAnchorDefinition);
+    }
+
+    /**
+     * Returns an anchor epoch which is common to all members of the datum 
ensemble.
+     * If the value is not the same for all members (including the case where 
a member
+     * has an empty value), then this method returns an empty value.
+     *
+     * @return the common anchor epoch, or empty if there is no common value.
+     */
+    @Override
+    public Optional<Temporal> getAnchorEpoch() {
+        return getCommonOptionalValue(Datum::getAnchorEpoch);
+    }
+
+    /**
+     * Returns a publication date which is common to all members of the datum 
ensemble.
+     * If the value is not the same for all members (including the case where 
a member
+     * has an empty value), then this method returns an empty value.
+     *
+     * @return the common publication date, or empty if there is no common 
value.
+     */
+    @Override
+    public Optional<Temporal> getPublicationDate() {
+        return getCommonOptionalValue(Datum::getPublicationDate);
+    }
+
+    /**
+     * Returns a conventional reference system which is common to all members 
of the datum ensemble.
+     * The returned value should never be empty, because it is illegal for a 
datum ensemble to have
+     * members with different conventional reference system. If this case 
nevertheless happens,
+     * this method returns an empty value.
+     *
+     * @return the common conventional reference system, or empty if there is 
no common value.
+     */
+    @Override
+    public Optional<IdentifiedObject> getConventionalRS() {
+        return getCommonOptionalValue(Datum::getConventionalRS);
+    }
+
+    /**
+     * Returns an optional value which is common to all ensemble members.
+     * If all members do not have the same value, returns an empty value.
+     *
+     * @param  <V>     type of value.
+     * @param  getter  method to invoke on each member for getting the value.
+     * @return a value common to all members, or {@code null} if none.
+     */
+    final <V> Optional<V> getCommonOptionalValue(final Function<D, 
Optional<V>> getter) {
+        final Iterator<D> it = ensemble.getMembers().iterator();
+check:  if (it.hasNext()) {
+            final Optional<V> value = getter.apply(it.next());
+            if (value.isPresent()) {
+                while (it.hasNext()) {
+                    if (!value.equals(getter.apply(it.next()))) {
+                        break check;
+                    }
+                }
+                return value;
+            }
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Returns a mandatory value which is common to all ensemble members.
+     *
+     * @param  <V>     type of value.
+     * @param  getter  method to invoke on each member for getting the value.
+     * @return a value common to all members.
+     * @throws NoSuchElementException if the ensemble does not contain at 
least one member.
+     * @throws GeodeticException if the value is not the same for all members 
of the datum ensemble.
+     */
+    final <V> V getCommonMandatoryValue(final Function<D, V> getter) {
+        final Iterator<D> it = ensemble.getMembers().iterator();
+        final V value = getter.apply(it.next());   // Mandatory.
+        if (it.hasNext()) {
+            final V other = getter.apply(it.next());
+            if (!Objects.equals(value, other)) {
+                throw new 
GeodeticException(Errors.format(Errors.Keys.NonUniformValue_2,
+                        (value instanceof IdentifiedObject) ? 
IdentifiedObjects.getDisplayName((IdentifiedObject) value) : value,
+                        (other instanceof IdentifiedObject) ? 
IdentifiedObjects.getDisplayName((IdentifiedObject) other) : other));
+            }
+        }
+        return value;
+    }
+
+    /**
+     * Returns comments on or information about the datum ensemble.
+     *
+     * @return {@code ensemble.getRemarks()}.
+     * @hidden
+     */
+    @Override
+    public Optional<InternationalString> getRemarks() {
+        return ensemble.getRemarks();
+    }
+
+    /**
+     * Formats a <i>Well-Known Text</i> (WKT) for the datum ensemble.
+     *
+     * @return {@code ensemble.toWKT()}.
+     * @hidden
+     */
+    @Override
+    public String toWKT() {
+        return ensemble.toWKT();
+    }
+
+    /**
+     * Returns a string representation of the datum ensemble.
+     *
+     * @return {@code ensemble.toString()}.
+     * @hidden
+     */
+    @Override
+    public String toString() {
+        return ensemble.toString();
+    }
+
+    /**
+     * Returns a hash-code value of this pseudo-datum.
+     *
+     * @return a hash-code value of this pseudo-datum.
+     */
+    @Override
+    public int hashCode() {
+        return ensemble.hashCode() ^ getClass().hashCode();
+    }
+
+    /**
+     * Compares this pseudo-datum to the given object for equality.
+     * The two objects are equal if they are of the same classes and
+     * the wrapped {@link #ensemble} are equal.
+     *
+     * @param  other  the object to compare with this pseudo-datum.
+     * @return whether the two objects are equal.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        return (other != null) && other.getClass() == getClass() && 
ensemble.equals(((PseudoDatum<?>) other).ensemble);
+    }
+
+    /**
+     * Compares this object with the given object for equality.
+     * If the comparison mode is strict, then this method delegates to {@link 
#equals(Object)}.
+     * Otherwise, this method unwrap the ensembles, then compare the ensembles.
+     *
+     * @param  other  the object to compare to {@code this}.
+     * @param  mode   the strictness level of the comparison.
+     * @return {@code true} if both objects are equal according the given 
comparison mode.
+     */
+    @Override
+    public boolean equals(Object other, final ComparisonMode mode) {
+        if (mode == ComparisonMode.STRICT) {
+            return equals(other);
+        }
+        if (other instanceof PseudoDatum<?>) {
+            other = ((PseudoDatum<?>) other).ensemble;
+        }
+        return Utilities.deepEquals(ensemble, other, mode);
+    }
+
+    /**
+     * A pseudo-datum for an ensemble of geodetic datum.
+     */
+    private static final class Geodetic extends PseudoDatum<GeodeticDatum> 
implements GeodeticDatum {
+        /** For cross-versions compatibility. */
+        private static final long serialVersionUID = 7669230365507661290L;
+
+        /** Creates a new pseudo-datum wrapping the given ensemble. */
+        Geodetic(final DatumEnsemble<GeodeticDatum> ensemble) {
+            super(ensemble);
+        }
+
+        /**
+         * Returns the GeoAPI interface implemented by this pseudo-datum.
+         */
+        @Override
+        public Class<GeodeticDatum> getInterface() {
+            return GeodeticDatum.class;
+        }
+
+        /**
+         * Returns the ellipsoid which is indirectly (through a datum) 
associated to this datum ensemble.
+         * If all members of the ensemble use the same ellipsoid, then this 
method returns that ellipsoid.
+         *
+         * @return the ellipsoid indirectly associated to this datum ensemble.
+         * @throws NoSuchElementException if the ensemble does not contain at 
least one member.
+         * @throws GeodeticException if the ellipsoid is not the same for all 
members of the datum ensemble.
+         */
+        @Override
+        public Ellipsoid getEllipsoid() {
+            return getCommonMandatoryValue(GeodeticDatum::getEllipsoid);
+        }
+
+        /**
+         * Returns the prime meridian which is indirectly (through a datum) 
associated to this datum ensemble.
+         * If all members of the ensemble use the same prime meridian, then 
this method returns that meridian.
+         *
+         * @return the prime meridian indirectly associated to this datum 
ensemble.
+         * @throws NoSuchElementException if the ensemble does not contain at 
least one member.
+         * @throws GeodeticException if the prime meridian is not the same for 
all members of the datum ensemble.
+         */
+        @Override
+        public PrimeMeridian getPrimeMeridian() {
+            return getCommonMandatoryValue(GeodeticDatum::getPrimeMeridian);
+        }
+    }
+
+    /**
+     * A pseudo-datum for an ensemble of vertical datum.
+     */
+    private static final class Vertical extends PseudoDatum<VerticalDatum> 
implements VerticalDatum {
+        /** For cross-versions compatibility. */
+        private static final long serialVersionUID = 7242417944400289818L;
+
+        /** Creates a new pseudo-datum wrapping the given ensemble. */
+        Vertical(final DatumEnsemble<VerticalDatum> ensemble) {
+            super(ensemble);
+        }
+
+        /**
+         * Returns the GeoAPI interface implemented by this pseudo-datum.
+         */
+        @Override
+        public Class<VerticalDatum> getInterface() {
+            return VerticalDatum.class;
+        }
+
+        /**
+         * Returns a realization method which is common to all members of the 
datum ensemble.
+         * If the value is not the same for all members (including the case 
where a member
+         * has an empty value), then this method returns an empty value.
+         *
+         * @return the common realization method, or empty if there is no 
common value.
+         */
+        @Override
+        public Optional<RealizationMethod> getRealizationMethod() {
+            return getCommonOptionalValue(VerticalDatum::getRealizationMethod);
+        }
+    }
+
+    /**
+     * A pseudo-datum for an ensemble of temporal datum.
+     */
+    private static final class Time extends PseudoDatum<TemporalDatum> 
implements TemporalDatum {
+        /** For cross-versions compatibility. */
+        private static final long serialVersionUID = -4208563828181087035L;
+
+        /** Creates a new pseudo-datum wrapping the given ensemble. */
+        Time(final DatumEnsemble<TemporalDatum> ensemble) {
+            super(ensemble);
+        }
+
+        /**
+         * Returns the GeoAPI interface implemented by this pseudo-datum.
+         */
+        @Override
+        public Class<TemporalDatum> getInterface() {
+            return TemporalDatum.class;
+        }
+
+        /**
+         * Returns the temporal origin which is indirectly (through a datum) 
associated to this datum ensemble.
+         * If all members of the ensemble use the same temporal origin, then 
this method returns that origin.
+         *
+         * @return the temporal origin indirectly associated to this datum 
ensemble.
+         * @throws NoSuchElementException if the ensemble does not contain at 
least one member.
+         * @throws GeodeticException if the temporal origin is not the same 
for all members of the datum ensemble.
+         */
+        @Override
+        public Temporal getOrigin() {
+            return getCommonMandatoryValue(TemporalDatum::getOrigin);
+        }
+    }
+
+    /**
+     * A pseudo-datum for an ensemble of parametric datum.
+     */
+    private static final class Parametric extends PseudoDatum<ParametricDatum> 
implements ParametricDatum {
+        /** For cross-versions compatibility. */
+        private static final long serialVersionUID = -8277774591738789437L;
+
+        /** Creates a new pseudo-datum wrapping the given ensemble. */
+        Parametric(final DatumEnsemble<ParametricDatum> ensemble) {
+            super(ensemble);
+        }
+
+        /** Returns the GeoAPI interface implemented by this pseudo-datum. */
+        @Override public Class<ParametricDatum> getInterface() {
+            return ParametricDatum.class;
+        }
+    }
+
+    /**
+     * A pseudo-datum for an ensemble of engineering datum.
+     */
+    private static final class Engineering extends 
PseudoDatum<EngineeringDatum> implements EngineeringDatum {
+        /** For cross-versions compatibility. */
+        private static final long serialVersionUID = -8978468990963666861L;
+
+        /** Creates a new pseudo-datum wrapping the given ensemble. */
+        Engineering(final DatumEnsemble<EngineeringDatum> ensemble) {
+            super(ensemble);
+        }
+
+        /** Returns the GeoAPI interface implemented by this pseudo-datum. */
+        @Override public Class<EngineeringDatum> getInterface() {
+            return EngineeringDatum.class;
+        }
+    }
+}
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 6200fb4977..a01cbe8868 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
@@ -54,6 +54,7 @@ import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.datum.BursaWolfParameters;
 import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.provider.Affine;
@@ -338,8 +339,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         ////                                                                   
     ////
         
////////////////////////////////////////////////////////////////////////////////
         if (sourceCRS instanceof SingleCRS && targetCRS instanceof SingleCRS) {
-            final Datum sourceDatum = ((SingleCRS) sourceCRS).getDatum();
-            final Datum targetDatum = ((SingleCRS) targetCRS).getDatum();
+            final IdentifiedObject sourceDatum = 
PseudoDatum.getDatumOrEnsemble((SingleCRS) sourceCRS);
+            final IdentifiedObject targetDatum = 
PseudoDatum.getDatumOrEnsemble((SingleCRS) targetCRS);
             if (equalsIgnoreMetadata(sourceDatum, targetDatum)) try {
                 /*
                  * Because the CRS type is determined by the datum type 
(sometimes completed by the CS type),
@@ -514,8 +515,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
     {
         final CoordinateSystem sourceCS = sourceCRS.getCoordinateSystem();
         final CoordinateSystem targetCS = targetCRS.getCoordinateSystem();
-        final GeodeticDatum sourceDatum = sourceCRS.getDatum();
-        final GeodeticDatum targetDatum = targetCRS.getDatum();
+        final GeodeticDatum sourceDatum = PseudoDatum.of(sourceCRS);
+        final GeodeticDatum targetDatum = PseudoDatum.of(targetCRS);
         Matrix datumShift = null;
         /*
          * If the prime meridian is not the same, we will concatenate a 
longitude rotation before or after datum shift
@@ -741,7 +742,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
             final EllipsoidalCS cs = 
CommonCRS.WGS84.geographic3D().getCoordinateSystem();
             if (!equalsIgnoreMetadata(interpolationCS, cs)) {
                 final GeographicCRS stepCRS = factorySIS.crsFactory
-                        .createGeographicCRS(derivedFrom(sourceCRS), 
sourceCRS.getDatum(), cs);
+                        .createGeographicCRS(derivedFrom(sourceCRS), 
sourceCRS.getDatum(), sourceCRS.getDatumEnsemble(), cs);
                 step1 = createOperation(sourceCRS, 
toAuthorityDefinition(GeographicCRS.class, stepCRS));
                 interpolationCRS = step1.getTargetCRS();
                 interpolationCS  = interpolationCRS.getCoordinateSystem();
@@ -765,7 +766,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         VerticalCRS heightCRS = targetCRS;      // First candidate, will be 
replaced if it doesn't fit.
         VerticalCS  heightCS  = heightCRS.getCoordinateSystem();
         if (equalsIgnoreMetadata(heightCS.getAxis(0), expectedAxis)) {
-            isEllipsoidalHeight = 
ReferencingUtilities.isEllipsoidalHeight(heightCRS.getDatum());
+            isEllipsoidalHeight = 
ReferencingUtilities.isEllipsoidalHeight(PseudoDatum.of(heightCRS));
         } else {
             heightCRS = CommonCRS.Vertical.ELLIPSOIDAL.crs();
             heightCS  = heightCRS.getCoordinateSystem();
@@ -823,8 +824,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
                                                             final VerticalCRS 
targetCRS)
             throws FactoryException
     {
-        final VerticalDatum sourceDatum = sourceCRS.getDatum();
-        final VerticalDatum targetDatum = targetCRS.getDatum();
+        final VerticalDatum sourceDatum = PseudoDatum.of(sourceCRS);
+        final VerticalDatum targetDatum = PseudoDatum.of(targetCRS);
         if (!equalsIgnoreMetadata(sourceDatum, targetDatum)) {
             throw new OperationNotFoundException(notFoundMessage(sourceDatum, 
targetDatum));
         }
@@ -857,8 +858,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
                                                             final TemporalCRS 
targetCRS)
             throws FactoryException
     {
-        final TemporalDatum sourceDatum = sourceCRS.getDatum();
-        final TemporalDatum targetDatum = targetCRS.getDatum();
+        final TemporalDatum sourceDatum = PseudoDatum.of(sourceCRS);
+        final TemporalDatum targetDatum = PseudoDatum.of(targetCRS);
         final TimeCS sourceCS = sourceCRS.getCoordinateSystem();
         final TimeCS targetCS = targetCRS.getCoordinateSystem();
         /*
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 7edfc6f93f..136f8b010b 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
@@ -51,8 +51,11 @@ import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.provider.Affine;
+import org.apache.sis.referencing.operation.provider.AbstractProvider;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
 import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
@@ -67,8 +70,6 @@ import org.apache.sis.referencing.privy.ReferencingUtilities;
 import org.apache.sis.referencing.internal.ParameterizedTransformBuilder;
 import org.apache.sis.referencing.internal.DeferredCoordinateOperation;
 import org.apache.sis.referencing.internal.Resources;
-import org.apache.sis.referencing.operation.provider.Affine;
-import org.apache.sis.referencing.operation.provider.AbstractProvider;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.metadata.iso.extent.Extents;
 import org.apache.sis.system.Semaphores;
@@ -1194,6 +1195,9 @@ class CoordinateOperationRegistry {
     /**
      * If the given CRS is two-dimensional, appends an ellipsoidal height to 
it.
      * It is caller's responsibility to ensure that the given CRS is 
geographic.
+     *
+     * @param  crs        the two-dimensional CRS to replace by a 
three-dimensional CRS.
+     * @param  candidate  an existing three-dimensional instance that may be 
suitable, or {@code null}.
      */
     private CoordinateReferenceSystem toGeodetic3D(CoordinateReferenceSystem 
crs,
             final CoordinateReferenceSystem candidate) throws FactoryException
@@ -1208,7 +1212,9 @@ class CoordinateOperationRegistry {
          * to return the existing instance.
          */
         if (crs.getClass() == candidate.getClass() && 
candidate.getCoordinateSystem().getDimension() == 3) {
-            if (Utilities.equalsIgnoreMetadata(((SingleCRS) crs).getDatum(), 
((SingleCRS) candidate).getDatum())) {
+            if 
(Utilities.equalsIgnoreMetadata(PseudoDatum.getDatumOrEnsemble((SingleCRS) 
candidate),
+                                               
PseudoDatum.getDatumOrEnsemble((SingleCRS) crs)))
+            {
                 return candidate;               // Keep the existing instance 
since it may contain useful metadata.
             }
         }
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 1573b2ef58..40c87d28a8 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
@@ -37,12 +37,12 @@ import 
org.apache.sis.referencing.factory.GeodeticObjectFactory;
 import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 import org.apache.sis.referencing.operation.transform.AbstractMathTransform;
 import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.referencing.internal.MergedProperties;
 import org.apache.sis.referencing.internal.ParameterizedTransformBuilder;
 import org.apache.sis.referencing.privy.CoordinateOperations;
 import org.apache.sis.referencing.privy.ReferencingFactoryContainer;
-import org.apache.sis.referencing.privy.ReferencingUtilities;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.Utilities;
@@ -411,11 +411,11 @@ public class DefaultCoordinateOperationFactory extends 
AbstractFactory implement
         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] = 
ReferencingUtilities.getDatumOrEnsemble(components.get(i));
+            datum[i] = PseudoDatum.getDatumOrEnsemble(components.get(i));
         }
         components = CRS.getSingleComponents(targetCRS);
 next:   for (int i=components.size(); --i >= 0;) {
-            final IdentifiedObject d = 
ReferencingUtilities.getDatumOrEnsemble(components.get(i));
+            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.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/DefinitionVerifier.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/DefinitionVerifier.java
index 2bcd77dd12..5e1bd59b0d 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/DefinitionVerifier.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/DefinitionVerifier.java
@@ -32,6 +32,7 @@ import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.crs.AbstractCRS;
 import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
 import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
 import org.apache.sis.referencing.internal.Resources;
@@ -332,7 +333,9 @@ public final class DefinitionVerifier {
                 {
                     return PRIME_MERIDIAN;
                 }
-                if (!Utilities.equalsApproximately(crsA.getDatum(), 
crsG.getDatum())) {
+                if 
(!Utilities.equalsApproximately(PseudoDatum.getDatumOrEnsemble(crsA),
+                                                   
PseudoDatum.getDatumOrEnsemble(crsG)))
+                {
                     return DATUM;
                 }
                 break;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/EllipsoidalHeightCombiner.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/EllipsoidalHeightCombiner.java
index 27deaab23a..0a9ac3d3c6 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/EllipsoidalHeightCombiner.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/EllipsoidalHeightCombiner.java
@@ -35,6 +35,7 @@ import org.opengis.referencing.datum.VerticalDatum;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.CoordinateOperationFactory;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.metadata.iso.extent.Extents;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
@@ -113,7 +114,7 @@ public final class EllipsoidalHeightCombiner {
         for (int i=0; i<components.length; i++) {
             final CoordinateReferenceSystem vertical = components[i];
             if (vertical instanceof VerticalCRS) {
-                final VerticalDatum datum = ((VerticalCRS) 
vertical).getDatum();
+                final VerticalDatum datum = PseudoDatum.of((VerticalCRS) 
vertical);
                 if (ReferencingUtilities.isEllipsoidalHeight(datum)) {
                     int axisPosition = 0;
                     CoordinateSystem cs2D;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/GeodeticObjectBuilder.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/GeodeticObjectBuilder.java
index 1c30eea584..bd34098df3 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/GeodeticObjectBuilder.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/GeodeticObjectBuilder.java
@@ -84,11 +84,15 @@ import org.opengis.referencing.crs.GeodeticCRS;
 public class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> {
     /**
      * The geodetic reference frame, or {@code null} if none.
+     *
+     * @see #getDatumOrEnsemble()
      */
     private GeodeticDatum datum;
 
     /**
      * The datum ensemble, or {@code null} if none.
+     *
+     * @see #getDatumOrEnsemble()
      */
     private DatumEnsemble<GeodeticDatum> ensemble;
 
@@ -516,13 +520,21 @@ public class GeodeticObjectBuilder extends 
Builder<GeodeticObjectBuilder> {
      */
     public ProjectedCRS createProjectedCRS() throws FactoryException {
         GeographicCRS crs = getBaseCRS();
-        if (datum != null || ensemble != null) {
-            crs = factories.getCRSFactory().createGeographicCRS(
-                    name(datum != null ? datum : ensemble), datum, ensemble, 
crs.getCoordinateSystem());
+        final IdentifiedObject id = getDatumOrEnsemble();
+        if (id != null) {
+            crs = factories.getCRSFactory().createGeographicCRS(name(id), 
datum, ensemble, crs.getCoordinateSystem());
         }
         return createProjectedCRS(crs, factories.getStandardProjectedCS());
     }
 
+    /**
+     * Returns the datum if defined, or the datum ensemble otherwise.
+     * Both of them may be {@code null}.
+     */
+    private IdentifiedObject getDatumOrEnsemble() {
+        return (datum != null) ? datum : ensemble;
+    }
+
     /**
      * Returns the CRS to use as the base of a projected CRS.
      *
@@ -540,8 +552,7 @@ public class GeodeticObjectBuilder extends 
Builder<GeodeticObjectBuilder> {
      */
     public GeographicCRS createGeographicCRS() throws FactoryException {
         final GeographicCRS crs = getBaseCRS();
-        IdentifiedObject id = datum;
-        if (id == null) id = ensemble;
+        final IdentifiedObject id = getDatumOrEnsemble();
         if (id != null) {
             properties.putIfAbsent(GeographicCRS.NAME_KEY, id.getName());
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
index 07aa3e2bbd..6cb1e5cbb8 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
@@ -207,35 +207,27 @@ public final class ReferencingUtilities extends Static {
     }
 
     /**
-     * Returns whether the given <abbr>CRS</abbr> use the same datum or the 
same datum ensemble.
+     * Returns whether the given <abbr>CRS</abbr> uses the given datum.
      *
-     * @param  crs1  the first <abbr>CRS</abbr>.
-     * @param  crs2  the second <abbr>CRS</abbr>.
-     * @return whether the two reference systems use the same datum or the 
same datum ensemble.
+     * @param  crs    the <abbr>CRS</abbr>, or {@code null}.
+     * @param  datum  the datum to compare with the <abbr>CRS</abbr> datum or 
datum ensemble.
+     * @return whether the given CRS <abbr>CRS</abbr> uses the specified datum.
      */
-    public static boolean areMembersOfSameEnsemble(final SingleCRS crs1, final 
SingleCRS crs2) {
-        IdentifiedObject d1 = crs1.getDatum();
-        IdentifiedObject d2 = crs2.getDatum();
-        if (d1 == null && d2 == null) {
-            d1 = crs1.getDatumEnsemble();
-            d2 = crs2.getDatumEnsemble();
-            if (d1 == null && d2 == null) {
-                return false;
+    public static boolean uses(final SingleCRS crs, final Datum datum) {
+        if (crs != null && datum != null) {
+            if (Utilities.equalsIgnoreMetadata(crs.getDatum(), datum)) {
+                return true;
+            }
+            final var ensemble = crs.getDatumEnsemble();
+            if (ensemble != null) {
+                for (final Datum member : ensemble.getMembers()) {
+                    if (Utilities.equalsIgnoreMetadata(member, datum)) {
+                        return true;
+                    }
+                }
             }
         }
-        return Utilities.equalsIgnoreMetadata(d1, d2);
-    }
-
-    /**
-     * Returns the datum of the given <abbr>CRS</abbr> if presents, or the 
datum ensemble otherwise.
-     *
-     * @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.
-     */
-    public static IdentifiedObject getDatumOrEnsemble(final SingleCRS crs) {
-        if (crs == null) return null;
-        final Datum datum = crs.getDatum();
-        return (datum != null) ? datum : crs.getDatumEnsemble();
+        return false;
     }
 
     /**
@@ -295,6 +287,8 @@ public final class ReferencingUtilities extends Static {
 
     /**
      * Implementation of {@code getEllipsoid(CRS)} and {@code 
getPrimeMeridian(CRS)}.
+     * The difference between this method and {@link 
org.apache.sis.referencing.datum.PseudoDatum}
+     * is that this method ignore null values and does not throw an exception 
in case of mismatch.
      *
      * @param  <P>     the type of object to get.
      * @param  crs     the coordinate reference system for which to get the 
ellipsoid or prime meridian.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
index 381d6e5a9a..041a34a5ad 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
@@ -66,6 +66,7 @@ import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.crs.DefaultProjectedCRS;
 import org.apache.sis.referencing.crs.DefaultGeographicCRS;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.privy.WKTKeywords;
 import org.apache.sis.referencing.privy.NilReferencingObject;
 import org.apache.sis.referencing.privy.ReferencingUtilities;
@@ -1105,7 +1106,7 @@ public final class CRSBuilder extends 
ReferencingFactoryContainer {
          * were specified in the GeoTIFF file or if we got the default values. 
We do not compare units for that reason.
          */
         final Unit<Length> linearUnit = createLinearUnit(UnitKey.LINEAR);
-        final GeodeticDatum datum = crs.getDatum();
+        final GeodeticDatum datum = PseudoDatum.of(crs);
         verifyIdentifier(crs, datum, GeoKeys.GeodeticDatum);
         verify(datum, angularUnit, linearUnit);
         geoKeys.remove(GeoKeys.GeodeticCitation);
@@ -1174,7 +1175,7 @@ public final class CRSBuilder extends 
ReferencingFactoryContainer {
          */
         final Unit<Length> linearUnit = createLinearUnit(UnitKey.LINEAR);
         final Unit<Angle> angularUnit = createAngularUnit(UnitKey.ANGULAR);
-        final GeodeticDatum datum = crs.getDatum();
+        final GeodeticDatum datum = PseudoDatum.of(crs);
         verifyIdentifier(crs, datum, GeoKeys.GeodeticDatum);
         verify(datum, angularUnit, linearUnit);
     }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
index 0519241239..a88c8b21d4 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
@@ -53,6 +53,7 @@ import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.privy.CollectionsExt;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -276,7 +277,7 @@ public final class GeoEncoder {
             if (writeEPSG(GeoKeys.Vertical, crs)) {
                 writeName(GeoKeys.VerticalCitation, null, crs);
                 addUnits(UnitKey.VERTICAL, crs.getCoordinateSystem());
-                final VerticalDatum datum = crs.getDatum();
+                final VerticalDatum datum = PseudoDatum.of(crs);
                 if (writeEPSG(GeoKeys.VerticalDatum, datum)) {
                     /*
                      * OGC requirement 25.5 said "VerticalCitationGeoKey SHALL 
be populated."
@@ -325,41 +326,37 @@ public final class GeoEncoder {
         writeModelType(isBaseCRS ? GeoCodes.ModelTypeProjected : type);
         if (writeEPSG(GeoKeys.GeodeticCRS, crs)) {
             writeName(GeoKeys.GeodeticCitation, "GCS Name", crs);
-            final GeodeticDatum  datum = crs.getDatum();
-            final Ellipsoid  ellipsoid = 
ReferencingUtilities.getEllipsoid(crs);
-            if (datum == null && ellipsoid != null) {
-                // Case of a datum ensemble instead of a single datum.
-                writeShort(GeoKeys.GeodeticDatum, GeoCodes.userDefined);
-            } else if (!writeEPSG(GeoKeys.GeodeticDatum, datum)) {
-                return true;
-            }
-            appendName(WKTKeywords.Datum, datum);
-            final PrimeMeridian primem = 
ReferencingUtilities.getPrimeMeridian(crs);
-            final double longitude;
-            if (writeEPSG(GeoKeys.PrimeMeridian, primem)) {
-                appendName(WKTKeywords.PrimeM, primem);
-                longitude = primem.getGreenwichLongitude();
-            } else {
-                longitude = 0;                                      // Means 
"do not write prime meridian".
-            }
-            final Unit<Length>  axisUnit   = ellipsoid.getAxisUnit();
-            final Unit<?>       linearUnit = units.putIfAbsent(UnitKey.LINEAR, 
axisUnit);
-            final UnitConverter toLinear   = 
axisUnit.getConverterToAny(linearUnit != null ? linearUnit : axisUnit);
-            writeUnit(UnitKey.LINEAR);     // Must be after the `units` map 
have been updated.
-            writeUnit(UnitKey.ANGULAR);
-            if (writeEPSG(GeoKeys.Ellipsoid, ellipsoid)) {
-                appendName(WKTKeywords.Ellipsoid, ellipsoid);
-                writeDouble(GeoKeys.SemiMajorAxis, 
toLinear.convert(ellipsoid.getSemiMajorAxis()));
-                if (ellipsoid.isSphere() || !ellipsoid.isIvfDefinitive()) {
-                    writeDouble(GeoKeys.SemiMinorAxis, 
toLinear.convert(ellipsoid.getSemiMinorAxis()));
+            final GeodeticDatum  datum = PseudoDatum.of(crs);
+            if (writeEPSG(GeoKeys.GeodeticDatum, datum)) {
+                appendName(WKTKeywords.Datum, datum);
+                final PrimeMeridian primem = datum.getPrimeMeridian();
+                final double longitude;
+                if (writeEPSG(GeoKeys.PrimeMeridian, primem)) {
+                    appendName(WKTKeywords.PrimeM, datum);
+                    longitude = primem.getGreenwichLongitude();
                 } else {
-                    writeDouble(GeoKeys.InvFlattening, 
ellipsoid.getInverseFlattening());
+                    longitude = 0;                                      // 
Means "do not write prime meridian".
+                }
+                final Ellipsoid     ellipsoid  = datum.getEllipsoid();
+                final Unit<Length>  axisUnit   = ellipsoid.getAxisUnit();
+                final Unit<?>       linearUnit = 
units.putIfAbsent(UnitKey.LINEAR, axisUnit);
+                final UnitConverter toLinear   = 
axisUnit.getConverterToAny(linearUnit != null ? linearUnit : axisUnit);
+                writeUnit(UnitKey.LINEAR);     // Must be after the `units` 
map have been updated.
+                writeUnit(UnitKey.ANGULAR);
+                if (writeEPSG(GeoKeys.Ellipsoid, ellipsoid)) {
+                    appendName(WKTKeywords.Ellipsoid, ellipsoid);
+                    writeDouble(GeoKeys.SemiMajorAxis, 
toLinear.convert(ellipsoid.getSemiMajorAxis()));
+                    if (ellipsoid.isSphere() || !ellipsoid.isIvfDefinitive()) {
+                        writeDouble(GeoKeys.SemiMinorAxis, 
toLinear.convert(ellipsoid.getSemiMinorAxis()));
+                    } else {
+                        writeDouble(GeoKeys.InvFlattening, 
ellipsoid.getInverseFlattening());
+                    }
+                }
+                if (longitude != 0) {
+                    Unit<Angle> unit = primem.getAngularUnit();
+                    UnitConverter c = 
unit.getConverterToAny(units.getOrDefault(UnitKey.ANGULAR, Units.DEGREE));
+                    writeDouble(GeoKeys.PrimeMeridianLongitude, 
c.convert(longitude));
                 }
-            }
-            if (longitude != 0) {
-                Unit<Angle> unit = primem.getAngularUnit();
-                UnitConverter c = 
unit.getConverterToAny(units.getOrDefault(UnitKey.ANGULAR, Units.DEGREE));
-                writeDouble(GeoKeys.PrimeMeridianLongitude, 
c.convert(longitude));
             }
         } else if (isBaseCRS) {
             writeUnit(UnitKey.ANGULAR);         // Map projection parameters 
may need this unit.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
index 549224ca0e..8ac1db8491 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
@@ -47,11 +47,13 @@ import org.apache.sis.feature.DefaultFeatureType;
 import org.apache.sis.feature.FoliationRepresentation;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.privy.GeodeticObjectBuilder;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.privy.UnmodifiableArrayList;
 import org.apache.sis.util.privy.Numerics;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.temporal.LenientDateFormat;
 import org.apache.sis.storage.DataOptionKey;
 import org.apache.sis.storage.DataStoreException;
@@ -61,17 +63,16 @@ import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.base.URIDataStore;
-import org.apache.sis.io.InvalidSeekException;
-import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.storage.internal.RewindableLineReader;
 import org.apache.sis.storage.internal.Resources;
+import org.apache.sis.io.InvalidSeekException;
+import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.setup.OptionKey;
-import org.apache.sis.util.resources.Errors;
 import org.apache.sis.measure.Units;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -450,7 +451,7 @@ final class Store extends URIDataStore implements 
FeatureSet {
                     timeEncoding = TimeEncoding.ABSOLUTE;
                 } else {
                     temporal = builder.createTemporalCRS(startTime, timeUnit);
-                    timeEncoding = new TimeEncoding(temporal.getDatum(), 
timeUnit);
+                    timeEncoding = new TimeEncoding(PseudoDatum.of(temporal), 
timeUnit);
                 }
                 components[count++] = temporal;
                 name = name + " + " + temporal.getName().getCode();
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/OperationFinder.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/OperationFinder.java
index 5ac6b8de43..61de906f91 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/OperationFinder.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/OperationFinder.java
@@ -20,18 +20,17 @@ import java.util.function.Predicate;
 import javafx.concurrent.Task;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.coverage.grid.PixelInCell;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.gui.coverage.CoverageCanvas;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.util.logging.Logging;
 import static org.apache.sis.gui.internal.LogHandler.LOGGER;
 
@@ -173,7 +172,7 @@ abstract class OperationFinder extends Task<MathTransform> {
      * We use the {@link 
org.apache.sis.referencing.CommonCRS.Engineering#GRID} datum as a signature.
      */
     private static boolean isGridCRS(final CoordinateReferenceSystem crs) {
-        return (crs instanceof SingleCRS) && 
CommonCRS.Engineering.GRID.datum().equals(((SingleCRS) crs).getDatum());
+        return CommonCRS.Engineering.GRID.datumUsedBy(crs);
     }
 
     /**
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/Utils.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/Utils.java
index ecf7ceaf8e..4a1e07554f 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/Utils.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/Utils.java
@@ -20,7 +20,7 @@ import org.opengis.geometry.Envelope;
 import org.opengis.util.FactoryException;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.referencing.ReferenceSystem;
-import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.CRSAuthorityFactory;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
@@ -103,7 +103,7 @@ final class Utils {
      * Returns {@code true} if the given reference system should be ignored.
      */
     static boolean isIgnoreable(final ReferenceSystem system) {
-        return (system instanceof SingleCRS)
-                && CommonCRS.Engineering.DISPLAY.datum().equals(((SingleCRS) 
system).getDatum());
+        return (system instanceof CoordinateReferenceSystem) &&
+                
CommonCRS.Engineering.DISPLAY.datumUsedBy((CoordinateReferenceSystem) system);
     }
 }


Reply via email to