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

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

commit 85dd6fbbf2affca8d4473f160195171f589e004b
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Sep 10 11:37:03 2025 +0200

    Fix wrong formatting of concatenated operation, and add parsing support.
---
 .../apache/sis/io/wkt/GeodeticObjectParser.java    |  90 ++++++++++------
 .../operation/AbstractCoordinateOperation.java     |  16 +--
 .../operation/DefaultConcatenatedOperation.java    |  32 ++++--
 .../DefaultCoordinateOperationFactory.java         |  40 ++++++-
 .../DefaultConcatenatedOperationTest.java          | 119 +++++++++++----------
 5 files changed, 185 insertions(+), 112 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
index 93742a1ddf..a712ecbbfa 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -318,19 +318,19 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         Object object;
         if    (null == (object = parseCoordinateReferenceSystem(element, 
false))
             && null == (object = parseCoordinateMetadata(FIRST, element))
-            && null == (object = parseMathTransform            (element, 
false))
-            && null == (object = parseAxis              (FIRST, element, null, 
 Units.METRE ))
-            && null == (object = parsePrimeMeridian     (FIRST, element, 
false, Units.DEGREE))
-            && null == (object = parseEnsemble          (FIRST, element, 
Datum.class, greenwich()))
+            && null == (object = parseOperation         (FIRST, element))
+            && null == (object = parseMathTransform     (       element, 
false))
+            && null == (object = parseEnsemble          (FIRST, element, 
greenwich(), Datum.class))
             && null == (object = parseDatum             (FIRST, element, 
greenwich(), null))
-            && null == (object = parseEllipsoid         (FIRST, element))
-            && null == (object = parseToWGS84           (FIRST, element))
             && null == (object = parseVerticalDatum     (FIRST, element, null, 
false))
             && null == (object = parseTimeDatum         (FIRST, element))
             && null == (object = parseParametricDatum   (FIRST, element))
             && null == (object = parseEngineeringDatum  (FIRST, element, 
false))
             && null == (object = parseImageDatum        (FIRST, element))
-            && null == (object = parseOperation         (FIRST, element))
+            && null == (object = parseEllipsoid         (FIRST, element))
+            && null == (object = parsePrimeMeridian     (FIRST, element, 
false, Units.DEGREE))
+            && null == (object = parseAxis              (FIRST, element, null, 
 Units.METRE ))
+            && null == (object = parseToWGS84           (FIRST, element))
             && null == (object = parseGeogTranslation   (FIRST, element)))
         {
             throw element.missingOrUnknownComponent(WKTKeywords.GeodeticCRS);
@@ -1485,15 +1485,15 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
      *
      * @param  mode       {@link #FIRST}, {@link #OPTIONAL} or {@link 
#MANDATORY}.
      * @param  parent     the parent element.
-     * @param  datumType  GeoAPI interface of the type of datum to create.
      * @param  meridian   the prime meridian, or {@code null} if the ensemble 
is not geodetic.
+     * @param  datumType  GeoAPI interface of the type of datum to create.
      * @return the {@code "Ensemble"} element as a {@link DatumEnsemble} 
object.
      * @throws ParseException if the {@code "Ensemble"} element cannot be 
parsed.
      *
      * @see 
org.apache.sis.referencing.datum.DefaultDatumEnsemble#formatTo(Formatter)
      */
     private <D extends Datum> DatumEnsemble<D> parseEnsemble(final int mode, 
final Element parent,
-            final Class<D> datumType, final PrimeMeridian meridian) throws 
ParseException
+            final PrimeMeridian meridian, final Class<D> datumType) throws 
ParseException
     {
         final Element ensemble = parent.pullElement(mode, 
WKTKeywords.Ensemble);
         if (ensemble == null) {
@@ -1826,7 +1826,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
             }
         }
         if (baseCRS == null) {      // The most usual case.
-            ensemble = parseEnsemble(OPTIONAL, element, 
EngineeringDatum.class, null);
+            ensemble = parseEnsemble(OPTIONAL, element, null, 
EngineeringDatum.class);
             datum = parseEngineeringDatum(ensemble == null ? MANDATORY : 
OPTIONAL, element, isWKT1);
         }
         final IdentifiedObject datumOrEnsemble = (datum != null) ? datum : 
ensemble;
@@ -2037,7 +2037,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
                 meridian = greenwich();
             }
             final Temporal epoch = parseDynamic(element);
