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 56da92e4a7 Allow `DefaultConcatenatedOperation` to contain steps in 
reverse order. https://issues.apache.org/jira/browse/SIS-594
56da92e4a7 is described below

commit 56da92e4a7838a2a4c83d708b9d1e27465a9153b
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Jan 9 19:41:37 2024 +0100

    Allow `DefaultConcatenatedOperation` to contain steps in reverse order.
    https://issues.apache.org/jira/browse/SIS-594
---
 .../apache/sis/referencing/IdentifiedObjects.java  |  16 ++-
 .../apache/sis/referencing/internal/Resources.java |   6 +
 .../sis/referencing/internal/Resources.properties  |   3 +-
 .../referencing/internal/Resources_fr.properties   |   3 +-
 .../apache/sis/referencing/operation/CRSPair.java  |  17 ++-
 .../operation/CoordinateOperationFinder.java       |  18 +--
 .../operation/CoordinateOperationRegistry.java     | 149 +++++++++++++++------
 .../operation/DefaultConcatenatedOperation.java    | 146 +++++++++++++++-----
 .../DefaultCoordinateOperationFactory.java         |  30 ++---
 .../operation/InverseOperationMethod.java          |   8 +-
 .../src/org.apache.sis.util/main/module-info.java  |   1 +
 .../main/org/apache/sis/pending/jdk/JDK21.java     |  31 +++++
 12 files changed, 312 insertions(+), 116 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
index 22429fc32c..fdb2b1c0d6 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
@@ -32,15 +32,18 @@ import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.crs.CompoundCRS;
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.ConcatenatedOperation;
+import static org.apache.sis.util.Utilities.equalsIgnoreMetadata;
 import org.apache.sis.util.Static;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.OptionalCandidate;
 import org.apache.sis.util.logging.Logging;
-import org.apache.sis.xml.IdentifierSpace;
 import org.apache.sis.util.internal.Strings;
 import org.apache.sis.util.internal.Constants;
 import org.apache.sis.util.internal.DefinitionURI;
+import static org.apache.sis.util.internal.CollectionsExt.nonNull;
+import org.apache.sis.pending.jdk.JDK21;
+import org.apache.sis.xml.IdentifierSpace;
 import org.apache.sis.metadata.internal.Identifiers;
 import org.apache.sis.metadata.internal.NameMeaning;
 import org.apache.sis.metadata.internal.NameToIdentifier;
@@ -48,7 +51,6 @@ import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
 import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
 import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException;
