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

asf-gitbox-commits 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 953ad86847 When invoking `CRS.findOperation(sourceCRS, targetCRS, 
context)` with a pair of CRSs created by `GridGeometry.createGridCRS(…)`, try 
to delegate to `GridGeometry.createTransformTo(…)`.
953ad86847 is described below

commit 953ad868472f1f9072b777a801a687bdde01e7a6
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon May 11 00:45:13 2026 +0200

    When invoking `CRS.findOperation(sourceCRS, targetCRS, context)` with a 
pair of CRSs created
    by `GridGeometry.createGridCRS(…)`, try to delegate to 
`GridGeometry.createTransformTo(…)`.
---
 .../coverage/grid/CoordinateOperationFinder.java   |  44 +++----
 .../apache/sis/coverage/grid/GridCRSBuilder.java   | 128 +++++++++++++++++----
 .../apache/sis/coverage/grid/GridGeometryTest.java |  48 ++++++++
 .../internal/ParameterizedTransformBuilder.java    |   4 +-
 .../internal/shared/CoordinateOperations.java      |   4 +-
 .../internal/shared/OperationMethodExt.java        |  73 ++++++++++++
 .../operation/AbstractCoordinateOperation.java     |   7 +-
 .../operation/AbstractSingleOperation.java         |   4 +-
 .../operation/CoordinateOperationContext.java      |   2 +-
 .../operation/CoordinateOperationFinder.java       |  69 +++++++----
 .../operation/CoordinateOperationRegistry.java     |  14 +--
 .../operation/DefaultConcatenatedOperation.java    |   4 +-
 .../DefaultCoordinateOperationFactory.java         |  19 +--
 .../sis/referencing/operation/DefaultFormula.java  |   9 +-
 .../operation/DefaultOperationMethod.java          |   4 +-
 .../operation/transform/MathTransformProvider.java |  31 ++---
 .../operation/DefaultTransformationTest.java       |   3 +-
 17 files changed, 346 insertions(+), 121 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