-            final DatumEnsemble<GeodeticDatum> ensemble = 
parseEnsemble(OPTIONAL, element, GeodeticDatum.class, meridian);
+            final DatumEnsemble<GeodeticDatum> ensemble = 
parseEnsemble(OPTIONAL, element, meridian, GeodeticDatum.class);
             final GeodeticDatum datum = parseDatum(ensemble == null ? 
MANDATORY : OPTIONAL, element, meridian, epoch);
             final IdentifiedObject datumOrEnsemble = (datum != null) ? datum : 
ensemble;
             final Map<String,?> properties = parseMetadataAndClose(element, 
name, datumOrEnsemble);
@@ -2112,7 +2112,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         }
         if (baseCRS == null) {      // The most usual case.
             final Temporal epoch = parseDynamic(element);
-            ensemble = parseEnsemble(OPTIONAL, element, VerticalDatum.class, 
null);
+            ensemble = parseEnsemble(OPTIONAL, element, null, 
VerticalDatum.class);
             datum = parseVerticalDatum(ensemble == null ? MANDATORY : 
OPTIONAL, element, epoch, isWKT1);
         }
         final IdentifiedObject datumOrEnsemble = (datum != null) ? datum : 
ensemble;
@@ -2201,7 +2201,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
             }
         }
         if (baseCRS == null) {      // The most usual case.
-            ensemble = parseEnsemble(OPTIONAL, element, TemporalDatum.class, 
null);
+            ensemble = parseEnsemble(OPTIONAL, element, null, 
TemporalDatum.class);
             datum = parseTimeDatum(ensemble == null ? MANDATORY : OPTIONAL, 
element);
         }
         final IdentifiedObject datumOrEnsemble = (datum != null) ? datum : 
ensemble;
@@ -2265,7 +2265,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
             }
         }
         if (baseCRS == null) {      // The most usual case.
-            ensemble = parseEnsemble(OPTIONAL, element, ParametricDatum.class, 
null);
+            ensemble = parseEnsemble(OPTIONAL, element, null, 
ParametricDatum.class);
             datum = parseParametricDatum(ensemble == null ? MANDATORY : 
OPTIONAL, element);
         }
         final IdentifiedObject datumOrEnsemble = (datum != null) ? datum : 