-import static org.apache.sis.util.internal.CollectionsExt.nonNull;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.referencing.ObjectDomain;
@@ -472,7 +474,15 @@ public final class IdentifiedObjects extends Static {
         if (object instanceof CompoundCRS) {
             components = CRS.getSingleComponents((CompoundCRS) object);
         } else if (object instanceof ConcatenatedOperation) {
-            components = ((ConcatenatedOperation) object).getOperations();
+            final var cop = (ConcatenatedOperation) object;
+            final List<? extends CoordinateOperation> steps = 
cop.getOperations();
+            if (equalsIgnoreMetadata(cop.getSourceCRS(), 
JDK21.getFirst(steps).getSourceCRS()) &&
+                equalsIgnoreMetadata(cop.getTargetCRS(), JDK21.getLast 
(steps).getTargetCRS()))
+            {
+                components = steps;
+            } else {
+                return null;
+            }
         } else {
             return null;
         }
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 af196a96b9..a40460efdf 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
@@ -350,6 +350,12 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short MismatchedPrimeMeridian_2 = 36;
 
+        /**
+         * Invalid coordinate operation step {0}, because the reference system 
“{1}” cannot be followed
+         * by “{2}”.
+         */
+        public static final short MismatchedSourceTargetCRS_3 = 100;
+
         /**
          * Despite its name, this parameter is effectively “{0}”.
          */
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 a04045bc48..18b3aba29d 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
@@ -26,6 +26,7 @@ ConformanceMeansDatumShift        = This result indicates if 
a datum shift metho
 ConstantProjParameterValue_1      = This parameter is shown for completeness, 
but should never have a value different than {0} for this projection.
 DeprecatedCode_3                  = Code \u201c{0}\u201d is deprecated and 
replaced by code {1}. Reason is: {2}
 ElementsOmitted_1                 = \u2026 {0} elements omitted \u2026
+FallbackAuthorityNotice           = Definitions from public sources. When a 
definition corresponds to an EPSG object (ignoring metadata), the EPSG code is 
provided as a reference where to find the complete definition.
 FallbackDefaultFactoryVersion_2   = There is no local registry for version {1} 
of \u201c{0}\u201d authority. Fallback on default version for objects creation.
 GeodeticDataBase_4                = {0} geodetic dataset version {1} on 
\u201c{2}\u201d version {3}.
 IgnoredServiceProvider_3          = More than one service provider of type 
\u2018{0}\u2019 are declared for \u201c{1}\u201d. Only the first provider (an 
instance of \u2018{2}\u2019) will be used.
@@ -76,7 +77,6 @@ DuplicatedSpatialComponents_1     = Compound coordinate 
reference systems cannot
 EllipsoidalHeightNotAllowed_1     = Compound coordinate reference systems 
should not contain ellipsoidal height. Use a three-dimensional 
{0,choice,0#geographic|1#projected} system instead.
 FileNotFound_2                    = Cannot find {0} file named \u201c{1}\u201d.
 FileNotReadable_2                 = Cannot parse \u201c{1}\u201d as a file in 
the {0} format.
-FallbackAuthorityNotice           = Definitions from public sources. When a 
definition corresponds to an EPSG object (ignoring metadata), the EPSG code is 
provided as a reference where to find the complete definition.
 IllegalAxisDirection_2            = Coordinate system of class \u2018{0}\u2019 
cannot have axis in the {1} direction.
 IllegalOperationDimension_3       = Dimensions of \u201c{0}\u201d operation 
cannot be ({1} \u2192 {2}).
 IllegalOperationForValueClass_1   = This operation cannot be applied to values 
of class \u2018{0}\u2019.
@@ -89,6 +89,7 @@ IncompatibleDatum_2               = Datum of \u201c{1}\u201d 
shall be \u201c{0}\
 LatitudesAreOpposite_2            = Latitudes {0} and {1} are opposite.
 MismatchedParameterDescriptor_1   = Mismatched descriptor for \u201c{0}\u201d 
parameter.
 MismatchedPrimeMeridian_2         = Expected the \u201c{0}\u201d prime 
meridian but found \u201c{1}\u201d.
+MismatchedSourceTargetCRS_3       = Invalid coordinate operation step {0}, 
because the reference system \u201c{1}\u201d cannot be followed by 
\u201c{2}\u201d.
 MissingAuthority_1                = No authority was specified for code 
\u201c{0}\u201d. The expected syntax is \u201cAUTHORITY:CODE\u201d.
 MissingAuthorityCode_1            = Missing or empty \u201cID[\u2026]\u201d 
element for \u201c{0}\u201d.
 MissingInterpolationOrdinates     = Not enough dimension in 
\u2018MathTransform\u2019 input or output coordinates for the interpolation 
points.
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 92be70805c..98e953dc70 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
@@ -31,6 +31,7 @@ ConformanceMeansDatumShift        = Ce r\u00e9sultat indique 
si un changement de
 ConstantProjParameterValue_1      = Ce param\u00e8tre est montr\u00e9 pour 
\u00eatre plus complet, mais sa valeur ne devrait jamais \u00eatre 
diff\u00e9rente de {0} pour cette projection.
 DeprecatedCode_3                  = Le code \u00ab\u202f{0}\u202f\u00bb est 
d\u00e9pr\u00e9ci\u00e9 et remplac\u00e9 par le code {1}. La raison est\u00a0: 
{2}
 ElementsOmitted_1                 = \u2026 {0} \u00e9l\u00e9ments omis \u2026
+FallbackAuthorityNotice           = D\u00e9finitions de sources publiques. 
Quand une d\u00e9finition correspond \u00e0 un objet EPSG (en ignorant les 
m\u00e9ta-donn\u00e9es), le code EPSG est fournit comme r\u00e9f\u00e9rence 
o\u00f9 trouver une d\u00e9finition compl\u00e8te.
 FallbackDefaultFactoryVersion_2   = Il n\u2019y a pas de registre local pour 
la version {1} de l\u2019autorit\u00e9 \u00ab\u202f{0}\u202f\u00bb. Les objets 
seront cr\u00e9\u00e9s avec la version par d\u00e9faut.
 GeodeticDataBase_4                = Base de donn\u00e9es g\u00e9od\u00e9sique 
{0} version {1} sur \u00ab\u202f{2}\u202f\u00bb version {3}.
 IgnoredServiceProvider_3          = Plusieurs fournisseurs de service de type 
\u2018{0}\u2019 sont d\u00e9clar\u00e9s pour \u00ab\u202f{1}\u202f\u00bb. Seul 
le premier fournisseur (une instance de \u2018{2}\u2019) sera utilis\u00e9.
@@ -81,7 +82,6 @@ DuplicatedSpatialComponents_1     = Un syst\u00e8me de 
r\u00e9f\u00e9rence des c
 EllipsoidalHeightNotAllowed_1     = Un syst\u00e8me de r\u00e9f\u00e9rence des 
coordonn\u00e9es ne devrait pas contenir une hauteur ellipso\u00efdale. 
Utilisez plut\u00f4t un syst\u00e8me 
{0,choice,0#g\u00e9ographique|1#projet\u00e9} \u00e0 trois dimensions.
 FileNotFound_2                    = Ne peut pas trouver le fichier {0} 
nomm\u00e9 \u00ab\u202f{1}\u202f\u00bb.
 FileNotReadable_2                 = Ne peut pas lire 
\u00ab\u202f{1}\u202f\u00bb comme un fichier au format {0}.
-FallbackAuthorityNotice           = D\u00e9finitions de sources publiques. 
Quand une d\u00e9finition correspond \u00e0 un objet EPSG (en ignorant les 
m\u00e9ta-donn\u00e9es), le code EPSG est fournit comme r\u00e9f\u00e9rence 
o\u00f9 trouver une d\u00e9finition compl\u00e8te.
 IllegalAxisDirection_2            = Les syst\u00e8mes de coordonn\u00e9es de 
classe \u2018{0}\u2019 ne peuvent pas avoir d\u2019axe dans la direction 
\u00ab\u202f{1}\u202f\u00bb.
 IllegalOperationDimension_3       = Les dimensions de l\u2019op\u00e9ration 
\u00ab\u202f{0}\u202f\u00bb ne peuvent pas \u00eatre ({1} \u2192 {2}).
 IllegalOperationForValueClass_1   = Cette op\u00e9ration ne peut pas 
s\u2019appliquer aux valeurs de classe \u2018{0}\u2019.
@@ -94,6 +94,7 @@ IncompatibleDatum_2               = Le r\u00e9f\u00e9rentiel 
de \u00ab\u202f{1}\
 LatitudesAreOpposite_2            = Les latitudes {0} et {1} sont 
oppos\u00e9es.
 MismatchedParameterDescriptor_1   = Le descripteur du param\u00e8tre 
\u00ab\u202f{0}\u202f\u00bb ne correspond pas.
 MismatchedPrimeMeridian_2         = Le m\u00e9ridien d\u2019origine 
\u00ab\u202f{0}\u202f\u00bb \u00e9tait attendu, mais 
\u00ab\u202f{1}\u202f\u00bb a \u00e9t\u00e9 trouv\u00e9.
+MismatchedSourceTargetCRS_3       = L\u2019\u00e9tape {0} de la transformation 
de coordonn\u00e9es est invalide, parce que le syst\u00e8me de 
r\u00e9f\u00e9rence \u00ab\u202f{1}\u202f\u00bb ne peut pas \u00eatre suivit de 
\u00ab\u202f{2}\u202f\u00bb.
 MissingAuthority_1                = Aucune autorit\u00e9 n\u2019a 
\u00e9t\u00e9 sp\u00e9cifi\u00e9e pour le code \u00ab\u202f{0}\u202f\u00bb. Le 
format attendu est \u00ab\u202fAUTORIT\u00c9:CODE\u202f\u00bb.
 MissingAuthorityCode_1            = L\u2019\u00e9l\u00e9ment 
\u00ab\u202fID[\u2026]\u202f\u00bb est manquant ou vide pour 
\u00ab\u202f{0}\u202f\u00bb.
 MissingInterpolationOrdinates     = La dimension des coordonn\u00e9es en 
entr\u00e9 ou en sortie du \u2018MathTransform\u2019 n\u2019est pas suffisante 
pour contenir les points d\u2019interpolation.
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 7f83c44b15..63c872587f 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
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.referencing.operation;
 
+import java.util.Locale;
 import java.util.Objects;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.cs.EllipsoidalCS;
@@ -77,11 +78,15 @@ final class CRSPair {
     }
 
     /**
-     * Returns the name of the GeoAPI interface implemented by the specified 
object. In the GeographicCRS
-     * or EllipsoidalCS cases, the trailing CRS or CS suffix is replaced by 
the number of dimensions
-     * (e.g. "Geographic3D").
+     * Returns the name of the GeoAPI interface implemented by the specified 
object, followed by the object name.
+     * In the GeographicCRS or EllipsoidalCS cases, the trailing CRS or CS 
suffix is replaced by the number of
+     * dimensions (e.g. "Geographic3D").
+     *
+     * @param  object  the object for which to get a label.
+     * @param  locale  the locale for the object name, or {@code null}.
+     * @return a label for the specified object.
      */
-    static String label(final IdentifiedObject object) {
+    static String label(final IdentifiedObject object, final Locale locale) {
         if (object == null) {
             return null;
         }
@@ -98,7 +103,7 @@ final class CRSPair {
                 label = sb.append(((CoordinateSystem) 
cs).getDimension()).append('D').toString();
             }
         }
-        String name = IdentifiedObjects.getDisplayName(object, null);
+        String name = IdentifiedObjects.getDisplayName(object, locale);
         if (name != null) {
             int i = 30;                                         // Arbitrary 
length threshold.
             if (name.length() >= i) {
@@ -119,6 +124,6 @@ final class CRSPair {
      */
     @Override
     public String toString() {
-        return label(sourceCRS) + " ⟶ " + label(targetCRS);
+        return label(sourceCRS, null) + " ⟶ " + label(targetCRS, null);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index 406ccd4db0..53d1602d2b 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
@@ -114,7 +114,7 @@ import static 
org.apache.sis.util.Utilities.equalsIgnoreMetadata;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see 
DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, 
CoordinateReferenceSystem, CoordinateOperationContext)
  *
@@ -238,7 +238,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
                             
CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(),
                                                                
targetCRS.getCoordinateSystem())));
         } catch (IllegalArgumentException | IncommensurableException e) {
-            throw new 
FactoryException(Resources.format(Resources.Keys.CanNotInstantiateGeodeticObject_1,
 new CRSPair(sourceCRS, targetCRS)), e);
+            final CRSPair key = new CRSPair(sourceCRS, targetCRS);
+            throw new 
FactoryException(resources().getString(Resources.Keys.CanNotInstantiateGeodeticObject_1,
 key), e);
         }
         /*
          * If this method is invoked recursively, verify if the requested 
operation is already in the cache.
@@ -253,7 +254,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
             if (op != null) return asList(op);      // Must be a modifiable 
list as per this method contract.
         }
         if (previousSearches.put(key, Boolean.TRUE) != null) {
-            throw new 
FactoryException(Resources.format(Resources.Keys.RecursiveCreateCallForCode_2, 
CoordinateOperation.class, key));
+            throw new 
FactoryException(resources().getString(Resources.Keys.RecursiveCreateCallForCode_2,
 CoordinateOperation.class, key));
         }
         /*
          * If the user did not specified an area of interest, use the domain 
of validity of the CRS.
@@ -1237,8 +1238,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
 
         final Map<String,Object> properties = new HashMap<>(4);
         properties.put(IdentifiedObject.NAME_KEY, newID);
-        properties.put(IdentifiedObject.REMARKS_KEY, 
Vocabulary.formatInternational(
-                            Vocabulary.Keys.DerivedFrom_1, 
CRSPair.label(object)));
+        properties.put(IdentifiedObject.REMARKS_KEY, 
Vocabulary.formatInternational(Vocabulary.Keys.DerivedFrom_1, label(object)));
         return properties;
     }
 
@@ -1268,8 +1268,8 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
      * @param  target  the target CRS.
      * @return a default error message.
      */
-    private static String notFoundMessage(final IdentifiedObject source, final 
IdentifiedObject target) {
-        return Resources.format(Resources.Keys.CoordinateOperationNotFound_2, 
CRSPair.label(source), CRSPair.label(target));
+    private String notFoundMessage(final IdentifiedObject source, final 
IdentifiedObject target) {
+        return 
resources().getString(Resources.Keys.CoordinateOperationNotFound_2, 
label(source), label(target));
     }
 
     /**
@@ -1279,7 +1279,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
      * @param  crs  the CRS having a conversion that cannot be inverted.
      * @return a default error message.
      */
-    private static String canNotInvert(final GeneralDerivedCRS crs) {
-        return Resources.format(Resources.Keys.NonInvertibleOperation_1, 
crs.getConversionFromBase().getName().getCode());
+    private String canNotInvert(final GeneralDerivedCRS crs) {
+        return resources().getString(Resources.Keys.NonInvertibleOperation_1, 
label(crs.getConversionFromBase()));
     }
 }
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 c0ffadcae9..568569454f 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
@@ -24,7 +24,9 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.IdentityHashMap;
+import java.util.Arrays;
 import java.util.Objects;
+import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.util.function.Predicate;
@@ -58,8 +60,6 @@ import 
org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
 import org.apache.sis.referencing.factory.MissingFactoryResourceException;
 import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException;
-import org.apache.sis.metadata.iso.extent.Extents;
-import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.referencing.util.CoordinateOperations;
 import org.apache.sis.referencing.util.EllipsoidalHeightCombiner;
 import org.apache.sis.referencing.util.PositionalAccuracyConstant;
@@ -68,7 +68,10 @@ 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;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.Utilities;
@@ -236,6 +239,13 @@ class CoordinateOperationRegistry {
      */
     private final Map<CoordinateReferenceSystem, List<String>> authorityCodes;
 
+    /**
+     * The locale for error messages.
+     *
+     * @todo Add a setter method, or make configurable in some way.
+     */
+    private Locale locale;
+
     /**
      * Creates a new instance for the given factory and context.
      *
@@ -506,8 +516,9 @@ class CoordinateOperationRegistry {
                         return operations;
                     }
                 }
-            } catch (IllegalArgumentException | IncommensurableException e) {
-                String message = 
Resources.format(Resources.Keys.CanNotInstantiateGeodeticObject_1, new 
CRSPair(sourceCRS, targetCRS));
+            } catch (IllegalArgumentException | IncommensurableException | 
NoninvertibleTransformException e) {
+                CRSPair key = new CRSPair(sourceCRS, targetCRS);
+                String message = 
resources().getString(Resources.Keys.CanNotInstantiateGeodeticObject_1, key);
                 String details = e.getLocalizedMessage();
                 if (details != null) {
                     message = message + ' ' + details;
@@ -530,11 +541,12 @@ class CoordinateOperationRegistry {
      *         or {@code null} if no such operation is explicitly defined in 
the underlying database.
      * @throws IllegalArgumentException if the coordinate systems are not of 
the same type or axes do not match.
      * @throws IncommensurableException if the units are not compatible or a 
unit conversion is non-linear.
+     * @throws NoninvertibleTransformException if a step needs to be inverted 
but is not invertible.
      * @throws FactoryException if an error occurred while creating the 
operation.
      */
     private List<CoordinateOperation> search(final CoordinateReferenceSystem 
sourceCRS,
                                              final CoordinateReferenceSystem 
targetCRS)
-            throws IllegalArgumentException, IncommensurableException, 
FactoryException
+            throws IncommensurableException, NoninvertibleTransformException, 
FactoryException
     {
         final List<String> sources = findCode(sourceCRS); if 
(sources.isEmpty()) return null;
         final List<String> targets = findCode(targetCRS); if 
(targets.isEmpty()) return null;
@@ -645,14 +657,14 @@ class CoordinateOperationRegistry {
                     operation = fromDefiningConversion((SingleOperation) 
operation,
                                     foundDirectOperations ? sourceCRS : 
targetCRS,
                                     foundDirectOperations ? targetCRS : 
sourceCRS);
-                    if (operation == null) {
-                        it.remove();
-                        continue;
-                    }
                 }
                 if (!foundDirectOperations) {
                     operation = inverse(operation);
                 }
+                if (operation == null) {
+                    it.remove();
+                    continue;
+                }
             } catch (NoninvertibleTransformException | 
MissingFactoryResourceException e) {
                 /*
                  * If we failed to get the real CoordinateOperation instance, 
remove it from
@@ -691,15 +703,13 @@ class CoordinateOperationRegistry {
 
     /**
      * Creates the inverse of the given single operation.
-     * If this operation succeed, then the returned coordinate operations has 
the following properties:
+     * If this operation succeed, then the returned coordinate operation has 
the following properties:
      *
      * <ul>
      *   <li>Its {@code sourceCRS} is the {@code targetCRS} of the given 
operation.</li>
      *   <li>Its {@code targetCRS} is the {@code sourceCRS} of the given 
operation.</li>
      *   <li>Its {@code interpolationCRS} is {@code null}.</li>
-     *   <li>Its {@code MathTransform} is the
-     *       {@linkplain 
org.apache.sis.referencing.operation.transform.AbstractMathTransform#inverse() 
inverse}
-     *       of the {@code MathTransform} of this operation.</li>
+     *   <li>Its {@code MathTransform} is the inverse of the {@code 
MathTransform} of the given operation.</li>
      *   <li>Its domain of validity and accuracy is the same.</li>
      * </ul>
      *
@@ -749,20 +759,53 @@ class CoordinateOperationRegistry {
             return inverse((SingleOperation) operation);
         }
         if (operation instanceof ConcatenatedOperation) {
-            final List<? extends CoordinateOperation> operations = 
((ConcatenatedOperation) operation).getOperations();
-            final CoordinateOperation[] inverted = new 
CoordinateOperation[operations.size()];
-            for (int i=0; i<inverted.length;) {
-                final CoordinateOperation op = inverse(operations.get(i));
-                if (op == null) {
-                    return null;
-                }
-                inverted[inverted.length - ++i] = op;
+            final CoordinateOperation[] inverted = 
getSteps((ConcatenatedOperation) operation, true);
+            ArraysExt.reverse(inverted);
+            final Map<String,Object> properties = 
properties(INVERSE_OPERATION);
+            final MathTransform transform = operation.getMathTransform();
+            if (transform != null) {
+                properties.put(DefaultConcatenatedOperation.TRANSFORM_KEY, 
transform.inverse());
             }
-            return 
factory.createConcatenatedOperation(properties(INVERSE_OPERATION), inverted);
+            return factory.createConcatenatedOperation(properties, inverted);
         }
         return null;
     }
 
+    /**
+     * Returns all steps of the given concatenated operation, making sure that 
they are in the specified direction.
+     * This method returns an array where the source CRS of the first step is 
the {@code operation} source CRS, and
+     * where the target CRS of each step is the source CRS of the next step. 
If needed, some steps may be inverted
+     * in order to fulfill that requirement.
+     *
+     * <p>If {@code inverse} if {@code true}, then this method inverses all 
steps. Note however that the element
+     * order in the returned array is not inverted (this is left to the 
caller).</p>
+     *
+     * @param  operation  the operation for which to get the steps.
+     * @return all steps of the given concatenated operation.
+     * @throws NoninvertibleTransformException if a step needs to be inverted 
but is not invertible.
+     * @throws FactoryException if the operation creation failed for another 
reason (e.g., inconsistency found).
+     */
+    private CoordinateOperation[] getSteps(final ConcatenatedOperation 
operation, final boolean inverse)
+            throws NoninvertibleTransformException, FactoryException
+    {
+        final var steps = 
operation.getOperations().toArray(CoordinateOperation[]::new);
+        CoordinateReferenceSystem previous = operation.getSourceCRS();
+        for (int i=0; i<steps.length; i++) {
+            final CoordinateOperation step = steps[i];
+            final CoordinateReferenceSystem source = step.getSourceCRS();
+            final CoordinateReferenceSystem target = step.getTargetCRS();
+            final boolean r = 
DefaultConcatenatedOperation.verifyStepChaining(null, i, previous, source, 
target);
+            if (r != inverse) {
+                if ((steps[i] = inverse(step)) == null) {
+                    throw new 
NoninvertibleTransformException(resources().getString(
+                                Resources.Keys.NonInvertibleOperation_1, 
label(step)));
+                }
+            }
+            previous = r ? source : target;
+        }
+        return steps;
+    }
+
     /**
      * Completes (if necessary) the given coordinate operation for making sure 
that the source CRS
      * is the given one and the target CRS is the given one.  In principle, 
the given CRS shall be
@@ -775,12 +818,13 @@ class CoordinateOperationRegistry {
      * @return a coordinate operation for the given source and target CRS.
      * @throws IllegalArgumentException if the coordinate systems are not of 
the same type or axes do not match.
      * @throws IncommensurableException if the units are not compatible or a 
unit conversion is non-linear.
+     * @throws NoninvertibleTransformException if a step needs to be inverted 
but is not invertible.
      * @throws FactoryException if the operation cannot be constructed.
      */
     private CoordinateOperation complete(final CoordinateOperation       
operation,
                                          final CoordinateReferenceSystem 
sourceCRS,
                                          final CoordinateReferenceSystem 
targetCRS)
-            throws IllegalArgumentException, IncommensurableException, 
FactoryException
+            throws IncommensurableException, NoninvertibleTransformException, 
FactoryException
     {
         CoordinateReferenceSystem source = operation.getSourceCRS();
         CoordinateReferenceSystem target = operation.getTargetCRS();
@@ -807,7 +851,7 @@ class CoordinateOperationRegistry {
     private static MathTransform swapAndScaleAxes(final 
CoordinateReferenceSystem sourceCRS,
                                                   final 
CoordinateReferenceSystem targetCRS,
                                                   final MathTransformFactory   
   mtFactory)
-            throws IllegalArgumentException, IncommensurableException, 
FactoryException
+            throws IncommensurableException, FactoryException
     {
         /*
          * Assertion: source and target CRS must be equal, ignoring change in 
axis order or units.
@@ -837,6 +881,7 @@ class CoordinateOperationRegistry {
      * @param  mtFactory  the math transform factory to use.
      * @return a new operation, or {@code operation} if {@code prepend} and 
{@code append} were nulls or identity transforms.
      * @throws IllegalArgumentException if the operation method cannot have 
the desired number of dimensions.
+     * @throws NoninvertibleTransformException if a step needs to be inverted 
but is not invertible.
      * @throws FactoryException if the operation cannot be constructed.
      */
     private CoordinateOperation transform(final CoordinateReferenceSystem 
sourceCRS,
@@ -845,7 +890,7 @@ class CoordinateOperationRegistry {
                                           final MathTransform             
append,
                                           final CoordinateReferenceSystem 
targetCRS,
                                           final MathTransformFactory      
mtFactory)
-            throws IllegalArgumentException, FactoryException
+            throws NoninvertibleTransformException, FactoryException
     {
         if ((prepend == null || prepend.isIdentity()) && (append == null || 
append.isIdentity())) {
             return operation;
@@ -857,18 +902,17 @@ class CoordinateOperationRegistry {
          * with that). Instead, prepend to the first single operation and 
append to the last single operation.
          */
         if (operation instanceof ConcatenatedOperation) {
-            final List<? extends CoordinateOperation> c = 
((ConcatenatedOperation) operation).getOperations();
-            final CoordinateOperation[] op = 
c.toArray(CoordinateOperation[]::new);
-            switch (op.length) {
+            final CoordinateOperation[] steps = 
getSteps((ConcatenatedOperation) operation, false);
+            switch (steps.length) {
                 case 0: break;                              // Illegal, but we 
are paranoiac.
-                case 1: operation = op[0]; break;           // Useless 
ConcatenatedOperation.
+                case 1: operation = steps[0]; break;        // Useless 
ConcatenatedOperation.
                 default: {
-                    final int n = op.length - 1;
-                    final CoordinateOperation first = op[0];
-                    final CoordinateOperation last  = op[n];
-                    op[0] = transform(sourceCRS, prepend, first, null, 
first.getTargetCRS(), mtFactory);
-                    op[n] = transform(last.getSourceCRS(), null, last, append, 
targetCRS,    mtFactory);
-                    return 
factory.createConcatenatedOperation(derivedFrom(operation), op);
+                    final int n = steps.length - 1;
+                    final CoordinateOperation first = steps[0];
+                    final CoordinateOperation last  = steps[n];
+                    steps[0] = transform(sourceCRS, prepend, first, null, 
first.getTargetCRS(), mtFactory);
+                    steps[n] = transform(last.getSourceCRS(), null, last, 
append, targetCRS,    mtFactory);
+                    return 
factory.createConcatenatedOperation(derivedFrom(operation), steps);
                 }
             }
         }
@@ -901,7 +945,7 @@ class CoordinateOperationRegistry {
                                                CoordinateReferenceSystem 
targetCRS,
                                          final MathTransform             
transform,
                                                OperationMethod           
method)
-            throws IllegalArgumentException, FactoryException
+            throws FactoryException
     {
         /*
          * If the user-provided CRS are approximately equal to the coordinate 
operation CRS, keep the latter.
@@ -970,7 +1014,7 @@ class CoordinateOperationRegistry {
             }
         } else {
             // Should never happen because parameters are mandatory, but let 
be safe.
-            log(Resources.forLocale(null).getLogRecord(Level.WARNING, 
Resources.Keys.MissingParameterValues_1,
+            log(resources().getLogRecord(Level.WARNING, 
Resources.Keys.MissingParameterValues_1,
                     IdentifiedObjects.getIdentifierOrName(operation)), null);
         }
         return null;
@@ -999,17 +1043,18 @@ class CoordinateOperationRegistry {
      * @return a coordinate operation with the source and/or target 
coordinates made 3D,
      *         or {@code null} if this method does not know how to create the 
operation.
      * @throws IllegalArgumentException if the operation method cannot have 
the desired number of dimensions.
+     * @throws NoninvertibleTransformException if a step needs to be inverted 
but is not invertible.
      * @throws FactoryException if an error occurred while creating the 
coordinate operation.
      */
     private CoordinateOperation propagateVertical(final 
CoordinateReferenceSystem sourceCRS,
                                                   final 
CoordinateReferenceSystem targetCRS,
                                                   final CoordinateOperation 
operation,
                                                   final Decomposition 
decompose)
-            throws IllegalArgumentException, FactoryException
+            throws NoninvertibleTransformException, FactoryException
     {
         final List<CoordinateOperation> operations = new ArrayList<>();
         if (operation instanceof ConcatenatedOperation) {
-            operations.addAll(((ConcatenatedOperation) 
operation).getOperations());
+            operations.addAll(Arrays.asList(getSteps((ConcatenatedOperation) 
operation, false)));
         } else {
             operations.add(operation);
         }
@@ -1022,7 +1067,7 @@ class CoordinateOperationRegistry {
             case 0:  return null;
             case 1:  return operations.get(0);
             default: return 
factory.createConcatenatedOperation(derivedFrom(operation),
-                            operations.toArray(CoordinateOperation[]::new));
+                                        
operations.toArray(CoordinateOperation[]::new));
         }
     }
 
@@ -1042,14 +1087,14 @@ class CoordinateOperationRegistry {
                                       final CoordinateReferenceSystem target3D,
                                       final ListIterator<CoordinateOperation> 
operations,
                                       final boolean forward)
-            throws IllegalArgumentException, FactoryException
+            throws FactoryException
     {
         while (forward ? operations.hasNext() : operations.hasPrevious()) {
             final CoordinateOperation op = forward ? operations.next() : 
operations.previous();
             /*
              * We will accept to increase the number of dimensions only for 
operations between geographic CRS.
-             * We do not increase the number of dimensions for operations 
between other kind of CRS because we
-             * would not know which value to give to the new dimension.
+             * We do not increase the number of dimensions for operations 
between other kinds of CRS because
+             * we would not know which value to give to the new dimension.
              */
             CoordinateReferenceSystem sourceCRS, targetCRS;
             if ( !((sourceCRS = op.getSourceCRS()) instanceof GeodeticCRS
@@ -1175,7 +1220,7 @@ class CoordinateOperationRegistry {
      *
      * <h4>Note</h4>
      * In the datum shift case, an operation version is mandatory but unknown 
at this time.
-     * However, we noticed that the EPSG database do not always defines a 
version neither.
+     * 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
      * version and we do not try to provide this information here for now.
      *
@@ -1296,6 +1341,22 @@ class CoordinateOperationRegistry {
         return factorySIS.createSingleOperation(properties, sourceCRS, 
targetCRS, null, method, transform);
     }
 
+    /**
+     * {@return the localized resources for error messages}.
+     */
+    final Resources resources() {
+        return Resources.forLocale(locale);
+    }
+
+    /**
+     * {@return a label for identifying the given object in error message}.
+     *
+     * @param object the object of identify.
+     */
+    final String label(final IdentifiedObject object) {
+        return CRSPair.label(object, 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/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
index cad53a37b7..4b434f18f5 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
@@ -20,11 +20,11 @@ import java.util.Map;
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Objects;
+import java.util.Locale;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 import org.opengis.util.FactoryException;
-import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.ConcatenatedOperation;
@@ -32,15 +32,19 @@ import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.Transformation;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.apache.sis.referencing.IdentifiedObjects;
 import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 import org.apache.sis.referencing.util.PositionalAccuracyConstant;
 import org.apache.sis.referencing.internal.Resources;
+import org.apache.sis.util.Utilities;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.internal.UnmodifiableArrayList;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.io.wkt.Formatter;
-import static org.apache.sis.util.Utilities.deepEquals;
 
 
 /**
@@ -60,6 +64,23 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
      */
     private static final long serialVersionUID = 4199619838029045700L;
 
+    /**
+     * Optional key for specifying the {@link #transform} value.
+     * This property should generally not be specified, as the constructor 
builds the transform itself.
+     * It may be useful if the resulting transform is already known and we 
want to avoid the construction cost.
+     */
+    public static final String TRANSFORM_KEY = "transform";
+
+    /**
+     * The comparison modes to use for determining if two CRS are equal, in 
preference order.
+     * This is used for determining if an operation need to be inverted.
+     */
+    private static final ComparisonMode[] CRS_ORDER_CRITERIA = {
+        ComparisonMode.BY_CONTRACT,
+        ComparisonMode.IGNORE_METADATA,
+        ComparisonMode.APPROXIMATE
+    };
+
     /**
      * The sequence of operations.
      *
@@ -84,6 +105,12 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
      *     <th>Value type</th>
      *     <th>Returned by</th>
      *   </tr><tr>
+     *     <td>{@value #TRANSFORM_KEY}</td>
+     *     <td>{@link MathTransform}</td>
+     *     <td>{@link #getMathTransform()}</td>
+     *   </tr><tr>
+     *     <th colspan="3" class="hsep">Defined in parent class (reminder)</th>
+     *   </tr><tr>
      *     <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
      *     <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
      *     <td>{@link #getName()}</td>
@@ -91,13 +118,21 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
      *     <td>{@value 
org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
      *     <td>{@link org.opengis.metadata.Identifier} (optionally as 
array)</td>
      *     <td>{@link #getIdentifiers()}</td>
+     *   </tr><tr>
+     *     <td>{@value 
org.opengis.referencing.operation.CoordinateOperation#COORDINATE_OPERATION_ACCURACY_KEY}</td>
+     *     <td>{@link PositionalAccuracy} (optionally as array)</td>
+     *     <td>{@link #getCoordinateOperationAccuracy()}</td>
      *   </tr>
      * </table>
      *
+     * The {@value #TRANSFORM_KEY} property should generally not be provided, 
as it is automatically computed.
+     * That property is available for saving computation cost when the 
concatenated transform is known in advance,
+     * or for overriding the automatic concatenation.
+     *
      * @param  properties  the properties to be given to the identified object.
      * @param  operations  the sequence of operations. Shall contain at least 
two operations.
      * @param  mtFactory   the math transform factory to use for math 
transforms concatenation.
-     * @throws FactoryException if the factory cannot concatenate the math 
transforms.
+     * @throws FactoryException if this constructor or the factory cannot 
concatenate the operation steps.
      */
     public DefaultConcatenatedOperation(final Map<String,?> properties, final 
CoordinateOperation[] operations,
             final MathTransformFactory mtFactory) throws FactoryException
@@ -105,10 +140,11 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
         super(properties);
         ArgumentChecks.ensureNonNull("operations", operations);
         if (operations.length < 2) {
-            throw new 
IllegalArgumentException(Errors.getResources(properties).getString(
+            throw new 
InvalidGeodeticParameterException(Errors.getResources(properties).getString(
                     Errors.Keys.TooFewOccurrences_2, 2, 
CoordinateOperation.class));
         }
-        initialize(properties, operations, mtFactory);
+        transform = Containers.property(properties, TRANSFORM_KEY, 
MathTransform.class);
+        initialize(properties, operations, (transform == null) ? mtFactory : 
null);
         checkDimensions(properties);
     }
 
@@ -120,7 +156,7 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
      * @param  properties   the properties specified at construction time, or 
{@code null} if unknown.
      * @param  operations   the operations to concatenate.
      * @param  mtFactory    the math transform factory to use, or {@code null} 
for not performing concatenation.
-     * @throws FactoryException if the factory cannot concatenate the math 
transforms.
+     * @throws FactoryException if this constructor or the factory cannot 
concatenate the operation steps.
      */
     private void initialize(final Map<String,?>         properties,
                             final CoordinateOperation[] operations,
@@ -129,7 +165,7 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
     {
         final List<CoordinateOperation> flattened = new 
ArrayList<>(operations.length);
         final CoordinateReferenceSystem crs = initialize(properties, 
operations, flattened, mtFactory,
-                (sourceCRS == null), (coordinateOperationAccuracy == null));
+                sourceCRS, (sourceCRS == null), (coordinateOperationAccuracy 
== null));
         if (targetCRS == null) {
             targetCRS = crs;
         }
@@ -175,6 +211,7 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
      * @param  operations   the operations to concatenate.
      * @param  flattened    the destination list in which to add the {@code 
SingleOperation} instances.
      * @param  mtFactory    the math transform factory to use, or {@code null} 
for not performing concatenation.
+     * @param  previous     target CRS of the step before the first {@code 
operations} step, or {@code null}.
      * @param  setSource    {@code true} for setting the {@link #sourceCRS} on 
the very first CRS (regardless if null or not).
      * @param  setAccuracy  {@code true} for setting the {@link 
#coordinateOperationAccuracy} field.
      * @return the last target CRS, regardless if null or not.
@@ -185,51 +222,60 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
             final CoordinateOperation[]     operations,
             final List<CoordinateOperation> flattened,
             final MathTransformFactory      mtFactory,
+            CoordinateReferenceSystem       previous,
             boolean setSource,
             boolean setAccuracy) throws FactoryException
     {
-        CoordinateReferenceSystem previous = null;
+        CoordinateReferenceSystem source;                   // Source CRS of 
current iteration.
+        CoordinateReferenceSystem target = null;            // Target CRS of 
current and last iteration.
         for (int i=0; i<operations.length; i++) {
             final CoordinateOperation op = operations[i];
             ArgumentChecks.ensureNonNullElement("operations", i, op);
             /*
-             * Verify consistency of user argument: for each coordinate 
operation, the number of dimensions of the
-             * source CRS shall be equal to the number of dimensions of the 
target CRS in the previous operation.
+             * Verify consistency of user argument: for each coordinate 
operation, the source CRS
+             * should be equal (ignoring metadata) to the target CRS of the 
previous operation.
+             * An exception to this rule is when source and target CRS need to 
be swapped.
              */
-            final CoordinateReferenceSystem next = op.getSourceCRS();
-            if (previous != null && next != null) {
-                final int dim1 = previous.getCoordinateSystem().getDimension();
-                final int dim2 = next.getCoordinateSystem().getDimension();
-                if (dim1 != dim2) {
-                    throw new 
MismatchedDimensionException(Errors.getResources(properties).getString(
-                            Errors.Keys.MismatchedDimension_3, "operations[" + 
i + "].sourceCRS", dim1, dim2));
-                }
+            source = op.getSourceCRS();
+            target = op.getTargetCRS();
+            final boolean inverse = verifyStepChaining(properties, i, 
previous, source, target);
+            if (inverse) {
+                var t  = source;
+                source = target;
+                target = t;
             }
             if (setSource) {
                 setSource = false;
-                sourceCRS = next;                                              
 // Take even if null.
+                sourceCRS = source;                                            
 // Take even if null.
             }
-            previous = op.getTargetCRS();                                      
 // For next iteration cycle.
             /*
-             * Now that we have verified the CRS dimensions, we should be able 
to concatenate the transforms.
-             * If an operation is a nested ConcatenatedOperation (not allowed 
by ISO 19111, but we try to be
-             * safe), we will first try to use the 
ConcatenatedOperation.transform as a whole.  Only if that
-             * concatenated operation does not provide a transform we will 
concatenate its components.  Note
-             * however that we traverse nested concatenated operations 
unconditionally at least for checking
-             * its consistency.
+             * Now that we have verified the CRS chaining, we should be able 
to concatenate the transforms.
+             * If an operation is a nested `ConcatenatedOperation` (not 
allowed by ISO 19111, but we try to
+             * be safe), we will first try to use the 
`ConcatenatedOperation.transform` as a whole. Only if
+             * that concatenated operation does not provide a transform, we 
will concatenate its components.
+             * Note however that we traverse nested concatenated operations 
unconditionally at least for
+             * checking its consistency.
              */
-            final MathTransform step = op.getMathTransform();
+            NoninvertibleTransformException cause = null;
+            MathTransform step = op.getMathTransform();
+            if (step != null && inverse) try {
+                step = step.inverse();
+            } catch (NoninvertibleTransformException e) {
+                step = null;
+                cause = e;
+            }
             if (step == null) {
                 // May happen if the operation is a defining operation.
-                throw new IllegalArgumentException(Resources.format(
-                        Resources.Keys.OperationHasNoTransform_2, 
op.getClass(), op.getName()));
+                throw new InvalidGeodeticParameterException(Resources.format(
+                        Resources.Keys.OperationHasNoTransform_2, 
op.getClass(), op.getName()), cause);
             }
             if (op instanceof ConcatenatedOperation) {
-                final List<? extends CoordinateOperation> children = 
((ConcatenatedOperation) op).getOperations();
-                final CoordinateOperation[] asArray = 
children.toArray(CoordinateOperation[]::new);
-                initialize(properties, asArray, flattened, (step == null) ? 
mtFactory : null, false, setAccuracy);
+                final var nested = ((ConcatenatedOperation) 
op).getOperations().toArray(CoordinateOperation[]::new);
+                previous = initialize(properties, nested, flattened, null, 
previous, false, setAccuracy);
             } else if (!step.isIdentity()) {
+                // Note: operation (source, target) may be in reverse order, 
but it should be taken as metadata.
                 flattened.add(op);
+                previous = target;          // For next iteration cycle.
             }
             if (mtFactory != null) {
                 transform = (transform != null) ? 
mtFactory.createConcatenatedTransform(transform, step) : step;
@@ -253,9 +299,42 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
                 }
             }
         }
+        verifyStepChaining(properties, operations.length, target, targetCRS, 
null);
         return previous;
     }
 
+    /**
+     * Verifies if a step of a concatenated operation can be chained after the 
previous step.
+     *
+     * @param  properties  user-specified properties (for the locale of error 
message), or {@code null} if none.
+     * @param  step        index of the operation step, used only in case an 
exception it thrown.
+     * @param  previous    Target CRS of the previous step.
+     * @param  source      Source CRS of the current step.
+     * @param  target      Target CRS of the current step, or {@code null} if 
none.
+     * @return whether the math transform needs to be inverted.
+     * @throws FactoryException if the current operation cannot be chained 
after the previous operation.
+     */
+    static boolean verifyStepChaining(
+            final Map<String,?> properties, final int step,
+            final CoordinateReferenceSystem previous,
+            final CoordinateReferenceSystem source,
+            final CoordinateReferenceSystem target) throws FactoryException
+    {
+        if (previous == null || source == null) {
+            return false;
+        }
+        for (final ComparisonMode mode : CRS_ORDER_CRITERIA) {
+            if (Utilities.deepEquals(previous, source, mode)) return false;
+            if (Utilities.deepEquals(previous, target, mode)) return true;
+        }
+        Resources resources = Resources.forProperties(properties);
+        Locale locale = resources.getLocale();
+        throw new InvalidGeodeticParameterException(resources.getString(
+                Resources.Keys.MismatchedSourceTargetCRS_3, step,
+                IdentifiedObjects.getDisplayName(previous, locale),
+                IdentifiedObjects.getDisplayName(source, locale)));
+    }
+
     /**
      * Creates a new coordinate operation with the same values than the 
specified one.
      * This copy constructor provides a way to convert an arbitrary 
implementation into a SIS one
@@ -333,7 +412,7 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
             if (mode == ComparisonMode.STRICT) {
                 return Objects.equals(operations, 
((DefaultConcatenatedOperation) object).operations);
             } else {
-                return deepEquals(getOperations(), ((ConcatenatedOperation) 
object).getOperations(), mode);
+                return Utilities.deepEquals(getOperations(), 
((ConcatenatedOperation) object).getOperations(), mode);
             }
         }
         return false;
@@ -398,6 +477,7 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
      */
     @XmlElement(name = "coordOperation", required = true)
     private CoordinateOperation[] getSteps() {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final List<? extends CoordinateOperation> operations = getOperations();
         return (operations != null) ? 
operations.toArray(CoordinateOperation[]::new) : null;
     }
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 112647c2ac..03c8363dc1 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
@@ -662,12 +662,7 @@ next:   for (int i=components.size(); --i >= 0;) {
         if (operations != null && operations.length == 1) {
             return operations[0];
         }
-        final ConcatenatedOperation op;
-        try {
-            op = new DefaultConcatenatedOperation(properties, operations, 
getMathTransformFactory());
-        } catch (IllegalArgumentException exception) {
-            throw new 
InvalidGeodeticParameterException(exception.getLocalizedMessage(), exception);
-        }
+        final var op = new DefaultConcatenatedOperation(properties, 
operations, getMathTransformFactory());
         /*
          * Verifies again the number of single operations.  We may have a 
singleton if some operations
          * were omitted because their associated math transform were identity. 
This happen for example
@@ -678,17 +673,22 @@ next:   for (int i=components.size(); --i >= 0;) {
             return pool.unique(op);
         }
         final CoordinateOperation single = co.get(0);
-        assert op.getMathTransform().equals(single.getMathTransform()) : op;
-        if (!Objects.equals(single.getSourceCRS(), op.getSourceCRS()) ||
-            !Objects.equals(single.getTargetCRS(), op.getTargetCRS()))
+        if (Objects.equals(single.getSourceCRS(), op.getSourceCRS()) &&
+            Objects.equals(single.getTargetCRS(), op.getTargetCRS()))
         {
+            // Verify only if CRS are equal because otherwise, `op` transform 
may be the inverse.
+            assert single.getMathTransform().equals(op.getMathTransform()) : 
op;
+        } else {
             /*
              * The CRS of the single operation may be different than the CRS 
of the concatenated operation
-             * if the first or the last operation was an identity operation. 
It happens for example if the
-             * sole purpose of an operation step was to change the longitude 
range from [-180 … +180]° to
-             * [0 … 360]°: the MathTransform is identity (because Apache SIS 
does not handle those changes
-             * in MathTransform; we handle that elsewhere, for example in the 
Envelopes utility class),
-             * but omitting the transform should not cause the lost of the CRS 
with desired longitude range.
+             * for two reasons: optimization when the first or the last 
operation was an identity operation,
+             * or when the operation to apply is the inverse of the single 
operation (swapped source/target).
+             *
+             * The first case (optimization) happens, for example, if the sole 
purpose of an operation step was
+             * to change the longitude range from [-180 … +180]° to [0 … 
360]°. In such case, the `MathTransform`
+             * is identity (because Apache SIS does not handle those changes 
in `MathTransform`; we handle that
+             * elsewhere, for example in the Envelopes utility class), but 
omitting the transform should not
+             * cause the lost of the original CRS with the desired longitude 
range.
              */
             if (single instanceof SingleOperation) {
                 final Map<String,Object> merge = new HashMap<>(
@@ -700,7 +700,7 @@ next:   for (int i=components.size(); --i >= 0;) {
                 merge.putAll(properties);
                 return createSingleOperation(merge, op.getSourceCRS(), 
op.getTargetCRS(),
                         AbstractCoordinateOperation.getInterpolationCRS(op),
-                        ((SingleOperation) single).getMethod(), 
single.getMathTransform());
+                        ((SingleOperation) single).getMethod(), 
op.getMathTransform());
             }
         }
         return single;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java
index 640bf41977..e6262b6b24 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java
@@ -112,11 +112,11 @@ final class InverseOperationMethod extends 
DefaultOperationMethod {
 
     /**
      * Infers the properties to give to an inverse coordinate operation.
-     * The returned map will contain three kind of information:
+     * The returned map will contain the following kinds of information:
      *
      * <ul>
-     *   <li>Metadata (domain of validity, accuracy)</li>
-     *   <li>Parameter values, if possible</li>
+     *   <li>Metadata (domain of validity, accuracy).</li>
+     *   <li>Parameter values, if possible.</li>
      * </ul>
      *
      * If the inverse of the given operation can be represented by inverting 
the sign of all numerical
@@ -132,7 +132,7 @@ final class InverseOperationMethod extends 
DefaultOperationMethod {
      * see ISO 19111 for more information).
      *
      * @param source  the operation for which to get the inverse parameters.
-     * @param target  where to store the inverse parameters.
+     * @param target  where to store the properties of the inverse operation.
      */
     static void properties(final SingleOperation source, final 
Map<String,Object> target) {
         target.put(SingleOperation.DOMAIN_OF_VALIDITY_KEY, 
source.getDomainOfValidity());
diff --git a/endorsed/src/org.apache.sis.util/main/module-info.java 
b/endorsed/src/org.apache.sis.util/main/module-info.java
index 33c1e9dfec..3c846ed3ab 100644
--- a/endorsed/src/org.apache.sis.util/main/module-info.java
+++ b/endorsed/src/org.apache.sis.util/main/module-info.java
@@ -164,6 +164,7 @@ module org.apache.sis.util {
             org.apache.sis.referencing.database;        // In the "non-free" 
sub-project.
 
     exports org.apache.sis.pending.jdk to
+            org.apache.sis.referencing,
             org.apache.sis.referencing.gazetteer,
             org.apache.sis.feature,
             org.apache.sis.storage,
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
index 33580297b4..d1853ad2a5 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.NoSuchElementException;
 
 
 /**
@@ -35,6 +36,36 @@ public final class JDK21 {
     private JDK21() {
     }
 
+    /**
+     * Placeholder for {@code SequencedCollection.getFirst()}.
+     *
+     * @param  <E>        type of elements in the collection.
+     * @param  sequenced  the sequenced collection for which to get elements 
in reverse order.
+     * @return elements of the given collection in reverse order.
+     */
+    public static <E> E getFirst(final List<E> sequenced) {
+        try {
+            return sequenced.get(0);
+        } catch (IndexOutOfBoundsException e) {
+            throw new NoSuchElementException();
+        }
+    }
+
+    /**
+     * Placeholder for {@code SequencedCollection.getLast()}.
+     *
+     * @param  <E>        type of elements in the collection.
+     * @param  sequenced  the sequenced collection for which to get elements 
in reverse order.
+     * @return elements of the given collection in reverse order.
+     */
+    public static <E> E getLast(final List<E> sequenced) {
+        try {
+            return sequenced.get(sequenced.size() - 1);
+        } catch (IndexOutOfBoundsException e) {
+            throw new NoSuchElementException();
+        }
+    }
+
     /**
      * Placeholder for {@code SequencedCollection.reversed()}.
      *

Reply via email to