index 5d8d8aacc3..a37c8fe347 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
@@ -335,41 +335,22 @@ final class CoordinateOperationFinder extends 
CoordinateOperationContext {
      */
     private CoordinateOperation changeOfCRS() throws FactoryException, 
TransformException {
         if (!knowChangeOfCRS) {
-            changeOfCRS = changeOfCRS(source, target, this);
+            if (source.isDefined(GridGeometry.CRS) && 
target.isDefined(GridGeometry.CRS)) try {
+                /*
+                 * Unconditionally create the operation even if the CRS are 
equivalent. A non-null operation
+                 * trigs the check for wraparound axes, which is necessary 
even if the transform is identity.
+                 */
+                addAreaOfInterest(source.envelope);
+                addAreaOfInterest(target.envelope);
+                changeOfCRS = 
CRS.findOperation(source.getCoordinateMetadata(), 
target.getCoordinateMetadata(), this);
+            } catch (BackingStoreException e) {
+                throw e.unwrapOrRethrow(TransformException.class);
+            }
             knowChangeOfCRS = true;
         }
         return changeOfCRS;
     }
 
-    /**
-     * Computes the change of <abbr>CRS</abbr> between the given pair of grid 
geometries.
-     *
-     * @param  source   the grid geometry which is the source of the 
coordinate operation to find.
-     * @param  target   the grid geometry which is the target of the 
coordinate operation to find.
-     * @param  context  contains coordinate values to use as constant if a 
source CRS is missing.
-     * @return coordinate operation from source to target CRS, or {@code null} 
if none.
-     * @throws FactoryException if no operation can be found between the 
source and target CRS.
-     * @throws TransformException if some coordinates cannot be transformed to 
the specified target.
-     */
-    private static CoordinateOperation changeOfCRS(final GridGeometry source, 
final GridGeometry target,
-                                                   final 
CoordinateOperationContext context)
-            throws FactoryException, TransformException
-    {
-        CoordinateOperation changeOfCRS = null;
-        if (source.isDefined(GridGeometry.CRS) && 
target.isDefined(GridGeometry.CRS)) try {
-            /*
-             * Unconditionally create the operation even if the CRS are 
equivalent. A non-null operation
-             * trigs the check for wraparound axes, which is necessary even if 
the transform is identity.
-             */
-            context.addAreaOfInterest(source.envelope);
-            context.addAreaOfInterest(target.envelope);
-            changeOfCRS = CRS.findOperation(source.getCoordinateMetadata(), 
target.getCoordinateMetadata(), context);
-        } catch (BackingStoreException e) {
-            throw e.unwrapOrRethrow(TransformException.class);
-        }
-        return changeOfCRS;
-    }
-
     /**
      * Computes the transform from “grid coordinates of the source” to “grid 
coordinates of the target”.
      * This is a concatenation of {@link #gridToCRS()} with target "CRS to 
grid" transform.
@@ -468,7 +449,8 @@ apply:          if (forwardChangeOfCRS == null) {
                     inverseChangeOfCRS = 
changeOfCRS.getMathTransform().inverse();
                 }
             } else {
-                final CoordinateOperation inverse = changeOfCRS(target, 
source, new CoordinateOperationContext());
+                // Really need the `CoordinateOperationFinder` subclass 
because of `instanceof` checks elsewhere.
+                final CoordinateOperation inverse = new 
CoordinateOperationFinder(target, source).changeOfCRS();
                 if (inverse != null) {
                     sourceCRS = inverse.getTargetCRS();
                     inverseChangeOfCRS = inverse.getMathTransform();
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
index 1f96d230e7..5421b8639c 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
@@ -20,6 +20,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 import java.util.HashMap;
+import java.util.EnumSet;
 import java.util.Locale;
 import java.util.Optional;
 import org.opengis.util.FactoryException;
@@ -40,7 +41,7 @@ import org.opengis.referencing.crs.DerivedCRS;
 import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.datum.EngineeringDatum;
 import org.opengis.referencing.operation.Matrix;
-import org.opengis.referencing.operation.OperationMethod;
+import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
@@ -52,16 +53,20 @@ import org.apache.sis.referencing.cs.AbstractCS;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.operation.DefiningConversion;
 import org.apache.sis.referencing.operation.DefaultOperationMethod;
+import org.apache.sis.referencing.operation.CoordinateOperationContext;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 import org.apache.sis.referencing.internal.shared.AxisDirections;
 import org.apache.sis.referencing.internal.shared.DirectPositionView;
+import org.apache.sis.referencing.internal.shared.OperationMethodExt;
 import org.apache.sis.referencing.internal.shared.ReferencingFactoryContainer;
 import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Characters;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.collection.Containers;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.measure.Units;
 
@@ -82,21 +87,88 @@ import org.opengis.referencing.ObjectDomain;
  * @author  Martin Desruisseaux (IRD, Geomatys)
  */
 final class GridCRSBuilder extends ReferencingFactoryContainer {
+    /**
+     * Name of the parameter specifying the grid geometry.
+     * In principle, this parameter should be mandatory. However, it is 
defined as optional for now
+     * for avoiding the need to separate grid geometries when the 
<abbr>CRS</abbr> is compound.
+     */
+    private static final String GRID_PARAM = "Grid geometry";
+
     /**
      * Name of the parameter specifying which part (center or corner)
-     * of the call is associated with the coverage data attributes.
+     * of the cell is associated with the coverage data attributes.
      */
     private static final String ANCHOR_PARAM = "Pixel in cell";
 
     /**
      * Description of the "<abbr>CRS</abbr> to grid indices" operation method.
      */
-    private static final OperationMethod METHOD;
-    static {
-        final ParameterBuilder b = new ParameterBuilder().setRequired(true);
-        final ParameterDescriptor<?>   anchor = 
b.addName(ANCHOR_PARAM).create(PixelInCell.class, PixelInCell.CELL_CENTER);
-        final ParameterDescriptorGroup params = b.addName("CRS to grid 
indices").createGroup(anchor);
-        METHOD = new DefaultOperationMethod(Map.of(IdentifiedObject.NAME_KEY, 
params.getName()), params);
+    private static final class Method extends DefaultOperationMethod 
implements OperationMethodExt {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = -404891574462494877L;
+
+        /** Copy of {@link 
org.apache.sis.referencing.operation.DefaultConcatenatedOperation#TRANSFORM_KEY}.
 */
+        private static final String TRANSFORM_KEY = "transform";
+
+        /** The unique instance. */
+        static final Method INSTANCE;
+        static {
+            final ParameterBuilder b = new 
ParameterBuilder().setRequired(true);
+            final ParameterDescriptor<?> anchor = 
b.addName(ANCHOR_PARAM).create(PixelInCell.class, PixelInCell.CELL_CENTER);
+            final ParameterDescriptor<?> grid   = 
b.addName(GRID_PARAM).setRequired(false).create(GridGeometry.class, null);
+            INSTANCE = new Method(b.addName("CRS to grid 
indices").createGroup(grid, anchor));
+        }
+
+        /** Creates the unique instance. */
+        private Method(final ParameterDescriptorGroup params) {
+            super(Map.of(IdentifiedObject.NAME_KEY, params.getName()), params);
+        }
+
+        /**
+         * If the given <abbr>CRS</abbr> has been built by this method, 
returns the grid geometry.
+         * Otherwise, returns {@code null}. The {@code anchor} argument is 
used for verifying that
+         * the two operations use the same "pixel in cell" configuration.
+         */
+        private static GridGeometry grid(final CoordinateReferenceSystem crs, 
final EnumSet<PixelInCell> anchor) {
+            if (crs instanceof DerivedCRS) {
+                final Conversion conversion = ((DerivedCRS) 
crs).getConversionFromBase();
+                if (conversion.getMethod() instanceof Method) {
+                    final ParameterValueGroup values = 
conversion.getParameterValues();
+                    if (anchor.isEmpty() == anchor.add((PixelInCell) 
values.parameter(ANCHOR_PARAM).getValue())) {
+                        return (GridGeometry) 
values.parameter(GRID_PARAM).getValue();
+                    }
+                }
+            }
+            return null;
+        }
+
+        /**
+         * If the given pair of <abbr>CRS</abbr>s is derived from grid 
geometries, finds a transform between them.
+         * Compared to the default transform, the returned transform may 
handle the anti-meridian crossing.
+         */
+        @Override
+        public boolean completeOperationMetadata(final 
CoordinateOperationContext context,
+                                                 final 
CoordinateReferenceSystem  sourceCRS,
+                                                 final 
CoordinateReferenceSystem  targetCRS,
+                                                 final Map<String, Object> 
properties)
+        {
+            // The `instanceof` check is important for preventing never ending 
recursive invocations.
+            if (!(context instanceof CoordinateOperationFinder || 
properties.containsKey(TRANSFORM_KEY))) {
+                final EnumSet<PixelInCell> anchor = 
EnumSet.noneOf(PixelInCell.class);
+                final GridGeometry source = grid(sourceCRS, anchor);
+                if (source != null) {
+                    final GridGeometry target = grid(targetCRS, anchor);
+                    if (target != null) try {
+                        // The set should always have exactly one element. If 
not, it would be a bug in `grid(…)`.
+                        properties.put(TRANSFORM_KEY, 
source.createTransformTo(target, Containers.peekIfSingleton(anchor)));
+                        return true;
+                    } catch (TransformException e) {
+                        throw new BackingStoreException(e);
+                    }
+                }
+            }
+            return false;
+        }
     }
 
     /**
@@ -106,9 +178,12 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
     private static final InternationalString SCOPE = 
Resources.formatInternational(Resources.Keys.CrsToGridConversion);
 
     /**
-     * The extent of the grid geometry, or {@code null} if none.
+     * The grid geometry for which to create a derived or compound 
<abbr>CRS</abbr> for cell indices.
+     * This is kept constant after initialization, i.e. this field is not 
updated when descending in
+     * <abbr>CRS</abbr> components. May be {@code null} if the 
<abbr>CRS</abbr> is built only from a
+     * grid extent.
      */
-    private GridExtent extent;
+    private GridGeometry fullGrid;
 
     /**
      * The cell part (center or corner) to map.
@@ -176,9 +251,7 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
         grid.getGeographicExtent().ifPresent((domain) -> {
             properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, new 
DefaultExtent(null, domain, null, null));
         });
-        if (grid.isDefined(GridGeometry.EXTENT)) {
-            extent = grid.getExtent();
-        }
+        fullGrid = grid;
         if (derived || grid.isDefined(GridGeometry.CRS | 
GridGeometry.GRID_TO_CRS)) try {
             separator = new 
TransformSeparator(grid.getGridToCRS(anchor).inverse());
             return forComponent(name, grid.getCoordinateReferenceSystem(), 0, 
0);
@@ -192,8 +265,8 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
          */
         final int dimension = grid.getDimension();
         final DimensionNameType[] dimensionNames;
-        if (extent != null) {
-            dimensionNames = Arrays.copyOf(extent.getAxisTypes(), dimension);
+        if (grid.isDefined(GridGeometry.EXTENT)) {
+            dimensionNames = Arrays.copyOf(grid.getExtent().getAxisTypes(), 
dimension);
         } else {
             dimensionNames = new DimensionNameType[dimension];
         }
@@ -210,11 +283,11 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
      * Caller can get the dimensions that have been used. Caller shall invoke 
{@code transform.clear()}
      * before to invoke this method again.</p>
      *
-     * @param  name     name of the <abbr>CRS</abbr> to create.
+     * @param  name     name of the derived or compound <abbr>CRS</abbr> to 
create.
      * @param  baseCRS  real world <abbr>CRS</abbr> or component of that 
<abbr>CRS</abbr>.
      * @param  srcDim   dimension of the first axis of {@code baseCRS} 
relatively to the full real world <abbr>CRS</abbr>.
      * @param  tgtDim   dimension of the first axis of the return value 
relatively to the full derived <abbr>CRS</abbr>.
-     * @return grid extent <abbr>CRS</abbr> derived from the given {@code 
baseCRS}.
+     * @return <abbr>CRS</abbr> for cell indices derived from the 
<abbr>CRS</abbr> of the given grid geometry.
      * @throws FactoryException if an error occurred during the use of a 
referencing factory.
      */
     private CoordinateReferenceSystem forComponent(final Object name, final 
CoordinateReferenceSystem baseCRS, int srcDim, int tgtDim)
@@ -263,9 +336,6 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
         final MathTransform crsToGrid = separator.separate();
         final int[] dispatch = separator.getTargetDimensions();
         final var dimensionNames = new DimensionNameType[dispatch.length];
-        if (extent != null) {
-            Arrays.setAll(dimensionNames, (i) -> 
extent.getAxisType(dispatch[i]).orElse(null));
-        }
         /*
          * Get the directions of the axes of the coverage coordinate system, 
but in the order of grid dimensions.
          * The direction array may contain null elements if directions could 
not be inferred for some dimensions.
@@ -276,7 +346,9 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
         AxisDirection[] directions;
 toGrid: try {
             final Matrix derivative;
-            if (extent != null) {
+            if (fullGrid.isDefined(GridGeometry.EXTENT)) {
+                final GridExtent extent = fullGrid.getExtent();
+                Arrays.setAll(dimensionNames, (i) -> 
extent.getAxisType(dispatch[i]).orElse(null));
                 derivative = crsToGrid.derivative(new 
DirectPositionView.Double(extent.getPointOfInterest(anchor), srcDim, 
dimension));
             } else try {
                 derivative = crsToGrid.derivative(null);
@@ -302,12 +374,18 @@ toGrid: try {
             directions = directions(dimensionNames);
         }
         /*
-         * Creates the coordinate system, then the conversion, and finally the 
derived CRS.
+         * Create the coordinate system, then the conversion, and finally the 
derived CRS.
+         * The `GRID_PARAM` parameter should be mandatory, but for now it is 
not clear that
+         * it is worth to pay the cost of creating sub-grids.
          */
-        final CoordinateSystem cs = createCS(dispatch.length, dimensionNames, 
directions, tgtDim + 1, true);
-        final ParameterValueGroup params = 
METHOD.getParameters().createValue();
+        final Method method = Method.INSTANCE;
+        final ParameterValueGroup params = 
method.getParameters().createValue();
+        if (srcDim == 0 && dimension == fullGrid.getDimension()) {
+            params.parameter(GRID_PARAM).setValue(fullGrid);
+        }
         params.parameter(ANCHOR_PARAM).setValue(anchor);
-        final var conversion = new 
DefiningConversion(properties(METHOD.getName()), METHOD, crsToGrid, params);
+        final var conversion = new 
DefiningConversion(properties(method.getName()), method, crsToGrid, params);
+        final CoordinateSystem cs = createCS(dispatch.length, dimensionNames, 
directions, tgtDim + 1, true);
         return getCRSFactory().createDerivedCRS(properties(name), baseCRS, 
conversion, cs);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
index 4c03cc09fe..4c9c242138 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
@@ -33,6 +33,7 @@ import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.opengis.referencing.operation.CoordinateOperation;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.ImmutableIdentifier;
@@ -43,6 +44,7 @@ import org.apache.sis.referencing.operation.matrix.Matrix4;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.WraparoundTransform;
 import org.apache.sis.referencing.internal.shared.ExtendedPrecisionMatrix;
 import org.apache.sis.util.ComparisonMode;
 
@@ -883,6 +885,52 @@ public final class GridGeometryTest extends TestCase {
         assertAxisDirectionsEqual(cs, AxisDirection.NORTH, AxisDirection.WEST, 
AxisDirection.FUTURE);
     }
 
+    /**
+     * Tests coordinate operation between two derived <abbr>CRS</abbr>s
+     * created by {@link GridGeometry#createGridCRS(Identifier, PixelInCell)}.
+     * Apache <abbr>SIS</abbr> should recognize this special case and redirects
+     * to {@link GridGeometry#createTransformTo(GridGeometry, PixelInCell)}.
+     *
+     * @throws FactoryException if the <abbr>CRS</abbr> cannot be built.
+     */
+    @Test
+    public void testCreateConcatenatedOperation() throws FactoryException {
+        final var extent  = new GridExtent(90, 30);
+        final var crsPair = new CoordinateReferenceSystem[2];
+        for (int i=0; i<crsPair.length; i++) {
+            MathTransform gridToCRS = MathTransforms.linear(new Matrix3(
+                    1, 0, 150 + i,  // Longitude overlapping the anti-meridian.
+                    0, 1, 25,       // Latitude
+                    0, 0,  1));
+            if (i == 0) {
+                /*
+                 * Add an arbitrary non-linear transform while still 
pretending that the CRS is WGS84.
+                 * This is not correct as the CRS is Mercator. But we don't 
declare that CRS because,
+                 * for the purpose of this test, we don't want the projected 
CRS to be reversed, thus
+                 * cancelling our artificial non-linear part.
+                 */
+                gridToCRS = MathTransforms.concatenate(gridToCRS,
+                        
HardCodedConversions.mercator().getConversionFromBase().getMathTransform());
+            }
+            final var name = new ImmutableIdentifier(null, null, "Grid #" + i);
+            final var grid = new GridGeometry(extent, PixelInCell.CELL_CORNER, 
gridToCRS, HardCodedCRS.WGS84);
+            crsPair[i] = grid.createGridCRS(name, PixelInCell.CELL_CENTER);
+        }
+        /*
+         * Ask for the transform between the two grids and verify that it 
contains a `WraparoundTransform` step.
+         * The presence of this step is what differentiate the result from 
what we would get without the special
+         * case.
+         */
+        int wraparound = 0;
+        final CoordinateOperation op = CRS.findOperation(crsPair[0], 
crsPair[1], null);
+        for (MathTransform step : 
MathTransforms.getSteps(op.getMathTransform())) {
+            if (step instanceof WraparoundTransform) {
+                wraparound++;
+            }
+        }
+        assertEquals(1, wraparound);
+    }
+
     /**
      * Tests {@link GridGeometry#createTransformTo(GridGeometry, PixelInCell)}.
      *
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
index 68f4d7c600..1db2c40fca 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
@@ -136,7 +136,7 @@ public class ParameterizedTransformBuilder extends 
MathTransformBuilder implemen
      *
      * @see #getContextualParameters()
      */
-    private final Map<String,Boolean> contextualParameters;
+    private final Map<String, Boolean> contextualParameters;
 
     /**
      * Whether the user-specified parameters have been completed with the 
contextual parameters.
@@ -403,7 +403,7 @@ public class ParameterizedTransformBuilder extends 
MathTransformBuilder implemen
      * @return names of parameters inferred from context.
      */
     @Override
-    public Map<String,Boolean> getContextualParameters() {
+    public Map<String, Boolean> getContextualParameters() {
         return Collections.unmodifiableMap(contextualParameters);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
index 22d9716c6b..6f7937bdee 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
@@ -135,7 +135,7 @@ public final class CoordinateOperations {
             }
             properties = Map.of();
         }
-        final HashMap<String,Object> p = new HashMap<>(properties);
+        final var p = new HashMap<String, Object>(properties);
         p.putIfAbsent(ReferencingFactoryContainer.CRS_FACTORY, crsFactory);
         p.putIfAbsent(ReferencingFactoryContainer.CS_FACTORY,  csFactory);
         properties = p;
@@ -370,7 +370,7 @@ public final class CoordinateOperations {
          * unmodifiable List<Integer>. The list is for public API; internally, 
Apache SIS will use toBitMask(…).
          */
         long r = changes;
-        final Integer[] indices = new Integer[Long.bitCount(r)];
+        final var indices = new Integer[Long.bitCount(r)];
         for (int i=0; i<indices.length; i++) {
             final int dim = Long.numberOfTrailingZeros(r);
             indices[i] = dim;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/OperationMethodExt.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/OperationMethodExt.java
new file mode 100644
index 0000000000..f737645394
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/OperationMethodExt.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.internal.shared;
+
+import java.util.Map;
+import org.opengis.metadata.extent.Extent;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.SingleOperation;
+import org.opengis.referencing.operation.OperationMethod;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.referencing.operation.CoordinateOperationContext;
+import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
+import org.apache.sis.metadata.iso.extent.Extents;
+
+
+/**
+ * Extension of {@code OperationMethod} kept in a separated interface
+ * because we are not sure if it is ready for public <abbr>API</abbr>.
+ * Note: it could as well be a protected method in {@code 
DefaultOperationMethod}.
+ */
+public interface OperationMethodExt extends OperationMethod {
+    /**
+     * Optionally updates the metadata of a coordinate operation between a 
given pair of <abbr>CRS</abbr>s.
+     * This method may be invoked when a coordinate operation is or contains a 
{@link SingleOperation} step
+     * which uses this {@code OperationMethod}.
+     * The {@code source} and {@code target} arguments contains the 
<abbr>CRS</abbr>s of the operation which
+     * will be constructed with the given {@code properties} map.
+     *
+     * <p>This method can be implemented for purposes such as restricting the 
domain of validity or reducing the
+     * declared accuracy of any coordinate operation which uses (potentially 
indirectly) this {@code OperationMethod}.
+     * The given {@code properties} map contains the metadata that the caller 
intends to give to the operation to create.
+     * This method can update the given {@code properties} map and returns 
{@code true},
+     * or returns {@code false} if this method did nothing.</p>
+     *
+     * <p>This method may need to merge map values instead of replacing them.
+     * For example, a domain of validity may need to be set to the {@linkplain 
Extents#intersection(Extent, Extent)
+     * intersection} of the domain provided by this method with the domain 
already contained in the map (if any).</p>
+     *
+     * <p>The recognized keys and the valid values of the {@code properties} 
map are documented in the
+     * {@linkplain 
AbstractCoordinateOperation#AbstractCoordinateOperation(Map, 
CoordinateReferenceSystem,
+     * CoordinateReferenceSystem, CoordinateReferenceSystem, MathTransform) 
operation constructor}.</p>
+     *
+     * @param  context     context of the coordinate operation to create, or 
{@code null} if none.
+     * @param  source      the source <abbr>CRS</abbr> of the operation which 
will use the given {@code properties}.
+     * @param  target      the target <abbr>CRS</abbr> of the operation which 
will use the given {@code properties}.
+     * @param  properties  a modifiable map of metadata to be given to the 
operation between the given <abbr>CRS</abbr>s.
+     * @return whether this method has modified the {@code properties} map.
+     *
+     * @todo If this method move to public <abbr>API</abbr>, {@code 
CoordinateReferenceSystem}
+     *       should be replaced by {@code CoordinateMetadata}.
+     */
+    default boolean completeOperationMetadata(CoordinateOperationContext 
context,
+                                              CoordinateReferenceSystem  
source,
+                                              CoordinateReferenceSystem  
target,
+                                              Map<String, Object> properties)
+    {
+        return false;
+    }
+}
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 436f95a10b..6362c40b19 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
@@ -197,8 +197,8 @@ public class AbstractCoordinateOperation extends 
AbstractIdentifiedObject implem
      * to positions in the {@linkplain #getTargetCRS target coordinate 
reference system}.
      *
      * <p><b>Consider this field as final!</b>
-     * This field is non-final only for the convenience of constructors and 
for initialization
-     * at XML unmarshalling time by {@link 
AbstractSingleOperation#afterUnmarshal(Unmarshaller, Object)}.</p>
+     * This field is non-final for the convenience of constructors and for 
initialization at <abbr>XML</abbr>
+     * unmarshalling time by {@link 
AbstractSingleOperation#afterUnmarshal(Unmarshaller, Object)}.</p>
      */
     @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
     MathTransform transform;
@@ -491,7 +491,10 @@ check:      for (int isTarget=0; ; isTarget++) {        // 
0 == source check; 1
      * When this method returns {@code true}, the source and target CRS are 
not marshalled in XML documents.
      *
      * @return {@code true} if this coordinate operation is for the definition 
of a derived or projected CRS.
+     *
+     * @deprecated Replaced by the {@link DefiningConversion} class.
      */
+    @Deprecated(since = "1.7", forRemoval = true)
     public boolean isDefiningConversion() {
         /*
          * Trick: we do not need to verify if (this instanceof Conversion) 
because:
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
index 3d954d8889..2fe5df6bf1 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
@@ -230,8 +230,8 @@ class AbstractSingleOperation extends 
AbstractCoordinateOperation implements Sin
      * This situation happens when this operation has been initialized from a 
<em>defining conversion</em>
      * and the caller refined the parameters using information provided by the 
math transform factory.
      * On one hand, we want to take advantage of additional information 
present in {@code definition}
-     * such as OGC aliases (those information are often missing in {@link 
#method} if the latter
-     * is not a {@link 
org.apache.sis.referencing.operation.transform.MathTransformProvider}).
+     * such as <abbr>OGC</abbr> aliases (those information are often missing 
in {@link #method} if the
+     * latter is not a {@link 
org.apache.sis.referencing.operation.transform.MathTransformProvider}).
      * But on the other hand, {@code definition} may contain contextual 
parameters (ellipsoid semi-axis lengths)
      * which are unknown to {@link #method} and would cause an {@link 
InvalidParameterValueException} if we try
      * to set them. We could replace {@link #method}, but if the latter was 
created from EPSG database it also
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
index 93c24183af..aab9161cbd 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
@@ -47,7 +47,7 @@ import org.apache.sis.measure.Longitude;
  * </ul>
  *
  * While optional, those information can help {@link 
DefaultCoordinateOperationFactory}
- * to choose the most suitable coordinate transformation between two CRS.
+ * to choose the most suitable coordinate transformation between two 
<abbr>CRS</abbr>s.
  *
  * <p>{@code CoordinateOperationContext} is part of the <abbr>API</abbr>
  * used by <abbr>SIS</abbr> for implementing the <i>late binding</i> model.
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 53fc23a90d..baa510d453 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
@@ -24,6 +24,7 @@ import java.util.Collections;
 import java.util.ListIterator;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.function.Consumer;
 import java.time.Duration;
 import javax.measure.Unit;
 import javax.measure.IncommensurableException;
@@ -50,6 +51,7 @@ import org.apache.sis.referencing.internal.AnnotatedMatrix;
 import org.apache.sis.referencing.internal.PositionalAccuracyConstant;
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.referencing.internal.shared.AxisDirections;
+import org.apache.sis.referencing.internal.shared.OperationMethodExt;
 import org.apache.sis.referencing.internal.shared.CoordinateOperations;
 import org.apache.sis.referencing.internal.shared.EllipsoidalHeightCombiner;
 import org.apache.sis.referencing.internal.shared.ReferencingUtilities;
@@ -65,6 +67,7 @@ import 
org.apache.sis.referencing.operation.provider.DatumShiftMethod;
 import org.apache.sis.referencing.operation.provider.GeocentricAffine;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.internal.shared.Constants;
 import org.apache.sis.util.internal.shared.DoubleDouble;
 import org.apache.sis.util.resources.Vocabulary;
@@ -1127,7 +1130,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
             if (isAxisChange2 && mt2.getSourceDimensions() == 
mt2.getTargetDimensions()) main = step1;
         }
         if (main instanceof SingleOperation) {
-            final SingleOperation op = (SingleOperation) main;
+            final var op = (SingleOperation) main;
             main = createFromMathTransform(
                     new HashMap<>(IdentifiedObjects.getProperties(main)),
                     sourceCRS,
@@ -1137,9 +1140,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
                     op.getParameterValues(),
                     typeOf(op));
         } else {
-            main = factory.createConcatenatedOperation(
-                    defaultName(sourceCRS, targetCRS),
-                    sourceCRS, targetCRS, step1, step2);
+            main = createConcatenatedOperation(sourceCRS, targetCRS, step1, 
step2);
         }
         /*
          * Sometimes we get a concatenated operation made of an operation 
followed by its inverse.
@@ -1181,8 +1182,50 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         if (isIdentity(step3)) return concatenate(step1, step2);
         if (canHide(step1.getName())) return concatenate(concatenate(step1, 
step2), step3);
         if (canHide(step3.getName())) return concatenate(step1, 
concatenate(step2, step3));
-        final Map<String, ?> properties = defaultName(step1.getSourceCRS(), 
step3.getTargetCRS());
-        return factory.createConcatenatedOperation(properties, null, null, 
step1, step2, step3);
+        return createConcatenatedOperation(step1.getSourceCRS(), 
step3.getTargetCRS(), step1, step2, step3);
+    }
+
+    /**
+     * Creates an ordered sequence of two or more single coordinate operations.
+     * The {@code sourceCRS} and {@code targetCRS} arguments of this method are
+     * needed for detecting whether the source or last step needs to be 
reversed.
+     *
+     * <p>If any operation step uses a method that implements the {@link 
OperationMethodExt} interface,
+     * it will be used for enriching the metadata. It may go as far as 
overriding the default algorithm for
+     * computing the transform if a method supplies an {@value 
DefaultConcatenatedOperation#TRANSFORM_KEY}
+     * value.</p>
+     *
+     * @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. Should contain at least 
two operations.
+     * @return the concatenated operation created from the given arguments.
+     * @throws FactoryException if the object creation failed.
+     */
+    private CoordinateOperation createConcatenatedOperation(
+            final CoordinateReferenceSystem sourceCRS,
+            final CoordinateReferenceSystem targetCRS,
+            final CoordinateOperation... operations) throws FactoryException
+    {
+        final var properties = new HashMap<String, Object>(4);
+        properties.put(IdentifiedObject.NAME_KEY, new CRSPair(sourceCRS, 
targetCRS).toString());
+        final var merge  = new Consumer<CoordinateOperation>() {
+            @Override public void accept(final CoordinateOperation operation) {
+                final OperationMethod method = 
CoordinateOperations.getMethod(operation);
+                if (method instanceof OperationMethodExt) {
+                    final var provider = (OperationMethodExt) method;
+                    provider.completeOperationMetadata(context, sourceCRS, 
targetCRS, properties);
+                }
+                if (operation instanceof ConcatenatedOperation) {
+                    ((ConcatenatedOperation) 
operation).getOperations().stream().forEach(this);
+                }
+            }
+        };
+        try {
+            for (CoordinateOperation step : operations) merge.accept(step);
+            return factory.createConcatenatedOperation(properties, sourceCRS, 
targetCRS, operations);
+        } catch (BackingStoreException e) {
+            throw e.unwrapOrRethrow(FactoryException.class);
+        }
     }
 
     /**
@@ -1219,13 +1262,6 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         return (id == AXIS_CHANGES) || (id == IDENTITY);
     }
 
-    /**
-     * Returns the given name in a singleton map.
-     */
-    private static Map<String, ?> properties(final String name) {
-        return Map.of(IdentifiedObject.NAME_KEY, name);
-    }
-
     /**
      * Returns a name for an object derived from the specified one.
      * This method builds a name of the form "{@literal <original identifier>} 
(step 1)"
@@ -1249,13 +1285,6 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
         return properties;
     }
 
-    /**
-     * Returns a name for a transformation between two CRS.
-     */
-    private static Map<String, ?> defaultName(CoordinateReferenceSystem 
source, CoordinateReferenceSystem target) {
-        return properties(new CRSPair(source, target).toString());
-    }
-
     /**
      * Returns the given operation as a list of one element. We cannot use 
{@link Collections#singletonList(Object)}
      * because the list needs to be modifiable, as required by {@link 
#createOperations(CoordinateReferenceSystem,
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 d84eb81c72..d64f692bd4 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
@@ -787,7 +787,7 @@ class CoordinateOperationRegistry {
         final CoordinateReferenceSystem targetCRS = op.getTargetCRS();
         final MathTransform transform = op.getMathTransform().inverse();
         final OperationMethod method = 
InverseOperationMethod.create(op.getMethod(), this);
-        final Map<String,Object> properties = properties(INVERSE_OPERATION);
+        final Map<String, Object> properties = properties(INVERSE_OPERATION);
         InverseOperationMethod.properties(op, properties);
         inverse = createFromMathTransform(properties, targetCRS, sourceCRS, 
transform, method, null, typeOf(op));
         AbstractCoordinateOperation.setCachedInverse(op, inverse);
@@ -821,7 +821,7 @@ class CoordinateOperationRegistry {
         if (operation instanceof ConcatenatedOperation) {
             final CoordinateOperation[] inverted = 
getSteps((ConcatenatedOperation) operation, true);
             ArraysExt.reverse(inverted);
-            final Map<String,Object> properties = 
properties(INVERSE_OPERATION);
+            final Map<String, Object> properties = 
properties(INVERSE_OPERATION);
             final MathTransform transform = operation.getMathTransform();
             if (transform != null) {
                 properties.put(DefaultConcatenatedOperation.TRANSFORM_KEY, 
transform.inverse());
@@ -1016,7 +1016,7 @@ class CoordinateOperationRegistry {
         CoordinateReferenceSystem crs;
         if (Utilities.equalsApproximately(sourceCRS, crs = 
operation.getSourceCRS())) sourceCRS = crs;
         if (Utilities.equalsApproximately(targetCRS, crs = 
operation.getTargetCRS())) targetCRS = crs;
-        final Map<String,Object> properties = new 
HashMap<>(derivedFrom(operation));
+        final var properties = new HashMap<String, 
Object>(derivedFrom(operation));
         properties.put(CoordinateOperations.OPERATION_TYPE_KEY, 
typeOf(operation));
         /*
          * Reuse the same operation method, but we may need to change its 
number of dimension.
@@ -1024,7 +1024,7 @@ class CoordinateOperationRegistry {
          * The capability to resize an operation method is specific to Apache 
SIS.
          */
         if (operation instanceof SingleOperation) {
-            final SingleOperation single = (SingleOperation) operation;
+            final var single = (SingleOperation) operation;
             properties.put(CoordinateOperations.PARAMETERS_KEY, 
single.getParameterValues());
             if (method == null) {
                 method = single.getMethod();
@@ -1281,8 +1281,8 @@ class CoordinateOperationRegistry {
      * @param  name  the name to put in a map.
      * @return a modifiable map containing the given name. Callers can put 
other entries in this map.
      */
-    static Map<String,Object> properties(final Identifier name) {
-        final var properties = new HashMap<String,Object>(4);
+    static Map<String, Object> properties(final Identifier name) {
+        final var properties = new HashMap<String, Object>(4);
         properties.put(CoordinateOperation.NAME_KEY, name);
         return properties;
     }
@@ -1347,7 +1347,7 @@ class CoordinateOperationRegistry {
      * @return a coordinate operation using the specified math transform.
      * @throws FactoryException if the operation cannot be created.
      */
-    final CoordinateOperation createFromMathTransform(final Map<String,Object> 
       properties,
+    final CoordinateOperation createFromMathTransform(final Map<String, 
Object>       properties,
                                                       final 
CoordinateReferenceSystem sourceCRS,
                                                       final 
CoordinateReferenceSystem targetCRS,
                                                       final MathTransform      
       transform,
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 916bb702d3..c9a43c030f 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
@@ -210,8 +210,8 @@ final class DefaultConcatenatedOperation extends 
AbstractCoordinateOperation imp
      *   <li>Set the {@link #coordinateOperationAccuracy} field, but only if 
{@code setAccuracy} is {@code true}.</li>
      * </ul>
      *
-     * This method invokes itself recursively if there is nested {@code 
ConcatenatedOperation} instances
-     * in the given list. This should not happen according ISO 19111 standard, 
but we try to be safe.
+     * This method invokes itself recursively if there are nested {@code 
ConcatenatedOperation} instances
+     * in the given list. This should not happen according <abbr>ISO</abbr> 
19111 standard, but we try to be safe.
      *
      * <h4>How coordinate operation accuracy is determined</h4>
      * If {@code setAccuracy} is {@code true}, then this method copies 
accuracy information found in the single
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 99a1fb9c27..0604dff9fa 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
@@ -443,7 +443,7 @@ next:   for (SingleCRS component : 
CRS.getSingleComponents(targetCRS)) {
         /*
          * Undocumented (for now) feature: if the `transform` argument is null 
but parameters are
          * found in the given properties, create the MathTransform instance 
from those parameters.
-         * This is needed for WKT parsing of CoordinateOperation[…] among 
others.
+         * This is needed for WKT parsing of "CoordinateOperation[…]" among 
other user cases.
          */
         if (transform == null) {
             final ParameterValueGroup parameters = 
Containers.property(properties,
@@ -611,16 +611,19 @@ next:   for (SingleCRS component : 
CRS.getSingleComponents(targetCRS)) {
              * cause the lost of the original CRS with the desired longitude 
range.
              */
             if (single instanceof SingleOperation) {
-                final Map<String,Object> merge = new HashMap<>(
+                final var modifiedProperties = new HashMap<String, Object>(
                         IdentifiedObjects.getProperties(single, 
CoordinateOperation.IDENTIFIERS_KEY));
-                merge.put(CoordinateOperations.PARAMETERS_KEY, 
((SingleOperation) single).getParameterValues());
+                modifiedProperties.put(CoordinateOperations.PARAMETERS_KEY, 
((SingleOperation) single).getParameterValues());
                 if (single instanceof AbstractIdentifiedObject) {
-                    merge.put(CoordinateOperations.OPERATION_TYPE_KEY, 
((AbstractIdentifiedObject) single).getInterface());
+                    
modifiedProperties.put(CoordinateOperations.OPERATION_TYPE_KEY, 
((AbstractIdentifiedObject) single).getInterface());
                 }
-                merge.putAll(properties);
-                return createSingleOperation(merge, op.getSourceCRS(), 
op.getTargetCRS(),
-                        op.getInterpolationCRS().orElse(null),
-                        ((SingleOperation) single).getMethod(), 
op.getMathTransform());
+                modifiedProperties.putAll(properties);
+                return createSingleOperation(modifiedProperties,
+                                             op.getSourceCRS(),
+                                             op.getTargetCRS(),
+                                             
op.getInterpolationCRS().orElse(null),
+                                             ((SingleOperation) 
single).getMethod(),
+                                             op.getMathTransform());
             }
         }
         return single;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultFormula.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultFormula.java
index 4f66c2f9fe..cc7771e763 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultFormula.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultFormula.java
@@ -129,6 +129,8 @@ public class DefaultFormula extends FormattableObject 
implements Formula, Serial
 
     /**
      * Returns the formula(s) or procedure used by the operation method, or 
{@code null} if none.
+     *
+     * @return a textual description of the formula.
      */
     @Override
     public InternationalString getFormula() {
@@ -138,6 +140,8 @@ public class DefaultFormula extends FormattableObject 
implements Formula, Serial
     /**
      * Returns the reference to a publication giving the formula(s) or 
procedure used by the
      * coordinate operation method, or {@code null} if none.
+     *
+     * @return reference to the publication giving the formula.
      */
     @Override
     public Citation getCitation() {
@@ -146,6 +150,8 @@ public class DefaultFormula extends FormattableObject 
implements Formula, Serial
 
     /**
      * Returns a hash code value for this formula.
+     *
+     * @return hash code value computed from the textual representation or 
reference to the formula.
      */
     @Override
     public int hashCode() {
@@ -167,7 +173,7 @@ public class DefaultFormula extends FormattableObject 
implements Formula, Serial
             return true;
         }
         if (object != null && object.getClass() == getClass()) {
-            final DefaultFormula that = (DefaultFormula) object;
+            final var that = (DefaultFormula) object;
             return Objects.equals(this.formula,  that.formula) &&
                    Objects.equals(this.citation, that.citation);
         }
@@ -188,6 +194,7 @@ public class DefaultFormula extends FormattableObject 
implements Formula, Serial
     @Override
     protected String formatTo(final Formatter formatter) {
         InternationalString text = null;
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final Citation citation = getCitation();    // Gives to users a chance 
to override properties.
         if (citation != null) {
             text = citation.getTitle();
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultOperationMethod.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultOperationMethod.java
index acf3d1d4fc..21cb4ba6e1 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultOperationMethod.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultOperationMethod.java
@@ -275,8 +275,8 @@ public class DefaultOperationMethod extends 
AbstractIdentifiedObject implements
      *                    or {@code null} if it is not going to have any 
declared authority.
      * @return the identified object properties in a mutable map.
      */
-    private static Map<String,Object> getProperties(final IdentifiedObject 
info, final Citation authority) {
-        final Map<String,Object> properties = new 
HashMap<>(IdentifiedObjects.getProperties(info));
+    private static Map<String, Object> getProperties(final IdentifiedObject 
info, final Citation authority) {
+        final var properties = new HashMap<String, 
Object>(IdentifiedObjects.getProperties(info));
         properties.put(NAME_KEY, new NamedIdentifier(authority, 
info.getName().getCode()));
         properties.remove(IDENTIFIERS_KEY);
         return properties;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransformProvider.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransformProvider.java
index bb7d4ce36c..9a37ae59ef 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransformProvider.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransformProvider.java
@@ -28,12 +28,13 @@ import org.opengis.referencing.datum.Ellipsoid;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.OperationMethod;
 
 
 /**
  * An object capable to create {@code MathTransform} instances from given 
parameter values.
- * This interface is the Apache SIS mechanism by which
- * {@linkplain org.apache.sis.referencing.operation.DefaultFormula formula} 
are concretized as Java code.
+ * This interface is the Apache <abbr>SIS</abbr> mechanism by which
+ * {@linkplain org.opengis.referencing.operation.Formula formula} are 
concretized as Java code.
  * A math transform provider ignores the source and target <abbr>CRS</abbr> 
and works with coordinates in
  * predefined axis order and units — typically (east, north, up) in degrees or 
meters — although some
  * variations are allowed in the number of dimensions (typically the "up" 
dimension being optional).
@@ -41,21 +42,21 @@ import 
org.opengis.referencing.operation.MathTransformFactory;
  *
  * <p>This interface is generally not used directly. The recommended way to 
get a {@link MathTransform}
  * is to {@linkplain org.apache.sis.referencing.CRS#findOperation find the 
coordinate operation}
- * (generally from a pair of <var>source</var> and <var>target</var> CRS), 
then to invoke
- * {@link 
org.opengis.referencing.operation.CoordinateOperation#getMathTransform()}.
+ * (generally from a pair of <var>source</var> and <var>target</var> 
<abbr>CRS</abbr>s),
+ * then to invoke {@link 
org.opengis.referencing.operation.CoordinateOperation#getMathTransform()}.
  * Alternatively, one can also use a {@linkplain DefaultMathTransformFactory 
math transform factory}.</p>
  *
- * <p>Implementations of this interface usually extend {@link 
org.apache.sis.referencing.operation.DefaultOperationMethod},
- * but this is not mandatory. This interface can also be used alone since 
{@link MathTransform} instances can be created
- * for other purpose than coordinate operations.</p>
+ * <p>Implementations of this interface usually implement also the {@link 
OperationMethod} interface,
+ * but this is not mandatory. This interface can be used alone since {@link 
MathTransform} instances
+ * can be created for other purposes than coordinate operations.</p>
  *
  *
- * <h2>How to add custom coordinate operations to Apache SIS</h2>
- * {@link DefaultMathTransformFactory} can discover automatically new 
coordinate operations
- * (including map projections) by scanning the module path. To define a custom 
coordinate operation,
- * one needs to define a <strong>thread-safe</strong> class implementing 
<strong>both</strong> this
- * {@code MathTransformProvider} interface and the {@link 
org.opengis.referencing.operation.OperationMethod} one.
- * While not mandatory, we suggest to extend {@link 
org.apache.sis.referencing.operation.DefaultOperationMethod}.
+ * <h2>How to add custom coordinate operation methods to Apache 
<abbr>SIS</abbr></h2>
+ * {@link DefaultMathTransformFactory} can discover automatically new 
coordinate operation methods
+ * (including map projections) by scanning the module path. To define a custom 
method, one needs to
+ * define a <em>thread-safe</em> class implementing <em>both</em> this {@code 
MathTransformProvider}
+ * interface and the {@link OperationMethod} interface. The latter can be 
implemented indirectly by
+ * extending {@link 
org.apache.sis.referencing.operation.DefaultOperationMethod}.
  * Example:
  *
  * {@snippet lang="java" :
@@ -124,7 +125,7 @@ public interface MathTransformProvider {
             throws InvalidParameterNameException, ParameterNotFoundException,
                    InvalidParameterValueException, FactoryException
     {
-        return createMathTransform(new MathTransformProvider.Context() {
+        return createMathTransform(new Context() {
             @Override public MathTransformFactory getFactory() {
                 return (factory != null) ? factory : 
Context.super.getFactory();
             }
@@ -279,7 +280,7 @@ public interface MathTransformProvider {
          *
          * @return names of parameters inferred from context.
          */
-        default Map<String,Boolean> getContextualParameters() {
+        default Map<String, Boolean> getContextualParameters() {
             return Map.of();
         }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultTransformationTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultTransformationTest.java
index c9fdc5f560..359c0810b9 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultTransformationTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/DefaultTransformationTest.java
@@ -46,6 +46,7 @@ import static 
org.apache.sis.referencing.Assertions.assertWktEquals;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class DefaultTransformationTest extends TestCase {
     /**
      * Creates a new test case.
@@ -93,7 +94,7 @@ public final class DefaultTransformationTest extends TestCase 
{
          * did not bothered to define a specialized MathTransform class for 
our case. So we will help
          * a little bit DefaultTransformation by telling it the parameters 
that we used.
          */
-        final Map<String, Object> properties = new HashMap<>(4);
+        final var properties = new HashMap<String, Object>(4);
         properties.put(DefaultTransformation.NAME_KEY, "Tokyo to JGD2000 
(GSI)");
         properties.put(DefaultTransformation.OPERATION_VERSION_KEY, "GSI-Jpn");
         properties.put(CoordinateOperations.PARAMETERS_KEY, pg);

Reply via email to