ensemble;
@@ -2492,26 +2492,58 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
     }
 
     /**
-     * Parses a {@code "CoordinateOperation"} element.
+     * Parses a {@code "CoordinateOperation"} or {@code 
"ConcatenatedOperation"} element.
+     * This method accepts nested concatenated operations, even if not valid 
according
+     * <abbr>ISO</abbr> standards. Those nested operations will be flattened.
      *
      * @param  mode    {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
      * @param  parent  the parent element.
-     * @return the {@code "CoordinateOperation"} element as a {@link 
CoordinateOperation} object.
-     * @throws ParseException if the {@code "CoordinateOperation"} element 
cannot be parsed.
+     * @return the {@code "CoordinateOperation"} or {@code 
"ConcatenatedOperation"} element.
+     * @throws ParseException if the element cannot be parsed.
      */
     private CoordinateOperation parseOperation(final int mode, final Element 
parent) throws ParseException {
-        final Element element = parent.pullElement(mode, 
WKTKeywords.CoordinateOperation);
+        final Element element = parent.pullElement(mode, 
WKTKeywords.CoordinateOperation, WKTKeywords.ConcatenatedOperation);
         if (element == null) {
             return null;
         }
-        final String name = element.pullString("name");
-        final String version = pullElementAsString(element, 
WKTKeywords.Version);
-        final CoordinateReferenceSystem sourceCRS        = 
parseCoordinateReferenceSystem(element, MANDATORY, WKTKeywords.SourceCRS);
-        final CoordinateReferenceSystem targetCRS        = 
parseCoordinateReferenceSystem(element, MANDATORY, WKTKeywords.TargetCRS);
-        final CoordinateReferenceSystem interpolationCRS = 
parseCoordinateReferenceSystem(element, OPTIONAL,  
WKTKeywords.InterpolationCRS);
-        final OperationMethod           method           = 
parseMethod(element, WKTKeywords.Method);
-        final double                    accuracy         = 
pullElementAsDouble(element, WKTKeywords.OperationAccuracy, OPTIONAL);
-        final Map<String,Object>        properties       = 
parseParametersAndClose(element, name, method);
+        final boolean concat   = element.getKeywordIndex() != 0;
+        final String  name     = element.pullString("name");
+        final String  version  = pullElementAsString(element, 
WKTKeywords.Version);
+        final double  accuracy = pullElementAsDouble(element, 
WKTKeywords.OperationAccuracy, OPTIONAL);
+        final CoordinateReferenceSystem sourceCRS = 
parseCoordinateReferenceSystem(element, MANDATORY, WKTKeywords.SourceCRS);
+        final CoordinateReferenceSystem targetCRS = 
parseCoordinateReferenceSystem(element, MANDATORY, WKTKeywords.TargetCRS);
+        final DefaultCoordinateOperationFactory df = getOperationFactory();
+        try {
+            if (concat) {
+                final var steps = new ArrayList<CoordinateOperation>();
+                Element step;
+                while ((step = element.pullElement(steps.isEmpty() ? MANDATORY 
: OPTIONAL, WKTKeywords.Step)) != null) {
+                    steps.add(parseOperation(MANDATORY, step));
+                    step.close(ignoredElements);
+                }
+                Map<String,Object> properties = parseMetadataAndClose(element, 
name, null);
+                addOperationMetadata(properties, version, accuracy);
+                return df.createConcatenatedOperation(properties, sourceCRS, 
targetCRS, steps.toArray(CoordinateOperation[]::new));
+            } else {
+                CoordinateReferenceSystem interpolationCRS = 
parseCoordinateReferenceSystem(element, OPTIONAL, WKTKeywords.InterpolationCRS);
+                OperationMethod method = parseMethod(element, 
WKTKeywords.Method);
+                Map<String,Object> properties = 
parseParametersAndClose(element, name, method);
+                addOperationMetadata(properties, version, accuracy);
+                return df.createSingleOperation(properties, sourceCRS, 
targetCRS, interpolationCRS, method, null);
+            }
+        } catch (FactoryException e) {
+            throw element.parseFailed(e);
+        }
+    }
+
+    /**
+     * Stores in the given map some additional metadata that are specific to 
coordinate operations.
+     *
+     * @param properties  where to add the metadata.
+     * @param version     the operation version, or {@code null} if none.
+     * @param accuracy    the operation accuracy, or {@code null} if none.
+     */
+    private static void addOperationMetadata(final Map<String,Object> 
properties, final String version, final double accuracy) {
         if (version != null) {
             properties.put(CoordinateOperation.OPERATION_VERSION_KEY, version);
         }
@@ -2519,12 +2551,6 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
             
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY,
                            
PositionalAccuracyConstant.transformation(accuracy));
         }
