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