-        try {
-            final DefaultCoordinateOperationFactory df = getOperationFactory();
-            return df.createSingleOperation(properties, sourceCRS, targetCRS, 
interpolationCRS, method, null);
-        } catch (FactoryException e) {
-            throw element.parseFailed(e);
-        }
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
index 369e9182aa..d168be5067 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
@@ -975,8 +975,7 @@ check:      for (int isTarget=0; ; isTarget++) {        // 
0 == source check; 1
          * because the WKT 2 specification does not define pass-through 
operations.
          * This choice may change in any future SIS version.
          */
-        final FormattableObject enclosing = formatter.getEnclosingElement(1);
-        final boolean isSubOperation = (enclosing instanceof 
PassThroughOperation);
+        final boolean isSubOperation = (formatter.getEnclosingElement(1) 
instanceof PassThroughOperation);
         boolean isGeogTran = false;
         if (!isSubOperation) {
             isGeogTran = isWKT1 && (sourceCRS instanceof GeographicCRS) && 
(targetCRS instanceof GeographicCRS);
@@ -1042,17 +1041,8 @@ check:      for (int isTarget=0; ; isTarget++) {        
// 0 == source check; 1
                 return WKTKeywords.GeogTran;
             }
         }
-        /*
-         * If the coordinate operation is a step in a chain of operations, 
returns "step".
-         * Otherwise, if formatting a top-level single operation, add the 
interpolation CRS.
-         */
-        if (enclosing instanceof ConcatenatedOperation) {
-            return WKTKeywords.Step;
-        }
-        if (isSubOperation) {
-            if (!(this instanceof ConcatenatedOperation)) {
-                append(formatter, getInterpolationCRS().orElse(null), 
WKTKeywords.InterpolationCRS);
-            }
+        if (!(isSubOperation || this instanceof ConcatenatedOperation)) {
+            append(formatter, getInterpolationCRS().orElse(null), 
WKTKeywords.InterpolationCRS);
             
WKTUtilities.appendElementIfPositive(WKTKeywords.OperationAccuracy, 
getLinearAccuracy(), formatter);
         }
         return WKTKeywords.CoordinateOperation;
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 00aec82220..57917b27c6 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
@@ -38,6 +38,7 @@ import org.apache.sis.referencing.datum.DatumOrEnsemble;
 import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
 import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 import org.apache.sis.referencing.privy.WKTKeywords;
+import org.apache.sis.referencing.privy.WKTUtilities;
 import org.apache.sis.referencing.privy.CoordinateOperations;
 import org.apache.sis.referencing.internal.PositionalAccuracyConstant;
 import org.apache.sis.referencing.internal.Resources;
@@ -49,6 +50,7 @@ import org.apache.sis.util.privy.UnmodifiableArrayList;
 import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.io.wkt.Convention;
+import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.io.wkt.Formatter;
 
 
@@ -59,6 +61,9 @@ import org.apache.sis.io.wkt.Formatter;
  * first step and the target coordinate reference system of the last step are 
the source and target coordinate
  * reference system associated with the concatenated operation.
  *
+ * <p>Above requirements are relaxed when the source and target 
<abbr>CRS</abbr> of a step are swapped.
+ * In such case, this step will actually be implemented by the inverse 
operation.</p>
+ *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  */
 @XmlType(name = "ConcatenatedOperationType")
@@ -136,18 +141,26 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
      * or for overriding the automatic concatenation.
      *
      * @param  properties  the properties to be given to the identified object.
+     * @param  sourceCRS   the source <abbr>CRS</abbr>, or {@code null} for 
the source of the first step.
+     * @param  targetCRS   the target <abbr>CRS</abbr>, or {@code null} for 
the target of the last effective step.
      * @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 this constructor or the factory cannot 
concatenate the operation steps.
      */
-    public DefaultConcatenatedOperation(final Map<String,?> properties, final 
CoordinateOperation[] operations,
-            final MathTransformFactory mtFactory) throws FactoryException
+    public DefaultConcatenatedOperation(final Map<String,?>             
properties,
+                                        final CoordinateReferenceSystem 
sourceCRS,
+                                        final CoordinateReferenceSystem 
targetCRS,
+                                        final CoordinateOperation[]     
operations,
+                                        final MathTransformFactory      
mtFactory)
+            throws FactoryException
     {
         super(properties);
         if (operations.length < 2) {
             throw new 
InvalidGeodeticParameterException(Errors.forProperties(properties).getString(
                     Errors.Keys.TooFewOccurrences_2, 2, 
CoordinateOperation.class));
         }
+        this.sourceCRS = sourceCRS;
+        this.targetCRS = targetCRS;
         transform = Containers.property(properties, TRANSFORM_KEY, 
MathTransform.class);
         initialize(properties, operations, (transform == null) ? mtFactory : 
null);
         checkDimensions(properties);
@@ -155,8 +168,8 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
 
     /**
      * Initializes the {@link #sourceCRS}, {@link #targetCRS} and {@link 
#operations} fields.
-     * If the source or target CRS is already non-null (which may happen on 
JAXB unmarshalling),
-     * leaves that CRS unchanged.
+     * If the source and target <abbr>CRS</abbr> are already non-null, leaves 
them unchanged
+     * but verifies that they are consistent with the first and last steps.
      *
      * @param  properties   the properties specified at construction time, or 
{@code null} if unknown.
      * @param  operations   the operations to concatenate.
@@ -317,7 +330,7 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
                 }
             }
         }
-        if (!(mtFactory instanceof DefaultMathTransformFactory)) {
+        if (mtFactory != null) {
             verifyStepChaining(properties, operations.length, target, 
targetCRS, null);
             // Else verification will be done by the caller.
         }
@@ -461,8 +474,15 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
         super.formatTo(formatter);
         for (final CoordinateOperation component : operations) {
             formatter.newLine();
-            formatter.append(castOrCopy(component));
+            formatter.append(new FormattableObject() {
+                @Override protected String formatTo(Formatter formatter) {
+                    formatter.newLine();
+                    formatter.appendFormattable(component, 
AbstractCoordinateOperation::castOrCopy);
+                    return WKTKeywords.Step;
+                }
+            });
         }
+        WKTUtilities.appendElementIfPositive(WKTKeywords.OperationAccuracy, 
getLinearAccuracy(), formatter);
         if (!formatter.getConvention().supports(Convention.WKT2_2019)) {
             formatter.setInvalidWKT(this, 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 8e24b9dc6e..6c67f525e5 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
@@ -559,6 +559,26 @@ next:   for (SingleCRS component : 
CRS.getSingleComponents(targetCRS)) {
         return pool.unique(op);
     }
 
+    /**
+     * Creates an ordered sequence of two or more single coordinate operations.
+     *
+     * @deprecated Replaced by {@linkplain #createConcatenatedOperation(Map, 
CoordinateReferenceSystem,
+     * CoordinateReferenceSystem, CoordinateOperation...) a method with 
explicit CRS arguments} because
+     * of potential <abbr>CRS</abbr> swapping.
+     *
+     * @param  properties  the properties to be given to the identified object.
+     * @param  operations  the sequence of operations. Shall contain at least 
two operations.
+     * @return the concatenated operation created from the given arguments.
+     * @throws FactoryException if the object creation failed.
+     */
+    @Override
+    @Deprecated(since="1.5", forRemoval=true)
+    public CoordinateOperation createConcatenatedOperation(final Map<String,?> 
properties,
+            final CoordinateOperation... operations) throws FactoryException
+    {
+        return createConcatenatedOperation(properties, null, null, operations);
+    }
+
     /**
      * Creates an ordered sequence of two or more single coordinate operations.
      * The sequence of operations is constrained by the requirement that the 
source coordinate reference system
@@ -566,6 +586,12 @@ next:   for (SingleCRS component : 
CRS.getSingleComponents(targetCRS)) {
      * The source coordinate reference system of the first step and the target 
coordinate reference system of the
      * last step are the source and target coordinate reference system 
associated with the concatenated operation.
      *
+     * <p>As an exception to the above-cited constraint, a step can swap its 
source and target <abbr>CRS</abbr>.
+     * In such case, the effectively executed operation will be the inverse of 
that step. The {@code sourceCRS}
+     * and {@code targetCRS} arguments of this method are needed for detecting 
whether such swapping occurred
+     * in the first step or in the last step. Those optional arguments can be 
{@code null} if the caller did
+     * not swapped any <abbr>CRS</abbr>.</p>
+     *
      * <p>The properties given in argument follow the same rules as for any 
other
      * {@linkplain 
AbstractCoordinateOperation#AbstractCoordinateOperation(Map, 
CoordinateReferenceSystem,
      * CoordinateReferenceSystem, CoordinateReferenceSystem, MathTransform) 
coordinate operation} constructor.
@@ -589,12 +615,18 @@ next:   for (SingleCRS component : 
CRS.getSingleComponents(targetCRS)) {
      * </table>
      *
      * @param  properties  the properties to be given to the identified object.
+     * @param  sourceCRS   the source <abbr>CRS</abbr>, or {@code null} for 
the source of the first step.
+     * @param  targetCRS   the target <abbr>CRS</abbr>, or {@code null} for 
the target of the last effective step.
      * @param  operations  the sequence of operations. Shall contain at least 
two operations.
      * @return the concatenated operation created from the given arguments.
      * @throws FactoryException if the object creation failed.
+     *
+     * @since 1.5
      */
-    @Override
-    public CoordinateOperation createConcatenatedOperation(final Map<String,?> 
properties,
+    public CoordinateOperation createConcatenatedOperation(
+            final Map<String,?> properties,
+            final CoordinateReferenceSystem sourceCRS,
+            final CoordinateReferenceSystem targetCRS,
             final CoordinateOperation... operations) throws FactoryException
     {
         /*
@@ -604,10 +636,10 @@ next:   for (SingleCRS component : 
CRS.getSingleComponents(targetCRS)) {
          * code, in which case we do not want to modify any other metadata in 
order to stay compliant
          * with EPSG definition).
          */
-        if (operations != null && operations.length == 1) {
+        if (operations.length == 1 && sourceCRS == null && targetCRS == null) {
             return operations[0];
         }
-        final var op = new DefaultConcatenatedOperation(properties, 
operations, getMathTransformFactory());
+        final var op = new DefaultConcatenatedOperation(properties, sourceCRS, 
targetCRS, 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
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultConcatenatedOperationTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultConcatenatedOperationTest.java
index a8ac95a57c..00525ad83a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultConcatenatedOperationTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultConcatenatedOperationTest.java
@@ -67,6 +67,7 @@ public final class DefaultConcatenatedOperationTest extends 
TestCase {
 
     /**
      * Creates a “Tokyo to JGD2000” transformation.
+     * This is defined by {@code EPSG:15483}, but this test does not reproduce 
all metadata.
      *
      * @see DefaultTransformationTest#createGeocentricTranslation()
      */
@@ -92,6 +93,7 @@ public final class DefaultConcatenatedOperationTest extends 
TestCase {
 
         return new DefaultConcatenatedOperation(
                 Map.of(DefaultConversion.NAME_KEY, "Tokyo to JGD2000"),
+                null, null,
                 new AbstractSingleOperation[] {before, op, after}, mtFactory);
     }
 
@@ -121,63 +123,66 @@ public final class DefaultConcatenatedOperationTest 
extends TestCase {
                 "      Axis[“Longitude (L)”, east, Unit[“degree”, 
0.017453292519943295]],\n" +
                 "      Axis[“Latitude (B)”, north, Unit[“degree”, 
0.017453292519943295]],\n" +
                 "      Axis[“Ellipsoidal height (h)”, up, Unit[“metre”, 
1]]]],\n" +
-                "  Step[“Geographic to geocentric”,\n" +
-                "    SourceCRS[GeographicCRS[“Tokyo”,\n" +
-                "      Datum[“Tokyo 1918”,\n" +
-                "        Ellipsoid[“Bessel 1841”, 6377397.155, 
299.1528128]],\n" +
-                "      CS[ellipsoidal, 3],\n" +
-                "        Axis[“Longitude (L)”, east, Unit[“degree”, 
0.017453292519943295]],\n" +
-                "        Axis[“Latitude (B)”, north, Unit[“degree”, 
0.017453292519943295]],\n" +
-                "        Axis[“Ellipsoidal height (h)”, up, Unit[“metre”, 
1]]]],\n" +
-                "    TargetCRS[GeodeticCRS[“Tokyo 1918”,\n" +
-                "      Datum[“Tokyo 1918”,\n" +
-                "        Ellipsoid[“Bessel 1841”, 6377397.155, 
299.1528128]],\n" +
-                "      CS[Cartesian, 3],\n" +
-                "        Axis[“(X)”, geocentricX],\n" +
-                "        Axis[“(Y)”, geocentricY],\n" +
-                "        Axis[“(Z)”, geocentricZ],\n" +
-                "        Unit[“metre”, 1]]],\n" +
-                "    Method[“Geographic/geocentric conversions”]],\n" +        
 // Omit non-EPSG parameters for EPSG method.
-                "  Step[“Tokyo to JGD2000 (GSI)”, Version[“GSI-Jpn”],\n" +
-                "    SourceCRS[GeodeticCRS[“Tokyo 1918”,\n" +
-                "      Datum[“Tokyo 1918”,\n" +
-                "        Ellipsoid[“Bessel 1841”, 6377397.155, 
299.1528128]],\n" +
-                "      CS[Cartesian, 3],\n" +
-                "        Axis[“(X)”, geocentricX],\n" +
-                "        Axis[“(Y)”, geocentricY],\n" +
-                "        Axis[“(Z)”, geocentricZ],\n" +
-                "        Unit[“metre”, 1]]],\n" +
-                "    TargetCRS[GeodeticCRS[“JGD2000”,\n" +
-                "      Datum[“Japanese Geodetic Datum 2000”,\n" +
-                "        Ellipsoid[“GRS 1980”, 6378137.0, 298.257222101]],\n" +
-                "      CS[Cartesian, 3],\n" +
-                "        Axis[“(X)”, geocentricX],\n" +
-                "        Axis[“(Y)”, geocentricY],\n" +
-                "        Axis[“(Z)”, geocentricZ],\n" +
-                "        Unit[“metre”, 1]]],\n" +
-                "    Method[“Geocentric translations”],\n" +
-                "      Parameter[“X-axis translation”, -146.414],\n" +
-                "      Parameter[“Y-axis translation”, 507.337],\n" +
-                "      Parameter[“Z-axis translation”, 680.507]],\n" +
-                "  Step[“Geocentric to geographic”,\n" +
-                "    SourceCRS[GeodeticCRS[“JGD2000”,\n" +
-                "      Datum[“Japanese Geodetic Datum 2000”,\n" +
-                "        Ellipsoid[“GRS 1980”, 6378137.0, 298.257222101]],\n" +
-                "      CS[Cartesian, 3],\n" +
-                "        Axis[“(X)”, geocentricX],\n" +
-                "        Axis[“(Y)”, geocentricY],\n" +
-                "        Axis[“(Z)”, geocentricZ],\n" +
-                "        Unit[“metre”, 1]]],\n" +
-                "    TargetCRS[GeographicCRS[“JGD2000”,\n" +
-                "      Datum[“Japanese Geodetic Datum 2000”,\n" +
-                "        Ellipsoid[“GRS 1980”, 6378137.0, 298.257222101]],\n" +
-                "      CS[ellipsoidal, 3],\n" +
-                "        Axis[“Longitude (L)”, east, Unit[“degree”, 
0.017453292519943295]],\n" +
-                "        Axis[“Latitude (B)”, north, Unit[“degree”, 
0.017453292519943295]],\n" +
-                "        Axis[“Ellipsoidal height (h)”, up, Unit[“metre”, 
1]]]],\n" +
-                "    Method[“Geographic/geocentric conversions”],\n" +
-                "      Parameter[“semi_major”, 6378137.0, Unit[“metre”, 
1]],\n" +
-                "      Parameter[“semi_minor”, 6356752.314140356, 
Unit[“metre”, 1]]]]", op);
+                "  Step[\n" +
+                "    CoordinateOperation[“Geographic to geocentric”,\n" +
+                "      SourceCRS[GeographicCRS[“Tokyo”,\n" +
+                "        Datum[“Tokyo 1918”,\n" +
+                "          Ellipsoid[“Bessel 1841”, 6377397.155, 
299.1528128]],\n" +
+                "        CS[ellipsoidal, 3],\n" +
+                "          Axis[“Longitude (L)”, east, Unit[“degree”, 
0.017453292519943295]],\n" +
+                "          Axis[“Latitude (B)”, north, Unit[“degree”, 
0.017453292519943295]],\n" +
+                "          Axis[“Ellipsoidal height (h)”, up, Unit[“metre”, 
1]]]],\n" +
+                "      TargetCRS[GeodeticCRS[“Tokyo 1918”,\n" +
+                "        Datum[“Tokyo 1918”,\n" +
+                "          Ellipsoid[“Bessel 1841”, 6377397.155, 
299.1528128]],\n" +
+                "        CS[Cartesian, 3],\n" +
+                "          Axis[“(X)”, geocentricX],\n" +
+                "          Axis[“(Y)”, geocentricY],\n" +
+                "          Axis[“(Z)”, geocentricZ],\n" +
+                "          Unit[“metre”, 1]]],\n" +
+                "      Method[“Geographic/geocentric conversions”]]],\n" +  // 
Omit non-EPSG parameters for EPSG method.
+                "  Step[\n" +
+                "    CoordinateOperation[“Tokyo to JGD2000 (GSI)”, 
Version[“GSI-Jpn”],\n" +
+                "      SourceCRS[GeodeticCRS[“Tokyo 1918”,\n" +
+                "        Datum[“Tokyo 1918”,\n" +
+                "          Ellipsoid[“Bessel 1841”, 6377397.155, 
299.1528128]],\n" +
+                "        CS[Cartesian, 3],\n" +
+                "          Axis[“(X)”, geocentricX],\n" +
+                "          Axis[“(Y)”, geocentricY],\n" +
+                "          Axis[“(Z)”, geocentricZ],\n" +
+                "          Unit[“metre”, 1]]],\n" +
+                "      TargetCRS[GeodeticCRS[“JGD2000”,\n" +
+                "        Datum[“Japanese Geodetic Datum 2000”,\n" +
+                "          Ellipsoid[“GRS 1980”, 6378137.0, 
298.257222101]],\n" +
+                "        CS[Cartesian, 3],\n" +
+                "          Axis[“(X)”, geocentricX],\n" +
+                "          Axis[“(Y)”, geocentricY],\n" +
+                "          Axis[“(Z)”, geocentricZ],\n" +
+                "          Unit[“metre”, 1]]],\n" +
+                "      Method[“Geocentric translations”],\n" +
+                "        Parameter[“X-axis translation”, -146.414],\n" +
+                "        Parameter[“Y-axis translation”, 507.337],\n" +
+                "        Parameter[“Z-axis translation”, 680.507]]],\n" +
+                "  Step[\n" +
+                "    CoordinateOperation[“Geocentric to geographic”,\n" +
+                "      SourceCRS[GeodeticCRS[“JGD2000”,\n" +
+                "        Datum[“Japanese Geodetic Datum 2000”,\n" +
+                "          Ellipsoid[“GRS 1980”, 6378137.0, 
298.257222101]],\n" +
+                "        CS[Cartesian, 3],\n" +
+                "          Axis[“(X)”, geocentricX],\n" +
+                "          Axis[“(Y)”, geocentricY],\n" +
+                "          Axis[“(Z)”, geocentricZ],\n" +
+                "          Unit[“metre”, 1]]],\n" +
+                "      TargetCRS[GeographicCRS[“JGD2000”,\n" +
+                "        Datum[“Japanese Geodetic Datum 2000”,\n" +
+                "          Ellipsoid[“GRS 1980”, 6378137.0, 
298.257222101]],\n" +
+                "        CS[ellipsoidal, 3],\n" +
+                "          Axis[“Longitude (L)”, east, Unit[“degree”, 
0.017453292519943295]],\n" +
+                "          Axis[“Latitude (B)”, north, Unit[“degree”, 
0.017453292519943295]],\n" +
+                "          Axis[“Ellipsoidal height (h)”, up, Unit[“metre”, 
1]]]],\n" +
+                "      Method[“Geographic/geocentric conversions”],\n" +
+                "        Parameter[“semi_major”, 6378137.0, Unit[“metre”, 
1]],\n" +
+                "        Parameter[“semi_minor”, 6356752.314140356, 
Unit[“metre”, 1]]]]]", op);
     }
 
     /**


Reply via email to