You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2024/01/19 18:15:58 UTC

(sis) branch geoapi-4.0 updated: Replace the optimization of concatenated transforms by a new API making possible to provide more context. The new API does not yet provide additional context, but it will be done after the deprecated method is removed.

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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new f382d7da47 Replace the optimization of concatenated transforms by a new API making possible to provide more context. The new API does not yet provide additional context, but it will be done after the deprecated method is removed.
f382d7da47 is described below

commit f382d7da470a571c31f1c32ae7eeb6990b081f64
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jan 19 19:04:12 2024 +0100

    Replace the optimization of concatenated transforms by a new API making possible to provide more context.
    The new API does not yet provide additional context, but it will be done after the deprecated method is removed.
    
    https://issues.apache.org/jira/browse/SIS-595
---
 .../apache/sis/coverage/grid/SliceGeometry.java    |  12 +-
 .../sis/coverage/internal/CompoundTransform.java   |  54 ++--
 .../referencing/operation/projection/Mercator.java |  63 +++--
 .../operation/projection/NormalizedProjection.java | 116 +--------
 .../transform/AbstractLinearTransform.java         |  17 --
 .../operation/transform/AbstractMathTransform.java | 243 ++++++++++++++++--
 .../operation/transform/CartesianToPolar.java      |   2 +-
 .../operation/transform/CartesianToSpherical.java  |   2 +-
 .../operation/transform/ConcatenatedTransform.java |  58 +++--
 .../transform/CoordinateSystemTransform.java       |  57 +++--
 .../transform/EllipsoidToCentricTransform.java     |  40 ++-
 .../transform/ExponentialTransform1D.java          |  65 +++--
 .../transform/LogarithmicTransform1D.java          |  53 ++--
 .../operation/transform/PassThroughTransform.java  | 274 +++++++++++----------
 .../operation/transform/PolarToCartesian.java      |   2 +-
 .../operation/transform/PowerTransform1D.java      |  30 +--
 .../operation/transform/SphericalToCartesian.java  |   2 +-
 .../operation/transform/TransformSeparator.java    |  17 +-
 .../operation/transform/UnitConversion.java        |  26 +-
 .../operation/transform/WraparoundTransform.java   |  68 ++---
 .../sis/referencing/util/CoordinateOperations.java |   2 +-
 .../referencing/util/MathTransformsOrFactory.java  | 138 -----------
 .../sis/referencing/util/ReferencingUtilities.java |  13 +
 .../transform/EllipsoidToCentricTransformTest.java |  40 +--
 .../transform/PassThroughTransformTest.java        |  73 +++---
 .../transform/TransformSeparatorTest.java          | 179 +++++++-------
 .../transform/WraparoundTransformTest.java         |  11 +-
 27 files changed, 834 insertions(+), 823 deletions(-)

diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/SliceGeometry.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/SliceGeometry.java
index a7e6865ed3..67d70bbc6c 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/SliceGeometry.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/SliceGeometry.java
@@ -33,7 +33,7 @@ import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.referencing.util.DirectPositionView;
-import org.apache.sis.referencing.util.MathTransformsOrFactory;
+import org.apache.sis.referencing.util.ReferencingUtilities;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.internal.Numerics;
@@ -77,7 +77,7 @@ final class SliceGeometry implements Function<RenderedImage, GridGeometry> {
     private int[] crsDimensions;
 
     /**
-     * Factory to use for creating new transforms, or {@code null} for default.
+     * Factory to use for creating new transforms.
      */
     private final MathTransformFactory factory;
 
@@ -87,6 +87,7 @@ final class SliceGeometry implements Function<RenderedImage, GridGeometry> {
      * @param  geometry        the grid geometry for which the transform is desired.
      * @param  sliceExtent     the requested extent, or {@code null} for the whole coverage.
      * @param  gridDimensions  the grid (not CRS) dimensions to select, in strictly increasing order.
+     * @param  factory         factory to use for creating new transforms, or {@code null} for default.
      */
     SliceGeometry(final GridGeometry geometry, final GridExtent sliceExtent,
                   final int[] gridDimensions, final MathTransformFactory factory)
@@ -94,7 +95,7 @@ final class SliceGeometry implements Function<RenderedImage, GridGeometry> {
         this.geometry       = geometry;
         this.sliceExtent    = sliceExtent;
         this.gridDimensions = gridDimensions;
-        this.factory        = factory;
+        this.factory        = ReferencingUtilities.nonNull(factory);
     }
 
     /**
@@ -249,10 +250,9 @@ final class SliceGeometry implements Function<RenderedImage, GridGeometry> {
                     offset[i] = extent.getLow(gridDimensions[i]);
                 }
                 final LinearTransform translation = MathTransforms.translation(offset);
-                final MathTransformsOrFactory f = MathTransformsOrFactory.wrap(factory);
                 if (gridToCRS != null) {
-                    gridToCRS   = f.concatenate(translation, gridToCRS);
-                    cornerToCRS = f.concatenate(translation, cornerToCRS);
+                    gridToCRS   = factory.createConcatenatedTransform(translation, gridToCRS);
+                    cornerToCRS = factory.createConcatenatedTransform(translation, cornerToCRS);
                 }
             }
             extent = relativeExtent;
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/internal/CompoundTransform.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/internal/CompoundTransform.java
index 958b714016..90db3ba661 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/internal/CompoundTransform.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/internal/CompoundTransform.java
@@ -19,7 +19,6 @@ package org.apache.sis.coverage.internal;
 import java.util.Arrays;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
-import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.referencing.operation.transform.AbstractMathTransform;
@@ -163,34 +162,37 @@ public abstract class CompoundTransform extends AbstractMathTransform {
      * Concatenates or pre-concatenates in an optimized way this math transform with the given one, if possible.
      */
     @Override
-    protected final MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
-                                                 final MathTransformFactory factory) throws FactoryException
-    {
-build:  if (other instanceof CompoundTransform) {
-            final MathTransform[] components = components();
-            final MathTransform[] toConcatenate = ((CompoundTransform) other).components();
-            final int n = components.length;
-            if (toConcatenate.length == n) {
-                final MathTransform[] concatenated = new MathTransform1D[n];
-                for (int i=0; i<n; i++) {
-                    MathTransform c1 = components[i];
-                    MathTransform c2 = toConcatenate[i];
-                    if (applyOtherFirst) {
-                        c1 = c2;
-                        c2 = components[i];
-                    }
-                    if (c1.getTargetDimensions() != c2.getSourceDimensions()) {
-                        /*
-                         * TODO: if c1 or c2 are linear transforms, we could take sub-matrices.
-                         */
-                        break build;
+    protected final void tryConcatenate(final Joiner context) throws FactoryException {
+        int relativeIndex = +1;
+search: do {
+            final MathTransform other = context.getTransform(relativeIndex).orElse(null);
+            if (other instanceof CompoundTransform) {
+                final MathTransform[] components = components();
+                final MathTransform[] toConcatenate = ((CompoundTransform) other).components();
+                final int n = components.length;
+                if (toConcatenate.length == n) {
+                    final MathTransform[] concatenated = new MathTransform1D[n];
+                    for (int i=0; i<n; i++) {
+                        MathTransform c1 = components[i];
+                        MathTransform c2 = toConcatenate[i];
+                        if (relativeIndex < 0) {
+                            c1 = c2;
+                            c2 = components[i];
+                        }
+                        if (c1.getTargetDimensions() != c2.getSourceDimensions()) {
+                            /*
+                             * TODO: if c1 or c2 are linear transforms, we could take sub-matrices.
+                             */
+                            continue search;
+                        }
+                        concatenated[i] = context.factory.createConcatenatedTransform(c1, c2);
                     }
-                    concatenated[i] = factory.createConcatenatedTransform(c1, c2);
+                    context.replace(relativeIndex, create(concatenated));
+                    return;
                 }
-                return create(concatenated);
             }
-        }
-        return super.tryConcatenate(applyOtherFirst, other, factory);
+        } while ((relativeIndex = -relativeIndex) < 0);
+        super.tryConcatenate(context);
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
index 3e1240a20c..27724059bc 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
@@ -40,7 +40,7 @@ import org.apache.sis.util.Workaround;
 import org.apache.sis.util.internal.DoubleDouble;
 import org.apache.sis.referencing.operation.matrix.Matrix2;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.DomainDefinition;
 import org.apache.sis.referencing.operation.transform.ContextualParameters;
 import org.apache.sis.parameter.Parameters;
@@ -602,27 +602,52 @@ subst:  if (variant.spherical || eccentricity == 0) {
     }
 
     /**
-     * Invoked when {@link #tryConcatenate(boolean, MathTransform, MathTransformFactory)} detected a
-     * (inverse) → (affine) → (this) transforms sequence. If the affine transform in the middle does
-     * not change the latitude value, then we can take advantage of the fact that longitude conversion
-     * is linear.
+     * Concatenates in an optimized way the reverse projection with its neighbor, if possible.
+     * This optimization is symmetrical to the optimization done for the forward projection case.
+     *
+     * @param  context  information about the neighbor transforms, and the object where to set the result.
+     * @throws FactoryException if an error occurred while combining the transforms.
+     * @return whether or not an optimization has been done.
      */
     @Override
-    final MathTransform tryConcatenate(boolean projectedSpace, Matrix affine, MathTransformFactory factory)
-            throws FactoryException
-    {
-        /*
-         * Verify that the latitude row is an identity conversion except for the sign which is allowed to change
-         * (but no scale and no translation are allowed).  Ignore the longitude row because it just pass through
-         * this Mercator projection with no impact on any calculation.
-         */
-        if (affine.getElement(1,0) == 0 && affine.getElement(1, DIMENSION) == 0 && Math.abs(affine.getElement(1,1)) == 1) {
-            if (factory != null) {
-                return factory.createAffineTransform(affine);
-            } else {
-                return MathTransforms.linear(affine);
+    final boolean tryInverseConcatenate(Joiner context) throws FactoryException {
+        int relativeIndex = +1;
+        do {
+            final Matrix m = context.getMiddleMatrix(relativeIndex).orElse(null);
+            /*
+             * Verify that the latitude row is an identity conversion except for the sign which is allowed to change
+             * (but no scale and no translation are allowed). Ignore the longitude row because it just passes through
+             * this Mercator projection with no impact on any calculation.
+             */
+            if (m != null
+                    && Matrices.isAffine(m)
+                    && m.getNumRow() == DIMENSION+1
+                    && m.getNumCol() == DIMENSION+1
+                    && m.getElement(1, 0) == 0
+                    && m.getElement(1, DIMENSION) == 0
+                    && Math.abs(m.getElement(1,1)) == 1)
+            {
+                context.replace(relativeIndex, context.factory.createAffineTransform(m));
+                return true;
             }
+        } while ((relativeIndex = -relativeIndex) < 0);
+        return false;
+    }
+
+    /**
+     * Concatenates in an optimized way this projection with its neighbor, if possible.
+     * If the transforms are concatenated in a (projection) → (affine) → (reverse projection) sequence
+     * where the (projection) and (reverse projection) steps are the {@linkplain #inverse() inverse}
+     * of each other, and if the affine transform in the middle does not change the latitude value,
+     * then we can take advantage of the fact that longitude conversion is linear.
+     *
+     * @param  context  information about the neighbor transforms, and the object where to set the result.
+     * @throws FactoryException if an error occurred while combining the transforms.
+     */
+    @Override
+    protected final void tryConcatenate(final Joiner context) throws FactoryException {
+        if (!tryInverseConcatenate(context)) {
+            super.tryConcatenate(context);
         }
-        return super.tryConcatenate(projectedSpace, affine, factory);
     }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
index 2a68e489e1..afb6c2f8c9 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
@@ -17,12 +17,10 @@
 package org.apache.sis.referencing.operation.projection;
 
 import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Objects;
 import java.util.regex.Pattern;
-import java.util.logging.Logger;
 import java.io.Serializable;
 import java.lang.reflect.Modifier;
 import static java.lang.Math.*;
@@ -36,7 +34,6 @@ import org.opengis.referencing.operation.SingleOperation;
 import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.MathTransformFactory;
-import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.CharSequences;
@@ -46,8 +43,6 @@ import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.metadata.iso.citation.Citations;
 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.AbstractMathTransform;
 import org.apache.sis.referencing.operation.transform.AbstractMathTransform2D;
 import org.apache.sis.referencing.operation.transform.ContextualParameters;
 import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
@@ -57,11 +52,9 @@ import org.apache.sis.referencing.operation.provider.MapProjection;
 import org.apache.sis.referencing.util.CoordinateOperations;
 import org.apache.sis.referencing.util.Formulas;
 import org.apache.sis.system.Modules;
-import org.apache.sis.system.Loggers;
 import org.apache.sis.util.internal.Constants;
 import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.logging.Logging;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.metadata.Identifier;
@@ -890,110 +883,27 @@ public abstract class NormalizedProjection extends AbstractMathTransform2D imple
         }
 
         /**
-         * Concatenates or pre-concatenates in an optimized way this projection with the given transform, if possible.
-         * If no optimization is available, returns {@code null}.
+         * Concatenates in an optimized way this reverse projection with its neighbor, if possible.
+         * This method delegates to {@link #tryInverseConcatenate(Joiner)}.
          */
         @Override
-        protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
-                final MathTransformFactory factory) throws FactoryException
-        {
-            final Matrix m = getMiddleMatrix(this, other, applyOtherFirst);
-            if (m != null) {
-                /*
-                 * 'projectedSpace' values:
-                 *   - false if applyOtherFirst == false since we have (reverse projection) → (affine) → (projection).
-                 *   - true  if applyOtherFirst == true  since we have (projection) → (affine) → (reverse projection).
-                 */
-                return forward.tryConcatenate(applyOtherFirst, m, factory);
+        protected void tryConcatenate(final Joiner context) throws FactoryException {
+            if (!forward.tryInverseConcatenate(context)) {
+                super.tryConcatenate(context);
             }
-            return super.tryConcatenate(applyOtherFirst, other, factory);
         }
     }
 
     /**
-     * Concatenates or pre-concatenates in an optimized way this projection with the given transform, if possible.
-     * If transforms are concatenated in a (reverse projection) → (affine) → (projection) sequence where the
-     * (projection) and (reverse projection) steps are the {@linkplain #inverse() inverse} of each other,
-     * then in some particular case the sequence can be replaced by a single affine transform.
-     * If no such simplification is possible, this method returns {@code null}.
+     * Concatenates in an optimized way this projection with its neighbor, if possible.
+     * This method returns whether or not an optimization has been done.
      *
-     * @return the simplified (usually affine) transform, or {@code null} if no such optimization is available.
+     * @param  context  information about the neighbor transforms, and the object where to set the result.
      * @throws FactoryException if an error occurred while combining the transforms.
+     * @return whether or not an optimization has been done.
      */
-    @Override
-    protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
-            final MathTransformFactory factory) throws FactoryException
-    {
-        final Matrix m = getMiddleMatrix(this, other, applyOtherFirst);
-        if (m != null) {
-            /*
-             * 'projectedSpace' values:
-             *   - false if applyOtherFirst == true  since we have (reverse projection) → (affine) → (projection).
-             *   - true  if applyOtherFirst == false since we have (projection) → (affine) → (reverse projection).
-             */
-            return tryConcatenate(!applyOtherFirst, m, factory);
-        }
-        return super.tryConcatenate(applyOtherFirst, other, factory);
-    }
-
-    /**
-     * Returns the concatenation of (inverse) → (affine) → (this) transforms, or {@code null} if none.
-     * The affine transform is applied in the geographic space with angular values in radians.
-     *
-     * <p>Above description is for the usual case where {@code projectedSpace} is {@code false}.
-     * If {@code true} (should be unusual), then the affine transform is rather applied in the
-     * projected space and the sequence to concatenate is (this) → (affine) → (inverse).</p>
-     *
-     * <p>Default implementation returns {@code null}. Subclasses should override if applicable.</p>
-     *
-     * @param  projectedSpace  {@code true} if affine transform is applied in projected instead of geographic space.
-     * @param  affine          the affine transform in the middle of (inverse) → (affine) → (this) transform sequence.
-     * @param  factory         the factory to use for creating new transform, or {@code null}.
-     * @return the optimized concatenation, or {@code null} if none.
-     */
-    MathTransform tryConcatenate(boolean projectedSpace, Matrix affine, MathTransformFactory factory)
-            throws FactoryException
-    {
-        return null;
-    }
-
-    /**
-     * If a sequence of 3 transforms are (reverse projection) → (affine) → (projection) where
-     * the (projection) and (reverse projection) steps are the inverse of each other, returns
-     * the matrix of the affine transform step. Otherwise returns {@code null}. This method
-     * accepts also (projection) → (affine) → (reverse projection) sequence, but such sequences
-     * should be much more unusual.
-     *
-     * @param  projection       either {@link NormalizedProjection} or {@link Inverse}.
-     * @param  other            the arbitrary transforms to be concatenated with the given projection.
-     * @param  applyOtherFirst  whether {@code other} is concatenated before {@code projection} or the converse.
-     * @return the 3×3 matrix of the affine transform step, or {@code null} if none.
-     */
-    private static Matrix getMiddleMatrix(final AbstractMathTransform projection, final MathTransform other,
-            final boolean applyOtherFirst)
-    {
-        final List<MathTransform> steps = MathTransforms.getSteps(other);
-        if (steps.size() == 2) {
-            final int oi = applyOtherFirst ? 0 : 1;
-            final MathTransform inverse = steps.get(oi);
-            if (inverse instanceof Inverse || inverse instanceof NormalizedProjection) try {
-                /*
-                 * In principle the equality check below should be sufficient. But above `instanceof` checks
-                 * are added for avoiding potentially costly calls to `inverse()` in other implementations.
-                 */
-                if (projection.equals(inverse.inverse(), ComparisonMode.IGNORE_METADATA)) {
-                    final Matrix m = MathTransforms.getMatrix(steps.get(oi ^ 1));
-                    if (Matrices.isAffine(m) && m.getNumRow() == DIMENSION+1 && m.getNumCol() == DIMENSION+1) {
-                        return m;
-                    }
-                }
-            } catch (NoninvertibleTransformException e) {
-                final Logger LOGGER = Logger.getLogger(Loggers.COORDINATE_OPERATION);   // Defined in base class but field is inaccessible.
-                final Class<?> caller = (projection instanceof NormalizedProjection) ? NormalizedProjection.class : projection.getClass();
-                Logging.recoverableException(LOGGER, caller, "tryConcatenate", e);
-            }
-        }
-        return null;
+    boolean tryInverseConcatenate(Joiner context) throws FactoryException {
+        return false;
     }
 
     /**
@@ -1053,8 +963,8 @@ public abstract class NormalizedProjection extends AbstractMathTransform2D imple
             }
             case IGNORE_METADATA: {
                 /*
-                 * There is no need to compare both 'eccentricity' and 'eccentricitySquared' since the former
-                 * is computed from the latter. We are better to compare 'eccentricitySquared' since it is the
+                 * There is no need to compare both `eccentricity` and `eccentricitySquared` since the former
+                 * is computed from the latter. We are better to compare `eccentricitySquared` since it is the
                  * original value from which the other value is derived.
                  */
                 if (!Numerics.equals(eccentricitySquared, that.eccentricitySquared)) {
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
index fcaba2d4c7..5661b3f1b1 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
@@ -21,10 +21,7 @@ import java.io.Serializable;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.referencing.operation.Matrix;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
-import org.opengis.util.FactoryException;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.provider.Affine;
 import org.apache.sis.referencing.internal.Resources;
@@ -129,20 +126,6 @@ abstract class AbstractLinearTransform extends AbstractMathTransform implements
         return getSourceDimensions() + 1;
     }
 
-    /**
-     * Returns an identity transform if this transform is the inverse of the given transform.
-     * If this method is unsure, it conservatively returns {@code null}.
-     */
-    @Override
-    protected final MathTransform tryConcatenate(boolean applyOtherFirst, MathTransform other, MathTransformFactory factory)
-            throws FactoryException
-    {
-        if (other instanceof LinearTransform) {
-            return super.tryConcatenate(applyOtherFirst, other, factory);
-        }
-        return null;        // No need to compute the inverse if the other transform is not linear.
-    }
-
     /**
      * Returns the inverse transform of this object.
      * This method invokes {@link #createInverse()} when first needed, then caches the result.
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
index fa70064ec1..6ea6e41f49 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.Optional;
 import java.util.logging.Logger;
+import java.awt.geom.AffineTransform;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
@@ -38,6 +39,7 @@ import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.referencing.internal.Resources;
+import org.apache.sis.referencing.util.ReferencingUtilities;
 import org.apache.sis.referencing.util.WKTUtilities;
 import org.apache.sis.referencing.util.WKTKeywords;
 import org.apache.sis.system.Loggers;
@@ -46,7 +48,7 @@ import org.apache.sis.util.Utilities;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.LenientComparable;
 import org.apache.sis.util.resources.Errors;
-import static org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
+import org.apache.sis.util.logging.Logging;
 
 
 /**
@@ -82,7 +84,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
  * running the same SIS version.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see DefaultMathTransformFactory
  * @see org.apache.sis.referencing.operation.AbstractCoordinateOperation
@@ -308,9 +310,9 @@ public abstract class AbstractMathTransform extends FormattableObject
     public DirectPosition transform(final DirectPosition ptSrc, DirectPosition ptDst) throws TransformException {
         final int dimSource = getSourceDimensions();
         final int dimTarget = getTargetDimensions();
-        ensureDimensionMatches("ptSrc", dimSource, ptSrc);
+        ArgumentChecks.ensureDimensionMatches("ptSrc", dimSource, ptSrc);
         if (ptDst != null) {
-            ensureDimensionMatches("ptDst", dimTarget, ptDst);
+            ArgumentChecks.ensureDimensionMatches("ptDst", dimTarget, ptDst);
             /*
              * Transforms the coordinates using a temporary 'double[]' buffer,
              * and copies the transformation result in the destination position.
@@ -857,6 +859,7 @@ public abstract class AbstractMathTransform extends FormattableObject
         try {
             tr1 = tr1.inverse();
         } catch (NoninvertibleTransformException e) {
+            Logging.recoverableException(LOGGER, AbstractMathTransform.class, "isInverseEquals", e);
             return false;
         }
         if (tr1 instanceof LenientComparable) {
@@ -883,14 +886,6 @@ public abstract class AbstractMathTransform extends FormattableObject
      * </ul>
      *
      * If no optimization is available for the combined transform, then this method returns {@code null}.
-     * In the latter case, the concatenation will be prepared by {@link DefaultMathTransformFactory} using
-     * a generic implementation.
-     *
-     * <p>The default implementation returns the identity transform if the other transform is the inverse
-     * of this transform, or returns {@code null} otherwise. This method is ought to be overridden
-     * by subclasses capable of concatenating some combination of transforms in a special way.
-     * {@link LinearTransform} implementations do not need to override this method because matrix multiplications
-     * will be handled automatically. This method does not need to handle the {@link #isIdentity()} case neither.</p>
      *
      * @param  applyOtherFirst  {@code true} if the transformation order is {@code other} followed by {@code this}, or
      *                          {@code false} if the transformation order is {@code this} followed by {@code other}.
@@ -899,17 +894,231 @@ public abstract class AbstractMathTransform extends FormattableObject
      * @return the math transforms combined in an optimized way, or {@code null} if no such optimization is available.
      * @throws FactoryException if an error occurred while combining the transforms.
      *
-     * @see DefaultMathTransformFactory#createConcatenatedTransform(MathTransform, MathTransform)
-     *
      * @since 0.8
+     *
+     * @deprecated Replaced by {@link #tryConcatenate(Joiner)}.
+     *             See <a href="https://issues.apache.org/jira/browse/SIS-595">SIS-595</a>.
      */
+    @Deprecated(forRemoval=true, since="1.5")
     protected MathTransform tryConcatenate(boolean applyOtherFirst, MathTransform other, MathTransformFactory factory)
             throws FactoryException
     {
-        if (isInverseEquals(this, other)) {
-            return MathTransforms.identity(applyOtherFirst ? getTargetDimensions() : getSourceDimensions());
+        MathTransform first = this;
+        if (applyOtherFirst) {
+            first = other;
+            other = this;
+        }
+        final var context = new Joiner(List.of(first, other), applyOtherFirst ? 1 : 0, ReferencingUtilities.nonNull(factory));
+        tryConcatenate(context);
+        return context.replacement;
+    }
+
+    /**
+     * Concatenates or pre-concatenates in an optimized way this math transform with its neighbor, if possible.
+     * If an optimization is possible, a new math transform is created to perform the combined transformation.
+     * If no optimization is available, the concatenation will be prepared by {@link DefaultMathTransformFactory}
+     * using a generic implementation.
+     *
+     * <p>The default implementation checks if a neighbor transform is the inverse of this transform. This method is
+     * ought to be overridden by subclasses capable of concatenating some combination of transforms in a special way.
+     * {@link LinearTransform} implementations do not need to override this method because matrix multiplications
+     * will be handled automatically. This method does not need to handle the {@link #isIdentity()} case neither.</p>
+     *
+     * @param  context  information about the neighbor transforms, and the object where to set the result.
+     * @throws FactoryException if an error occurred while combining the transforms.
+     *
+     * @see DefaultMathTransformFactory#createConcatenatedTransform(MathTransform, MathTransform)
+     *
+     * @since 1.5
+     */
+    protected void tryConcatenate(final Joiner context) throws FactoryException {
+        int relativeIndex = +1;
+        do {
+            final MathTransform other = context.getTransform(relativeIndex).orElse(null);
+            if (other != null && isInverseEquals(this, other)) {
+                int dimension = (relativeIndex < 0) ? getTargetDimensions() : getSourceDimensions();
+                context.replace(relativeIndex, MathTransforms.identity(dimension));
+                break;
+            }
+        } while ((relativeIndex = -relativeIndex) < 0);
+    }
+
+    /**
+     * Information about how to concatenate the enclosing transform with the previous or next transforms.
+     * Those information give an opportunity for {@link AbstractMathTransform} implementations to replace
+     * the default concatenation algorithm by an optimized alternative. Examples of optimizations may be:
+     * detecting when two consecutive non-linear operations are equivalent to a third non-linear operation,
+     * or rewriting and moving linear transforms before or after the enclosing transform (such moves can
+     * cause two linear transforms to become consecutive, which enable their efficient merging).
+     *
+     * <h2>Usage</h2>
+     * Implementations can inspect the surrounding transforms by calls to {@link #getTransform(int)} or
+     * {@link #isLinear(int, boolean)}. If they decide to replace the enclosing transform and a neighbor
+     * transform by a single optimized transform, they can call {@link #replace(int, MathTransform)}.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.5
+     *
+     * @see #tryConcatenate(Joiner)
+     *
+     * @since 1.5
+     */
+    protected static final class Joiner {
+        /**
+         * Partial or complete list of coordinate operations to concatenate.
+         * This list may contain only the neighbor steps.
+         */
+        private final List<MathTransform> steps;
+
+        /**
+         * Index of the enclosing transform in the {@link #steps} list.
+         */
+        private final int indexOfThis;
+
+        /**
+         * The factory to use for creating the concatenated transform. Never null.
+         */
+        public final MathTransformFactory factory;
+
+        /**
+         * The concatenated transform to use as a replacement, or {@code null} if none.
+         */
+        MathTransform replacement;
+
+        /**
+         * -1 if {@link #replacement} replaces also the previous transform, or
+         * +1 if it replaces also the next transform.
+         */
+        int replacementBound;
+
+        /**
+         * Creates new concatenation information.
+         *
+         * @param steps        partial or complete list of coordinate operations to concatenate.
+         * @param indexOfThis  index of the enclosing transform in the {@code steps} list.
+         * @param factory      the factory to use for creating the concatenated transform.
+         */
+        Joiner(final List<MathTransform> steps, final int indexOfThis, final MathTransformFactory factory) {
+            this.factory     = factory;
+            this.steps       = steps;
+            this.indexOfThis = indexOfThis;
+        }
+
+        /**
+         * Returns the transform before or after the enclosing transform. By definition, invoking this method
+         * with a relative index of 0 always returns the enclosing transform. Invoking this method with other
+         * values may or may not return a transform, because {@code Joiner} may have information about only
+         * a subset of the transform steps. If present, a relative index of -1 returns the previous transform
+         * while a relative index of +1 returns the next transform.
+         *
+         * @param  relativeIndex  index of the transform to get, relatively to the enclosing transform.
+         * @return the requested transform. An empty value does not necessarily mean that the transform does not exist.
+         */
+        public Optional<MathTransform> getTransform(int relativeIndex) {
+            relativeIndex += indexOfThis;
+            if (relativeIndex >= 0 && relativeIndex < steps.size()) {
+                return Optional.of(steps.get(relativeIndex));
+            } else {
+                return Optional.empty();
+            }
+        }
+
+        /**
+         * If a neighbor transform is linear and located between a transform and its inverse, returns it as a matrix.
+         * This method checks for a sequence of <var>forward</var> → <var>affine</var> → <var>reverse</var> transforms,
+         * where the <var>forward</var> and <var>reverse</var> steps are the inverse of each other.
+         * If such sequence is found, the matrix of the <var>affine</var> step is returned.
+         *
+         * @param  relativeIndex  relative index of the affine step.
+         * @return the matrix of the affine transform step if it is located
+         *         between transforms that are the inverse of each other.
+         */
+        public Optional<Matrix> getMiddleMatrix(final int relativeIndex) {
+            if (Math.abs(relativeIndex) == 1) {
+                final MathTransform affine = getTransform(relativeIndex).orElse(null);
+                final List<MathTransform> subSteps = MathTransforms.getSteps(affine);
+                if (subSteps.size() == 2) {
+                    final int oi = Math.max(0, relativeIndex);
+                    final MathTransform inverse = subSteps.get(oi);
+                    if (isInverseEquals(steps.get(indexOfThis), inverse)) {
+                        return Optional.ofNullable(MathTransforms.getMatrix(subSteps.get(oi ^ 1)));
+                    }
+                }
+            }
+            return Optional.empty();
+        }
+
+        /**
+         * Tests whether the transform before or after the enclosing transform is linear.
+         * Invoking this method is equivalent to invoking {@link #getTransform(int)} and testing whether
+         * {@link MathTransforms#getMatrix(MathTransform)} returns a non-null value for that transform.
+         *
+         * @param  relativeIndex  index of the transform to test, relatively to the enclosing transform.
+         * @param  ifUnknown      value to return if the math transform at the given index is unknown.
+         * @return whether the transform is linear, or {@code ifNone} if unknown.
+         */
+        public boolean isLinear(int relativeIndex, boolean ifUnknown) {
+            relativeIndex += indexOfThis;
+            if (relativeIndex < 0 || relativeIndex >= steps.size()) {
+                return ifUnknown;
+            }
+            MathTransform step = steps.get(relativeIndex);
+            return (step instanceof LinearTransform) || (step instanceof AffineTransform);
+        }
+
+        /**
+         * Concatenates the two given transforms, swapping their order if {@code swap} is {@code true}.
+         *
+         * @param  first   the first math transform.
+         * @param  second  the second math transform.
+         * @param  swap    whether the given transforms should be swapped.
+         * @return the concatenated transform.
+         * @throws FactoryException if the factory cannot perform the concatenation.
+         */
+        MathTransform concatenate(MathTransform first, MathTransform second, boolean swap) throws FactoryException {
+            if (swap) {
+                return factory.createConcatenatedTransform(second, first);
+            } else {
+                return factory.createConcatenatedTransform(first, second);
+            }
+        }
+
+        /**
+         * Requests to replace the enclosing transform and neighbor transforms by the given transform.
+         * The {@code bound} argument specifies which neighbors are replaced by the specified transform:
+         *
+         * <ul class="verbose">
+         *   <li>If -1, then transforming a point <var>p</var> by {@code concatenation} shall be equivalent
+         *       to first transforming <var>p</var> by {@code getTransform(-1)}, and then transforming the
+         *       result by {@code getTransform(0)}.</li>
+         *   <li>If +1, then transforming a point <var>p</var> by {@code concatenation} shall be equivalent
+         *       to first transforming <var>p</var> by {@code getTransform(0)}, and then transforming the
+         *       result by {@code getTransform(+1)}.</li>
+         *   <li>If 0, then only the enclosing transform is replaced. Neighbor are unchanged.</li>
+         * </ul>
+         *
+         * This method can be invoked only once per {@code Joiner} instance.
+         *
+         * @param  concatenation  the transform to use instead of the enclosing and neighbor ones.
+         * @throws IllegalStateException if a {@code replace(…)} method has already been invoked.
+         */
+        public void replace(final int bound, final MathTransform concatenation) {
+            if (replaced()) {
+                throw new IllegalStateException(Errors.format(Errors.Keys.AlreadyInitialized_1, "concatenation"));
+            }
+            ArgumentChecks.ensureNonNull("concatenation", concatenation);
+            ArgumentChecks.ensureBetween("bound", -1, +1, bound);
+            replacement = concatenation;
+            replacementBound = bound;
+        }
+
+        /**
+         * {@return whether a concatenated transform has been specified}.
+         * This is initially {@code false}, and become {@code true} after a {@code replace(…)} call.
+         */
+        public boolean replaced() {
+            return replacement != null;
         }
-        return null;
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CartesianToPolar.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CartesianToPolar.java
index 22b40110d4..217bb57cfe 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CartesianToPolar.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CartesianToPolar.java
@@ -65,7 +65,7 @@ final class CartesianToPolar extends CoordinateSystemTransform implements Serial
      * Output coordinates are in radians.
      */
     private CartesianToPolar() {
-        super("Cartesian to polar", "Cartesian to cylindrical", 2, new int[1], false);
+        super("Cartesian to polar", "Cartesian to cylindrical", 2, new int[1], (byte) -1);
         context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION)
                .convertAfter(1, DoubleDouble.RADIANS_TO_DEGREES, null);
     }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CartesianToSpherical.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CartesianToSpherical.java
index 74a5c420aa..70a734b869 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CartesianToSpherical.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CartesianToSpherical.java
@@ -58,7 +58,7 @@ final class CartesianToSpherical extends CoordinateSystemTransform implements Se
      * Output coordinates are in radians.
      */
     private CartesianToSpherical() {
-        super("Cartesian to spherical", null, 3, new int[] {2}, false);
+        super("Cartesian to spherical", null, 3, new int[] {2}, (byte) -1);
         context.denormalizeGeographicOutputs(0);                // Convert (θ,Ω) from radians to degrees.
     }
 
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
index 2ab1a48ce9..16b3cfc771 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
@@ -38,6 +38,7 @@ import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.provider.GeocentricAffine;
 import org.apache.sis.referencing.util.WKTKeywords;
 import org.apache.sis.referencing.internal.Resources;
+import org.apache.sis.referencing.util.ReferencingUtilities;
 import org.apache.sis.system.Semaphores;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.ComparisonMode;
@@ -60,7 +61,7 @@ import org.apache.sis.util.resources.Errors;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  *
- * @see org.opengis.referencing.operation.MathTransformFactory#createConcatenatedTransform(MathTransform, MathTransform)
+ * @see MathTransformFactory#createConcatenatedTransform(MathTransform, MathTransform)
  */
 class ConcatenatedTransform extends AbstractMathTransform implements Serializable {
     /**
@@ -99,8 +100,7 @@ class ConcatenatedTransform extends AbstractMathTransform implements Serializabl
 
     /**
      * Constructs a concatenated transform. This constructor is for subclasses only.
-     * To create a concatenated transform, use the {@link #create(MathTransform, MathTransform, MathTransformFactory)}
-     * factory method instead.
+     * To create a concatenated transform, use the {@link #create create(…)} method instead.
      *
      * @param  transform1  the first math transform.
      * @param  transform2  the second math transform.
@@ -190,7 +190,7 @@ class ConcatenatedTransform extends AbstractMathTransform implements Serializabl
      * @param  factory  the factory which is (indirectly) invoking this method, or {@code null} if none.
      */
     private static MathTransform tryOptimized(final MathTransform tr1, final MathTransform tr2,
-            final MathTransformFactory factory) throws FactoryException
+                                              MathTransformFactory factory) throws FactoryException
     {
         /*
          * Trivial - but actually essential!! - check for the identity cases.
@@ -208,6 +208,7 @@ class ConcatenatedTransform extends AbstractMathTransform implements Serializabl
         MathTransform shortest = null;
         boolean inverseCaseTested = false;
         if (tr1 instanceof AbstractMathTransform) {
+            // TODO: after removal of the deprecated method, invoke `tryConcatenate(Joiner)` only once.
             final MathTransform optimized = ((AbstractMathTransform) tr1).tryConcatenate(false, tr2, factory);
             inverseCaseTested = true;
             if (optimized != null) {
@@ -242,6 +243,7 @@ class ConcatenatedTransform extends AbstractMathTransform implements Serializabl
          * If both transforms use matrix, then we can create
          * a single transform using the concatenated matrix.
          */
+        factory = ReferencingUtilities.nonNull(factory);
         final MathTransform concatenated = multiply(tr1, tr2, factory);
         if (concatenated instanceof AbstractLinearTransform) {
             /*
@@ -282,7 +284,7 @@ class ConcatenatedTransform extends AbstractMathTransform implements Serializabl
      * Returns a transform resulting from the multiplication of the matrices of given transforms.
      * If the given transforms does not provide matrix, then this method returns {@code null}.
      *
-     * @param  factory  the factory which is (indirectly) invoking this method, or {@code null} if none.
+     * @param  factory  wrapper for the factory which is (indirectly) invoking this method.
      */
     private static MathTransform multiply(final MathTransform tr1, final MathTransform tr2,
             final MathTransformFactory factory) throws FactoryException
@@ -302,11 +304,7 @@ class ConcatenatedTransform extends AbstractMathTransform implements Serializabl
                  * Apache SIS performs matrix operations using double-double arithmetic in the hope to get exact
                  * results at the `double` accuracy, which avoid the need for a tolerance threshold.
                  */
-                if (factory != null) {
-                    return factory.createAffineTransform(matrix);
-                } else {
-                    return MathTransforms.linear(matrix);
-                }
+                return factory.createAffineTransform(matrix);
             }
         }
         // No optimized case found.
@@ -911,29 +909,35 @@ class ConcatenatedTransform extends AbstractMathTransform implements Serializabl
      *   Instead of : tr1 → tr2 → other
      *   Try:         tr1 → (tr2 → other)          where (…) denote an optimized concatenation.</pre>
      *
-     * @return the simplified transform, or {@code null} if no such optimization is available.
      * @throws FactoryException if an error occurred while combining the transforms.
      */
     @Override
-    protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other, final MathTransformFactory factory)
-            throws FactoryException
-    {
-        if (applyOtherFirst) {
-            final MathTransform candidate = tryOptimized(other, transform1, factory);
-            if (candidate != null) {
-                return create(candidate, transform2, factory);
-            }
-        } else {
-            final MathTransform candidate = tryOptimized(transform2, other, factory);
-            if (candidate != null) {
-                return create(transform1, candidate, factory);
+    protected void tryConcatenate(final Joiner context) throws FactoryException {
+        int relativeIndex = +1;
+        do {
+            MathTransform step1 = transform2;     // In the first iteration, we will try to concatenate after this.
+            MathTransform step2 = context.getTransform(relativeIndex).orElse(null);
+            if (step2 != null) {
+                if (relativeIndex < 0) {
+                    step1 = step2;
+                    step2 = transform1;           // In the second iteration, we will try to concatenate before this.
+                }
+                step2 = tryOptimized(step1, step2, context.factory);
+                if (step2 != null) {
+                    step1 = transform1;           // In the first iteration, keep this and replace after.
+                    if (relativeIndex < 0) {
+                        step1 = step2;
+                        step2 = transform2;       // In the second iteration, keep this and replace before.
+                    }
+                    context.replace(relativeIndex, create(step1, step2, context.factory));
+                    return;
+                }
             }
-        }
+        } while ((relativeIndex = -relativeIndex) < 0);
         /*
-         * Do not invoke `super.tryConcatenate(applyOtherFirst, other, factory)`; the test of whether
-         * `this` is the inverse of `other` has been done indirectly by the calls to `tryOptimized(…)`.
+         * Do not invoke `super.tryConcatenate(context)`. The test of whether `this` is the
+         * inverse of `other` has been done indirectly by the calls to `tryOptimized(…)`.
          */
-        return null;
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransform.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransform.java
index 4d471062cd..5858add515 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransform.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransform.java
@@ -42,7 +42,6 @@ import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.referencing.util.WKTUtilities;
 import org.apache.sis.referencing.util.CoordinateOperations;
 import org.apache.sis.referencing.util.ExtendedPrecisionMatrix;
-import org.apache.sis.referencing.util.MathTransformsOrFactory;
 import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.util.internal.Constants;
@@ -64,19 +63,24 @@ abstract class CoordinateSystemTransform extends AbstractMathTransform {
     private final int dimension;
 
     /**
-     * Index of dimensions having linear units in the coordinate system identified by {@link #angularIsInput}.
+     * Index of dimensions having linear units in the coordinate system identified by {@code -linearTransformPosition}.
      * This is used for optimizing the concatenation of this transform by an affine transform.
      *
-     * @see #tryConcatenate(boolean, MathTransform, MathTransformFactory)
+     * @see #tryConcatenate(Joiner)
      */
     private final int[] linearDimensions;
 
     /**
-     * Whether the coordinate system having some angular units is the input coordinate system.
-     * If {@code false}, then the coordinate system having some angular units is the output CS.
-     * That coordinate system may also have linear units identified by {@link #linearDimensions}.
+     * Relative index of the affine transform doing conversion of linear coordinates.
+     * If the inputs of this transform are linear coordinate values in a Cartesian coordinate system,
+     * then this field shall be -1. If the above-cited linear coordinates are rather in the outputs,
+     * then this field shall be +1.
+     *
+     * <p>The transform at the relative index {@code -linearTransformPosition} should be a mix
+     * of angular and linear coordinate values. The coordinates at the positions identified by
+     * {@link #linearDimensions} are linear values, and all other coordinates are angular values.
      */
-    private final boolean angularIsInput;
+    private final byte linearTransformPosition;
 
     /**
      * An operation method that describe this coordinate system conversion.
@@ -123,14 +127,14 @@ abstract class CoordinateSystemTransform extends AbstractMathTransform {
      * or {@link ContextualParameters#denormalizeGeographicOutputs(double)} after this constructor.
      */
     CoordinateSystemTransform(final String method, final String method3D, final int dimension,
-                              final int[] linearDimensions, final boolean angularIsInput)
+                              final int[] linearDimensions, final byte linearTransformPosition)
     {
-        this.dimension        = dimension;
+        this.dimension = dimension;
         this.linearDimensions = linearDimensions;
-        this.angularIsInput   = angularIsInput;
-        this.method           = method(method);
-        this.method3D         = (method3D != null) ? method(method3D) : this.method;
-        this.context          = new ContextualParameters(this.method.getParameters(), dimension, dimension);
+        this.linearTransformPosition = linearTransformPosition;
+        this.method = method(method);
+        this.method3D = (method3D != null) ? method(method3D) : this.method;
+        this.context = new ContextualParameters(this.method.getParameters(), dimension, dimension);
     }
 
     /**
@@ -216,11 +220,11 @@ abstract class CoordinateSystemTransform extends AbstractMathTransform {
      * needed anyway for the conversions between radians and degrees.
      */
     @Override
-    protected final MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
-                                                 final MathTransformFactory factory) throws FactoryException
-    {
-concat: if (applyOtherFirst != angularIsInput) {
-            final ExtendedPrecisionMatrix linear = ExtendedPrecisionMatrix.castOrWrap(MathTransforms.getMatrix(other));
+    protected final void tryConcatenate(final Joiner info) throws FactoryException {
+        // Do nothing if there is no linear transform for angular values.
+concat: if (info.isLinear(-linearTransformPosition, true)) {
+            final var linear = ExtendedPrecisionMatrix.castOrWrap(MathTransforms.getMatrix(
+                                    info.getTransform(linearTransformPosition).orElse(null)));
             if (linear != null) {
                 final int n = linear.getNumRow();
                 if (n == linear.getNumCol()) {
@@ -240,26 +244,21 @@ concat: if (applyOtherFirst != angularIsInput) {
                     /*
                      * The transform is affine and applies an uniform scale in all dimensions.
                      * Replace it by the same scale in all output dimensions having a linear unit.
-                     *
                      */
                     if (scale != null) {
-                        final MatrixSIS angular = Matrices.createIdentity(n);
+                        final MatrixSIS angular = Matrices.create(n, n, ExtendedPrecisionMatrix.CREATE_IDENTITY);
                         for (int j : linearDimensions) {
                             angular.setNumber(j, j, scale);
                         }
-                        final var w = MathTransformsOrFactory.wrap(factory);
-                        MathTransform step1 = w.linear(angular);
-                        MathTransform step2 = this;
-                        if (applyOtherFirst) {
-                            step2 = step1;
-                            step1 = this;
-                        }
-                        return w.concatenate(step1, step2);
+                        MathTransform t = info.factory.createAffineTransform(angular);
+                        t = info.concatenate(t, this, linearTransformPosition < 0);
+                        info.replace(linearTransformPosition, t);
+                        return;
                     }
                 }
             }
         }
-        return super.tryConcatenate(applyOtherFirst, other, factory);
+        super.tryConcatenate(info);
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
index 87520fc8f1..d44836b495 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
@@ -95,7 +95,7 @@ import static org.apache.sis.referencing.operation.provider.GeocentricAffineBetw
  * </ul>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.7
  */
 public class EllipsoidToCentricTransform extends AbstractMathTransform implements Serializable {
@@ -921,29 +921,27 @@ next:   while (--numPts >= 0) {
          * </ul>
          */
         @Override
-        protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
-                final MathTransformFactory factory) throws FactoryException
-        {
-            if (!applyOtherFirst && forward.withHeight && other instanceof LinearTransform && other.getTargetDimensions() == 2) {
-                /*
-                 * Found a 3×4 matrix after this transform. We can reduce to a 3×3 matrix only if no dimension
-                 * use the column that we are about to drop (i.e. all coefficients in that column are zero).
-                 */
-                Matrix matrix = ((LinearTransform) other).getMatrix();
-                if (matrix.getElement(0,2) == 0 &&
-                    matrix.getElement(1,2) == 0 &&
-                    matrix.getElement(2,2) == 0)
-                {
-                    matrix = MatrixSIS.castOrCopy(matrix).removeColumns(2, 3);
-                    final MathTransform tr2D = forward.create2D().inverse();
-                    if (factory != null) {
-                        return factory.createConcatenatedTransform(tr2D, factory.createAffineTransform(matrix));
-                    } else {
-                        return ConcatenatedTransform.create(tr2D, MathTransforms.linear(matrix), factory);
+        protected void tryConcatenate(final Joiner context) throws FactoryException {
+            if (forward.withHeight) {
+                Matrix matrix = MathTransforms.getMatrix(context.getTransform(+1).orElse(null));
+                if (matrix != null && matrix.getNumRow() == 3) {
+                    /*
+                     * Found a 3×4 matrix after this transform. We can reduce to a 3×3 matrix only if no dimension
+                     * use the column that we are about to drop (i.e. all coefficients in that column are zero).
+                     */
+                    if (matrix.getElement(0,2) == 0 &&
+                        matrix.getElement(1,2) == 0 &&
+                        matrix.getElement(2,2) == 0)
+                    {
+                        matrix = MatrixSIS.castOrCopy(matrix).removeColumns(2, 3);
+                        final MathTransform tr2D = forward.create2D().inverse();
+                        MathTransform next = context.factory.createAffineTransform(matrix);
+                        context.replace(+1, context.factory.createConcatenatedTransform(tr2D, next));
+                        return;
                     }
                 }
             }
-            return super.tryConcatenate(applyOtherFirst, other, factory);
+            super.tryConcatenate(context);
         }
 
         /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ExponentialTransform1D.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ExponentialTransform1D.java
index cb7f888aca..d046218a2a 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ExponentialTransform1D.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ExponentialTransform1D.java
@@ -19,7 +19,6 @@ package org.apache.sis.referencing.operation.transform;
 import java.io.Serializable;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
-import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.util.FactoryException;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.internal.Numerics;
@@ -211,51 +210,51 @@ final class ExponentialTransform1D extends AbstractMathTransform1D implements Se
     }
 
     /**
-     * Concatenates in an optimized way a {@link MathTransform} {@code other} to this
-     * {@code MathTransform}. This implementation can optimize some concatenation with
-     * {@link LinearTransform1D} and {@link LogarithmicTransform1D}.
-     *
-     * @param  applyOtherFirst  {@code true} if the transformation order is {@code other} followed by {@code this}, or
-     *                          {@code false} if the transformation order is {@code this} followed by {@code other}.
-     * @param  other            the other math transform to (pre-)concatenate with this transform.
-     * @param  factory          the factory which is (indirectly) invoking this method, or {@code null} if none.
-     * @return the combined math transform, or {@code null} if no optimized combined transform is available.
+     * Concatenates in an optimized way a neighbor math transform with this transform.
+     * This implementation does special cases for {@link LinearTransform1D} and {@link LogarithmicTransform1D}.
      */
     @Override
-    protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
-            final MathTransformFactory factory) throws FactoryException
-    {
-        if (other instanceof LinearTransform) {
-            final LinearTransform1D linear = (LinearTransform1D) other;
-            if (applyOtherFirst) {
-                final double newBase  = Math.pow(base, linear.scale);
-                final double newScale = Math.pow(base, linear.offset) * scale;
-                if (!Double.isNaN(newBase) && !Double.isNaN(newScale)) {
-                    return create(newBase, newScale);
-                }
-            } else {
-                if (linear.offset == 0) {
-                    return create(base, scale * linear.scale);
+    protected void tryConcatenate(final Joiner context) throws FactoryException {
+        MathTransform concatenation = null;
+        int relativeIndex = +1;
+        do {
+            final MathTransform other = context.getTransform(relativeIndex).orElse(null);
+            if (other instanceof LinearTransform1D) {
+                final LinearTransform1D linear = (LinearTransform1D) other;
+                if (relativeIndex < 0) {
+                    final double newBase  = Math.pow(base, linear.scale);
+                    final double newScale = Math.pow(base, linear.offset) * scale;
+                    if (!Double.isNaN(newBase) && !Double.isNaN(newScale)) {
+                        concatenation = create(newBase, newScale);
+                    }
+                } else {
+                    if (linear.offset == 0) {
+                        concatenation = create(base, scale * linear.scale);
+                    }
                 }
+            } else if (other instanceof LogarithmicTransform1D) {
+                concatenation = concatenateLog((LogarithmicTransform1D) other, relativeIndex);
             }
-        } else if (other instanceof LogarithmicTransform1D) {
-            return concatenateLog((LogarithmicTransform1D) other, applyOtherFirst);
-        }
-        return super.tryConcatenate(applyOtherFirst, other, factory);
+            if (concatenation != null) {
+                context.replace(relativeIndex, concatenation);
+                return;
+            }
+        } while ((relativeIndex = -relativeIndex) < 0);
+        super.tryConcatenate(context);
     }
 
     /**
      * Concatenates in an optimized way a {@link LogarithmicTransform1D} {@code other}
      * to this {@code ExponentialTransform1D}.
      *
-     * @param  other            the math transform to apply.
-     * @param  applyOtherFirst  {@code true} if the transformation order is {@code other} followed by {@code this}, or
-     *                          {@code false} if the transformation order is {@code this} followed by {@code other}.
+     * @param  other          the math transform to apply.
+     * @param  relativeIndex  -1 if the transformation order is {@code other} followed by {@code this}, or
+     *                        +1 if the transformation order is {@code this} followed by {@code other}.
      * @return the combined math transform, or {@code null} if no optimized combined transform is available.
      */
-    final MathTransform concatenateLog(final LogarithmicTransform1D other, final boolean applyOtherFirst) {
+    final MathTransform concatenateLog(final LogarithmicTransform1D other, final int relativeIndex) {
         final double newScale = lnBase / other.lnBase();
-        if (applyOtherFirst) {
+        if (relativeIndex < 0) {
             return MathTransforms.concatenate(PowerTransform1D.create(newScale),
                     LinearTransform1D.create(scale * Math.pow(base, other.offset()), null));
         } else {
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/LogarithmicTransform1D.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/LogarithmicTransform1D.java
index 8fc9e5eec9..aa0b4ec8ad 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/LogarithmicTransform1D.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/LogarithmicTransform1D.java
@@ -19,7 +19,6 @@ package org.apache.sis.referencing.operation.transform;
 import java.io.Serializable;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
-import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.util.FactoryException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
@@ -107,36 +106,36 @@ class LogarithmicTransform1D extends AbstractMathTransform1D implements Serializ
     }
 
     /**
-     * Concatenates in an optimized way a {@link MathTransform} {@code other} to this
-     * {@code MathTransform}. This implementation can optimize some concatenation with
-     * {@link LinearTransform1D} and {@link ExponentialTransform1D}.
-     *
-     * @param  applyOtherFirst  {@code true} if the transformation order is {@code other} followed by {@code this}, or
-     *                          {@code false} if the transformation order is {@code this} followed by {@code other}.
-     * @param  other            the other math transform to (pre-)concatenate with this transform.
-     * @param  factory          the factory which is (indirectly) invoking this method, or {@code null} if none.
-     * @return the combined math transform, or {@code null} if no optimized combined transform is available.
+     * Concatenates in an optimized way a neighbor math transform with this transform.
+     * This implementation does special cases for {@link LinearTransform1D} and {@link ExponentialTransform1D}.
      */
     @Override
-    protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
-            final MathTransformFactory factory) throws FactoryException
-    {
-        if (other instanceof LinearTransform1D) {
-            final LinearTransform1D linear = (LinearTransform1D) other;
-            if (applyOtherFirst) {
-                if (linear.offset == 0 && linear.scale > 0) {
-                    return create(base(), transform(linear.scale));
-                }
-            } else {
-                final double newBase = pow(1 / linear.scale);
-                if (!Double.isNaN(newBase)) {
-                    return create(newBase, linear.transform(offset()));
+    protected void tryConcatenate(final Joiner context) throws FactoryException {
+        MathTransform concatenation = null;
+        int relativeIndex = +1;
+        do {
+            final MathTransform other = context.getTransform(relativeIndex).orElse(null);
+            if (other instanceof LinearTransform1D) {
+                final LinearTransform1D linear = (LinearTransform1D) other;
+                if (relativeIndex < 0) {
+                    if (linear.offset == 0 && linear.scale > 0) {
+                        concatenation = create(base(), transform(linear.scale));
+                    }
+                } else {
+                    final double newBase = pow(1 / linear.scale);
+                    if (!Double.isNaN(newBase)) {
+                        concatenation = create(newBase, linear.transform(offset()));
+                    }
                 }
+            } else if (other instanceof ExponentialTransform1D) {
+                concatenation = ((ExponentialTransform1D) other).concatenateLog(this, -relativeIndex);
             }
-        } else if (other instanceof ExponentialTransform1D) {
-            return ((ExponentialTransform1D) other).concatenateLog(this, !applyOtherFirst);
-        }
-        return super.tryConcatenate(applyOtherFirst, other, factory);
+            if (concatenation != null) {
+                context.replace(relativeIndex, concatenation);
+                return;
+            }
+        } while ((relativeIndex = -relativeIndex) < 0);
+        super.tryConcatenate(context);
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PassThroughTransform.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
index e8c0d92b06..53dcc5a4ef 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
@@ -30,7 +30,6 @@ import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
-import org.apache.sis.referencing.util.MathTransformsOrFactory;
 import org.apache.sis.referencing.util.DirectPositionView;
 import org.apache.sis.referencing.util.WKTKeywords;
 import org.apache.sis.util.ArgumentChecks;
@@ -270,14 +269,14 @@ public class PassThroughTransform extends AbstractMathTransform implements Seria
             if (subLower == 0 && subUpper == subDim) {
                 step = subTransform;
             } else {
-                // Restriction below apply only if the `subTransform` cannot be used as a whole.
+                // Restriction below applies only if the `subTransform` cannot be used as a whole.
                 ArgumentChecks.ensureDimensionsMatch("subTransform", subDim, subDim, subTransform);
                 sep.addSourceDimensionRange(subLower, subUpper);
                 sep.addTargetDimensionRange(subLower, subUpper);
                 step = sep.separate();
             }
-            step   = sep.factory.passThrough(lower, step, resultDim - upper);
-            result = sep.factory.concatenate(result, step);
+            step   = sep.factory.createPassThroughTransform(lower, step, resultDim - upper);
+            result = sep.factory.createConcatenatedTransform(result, step);
             sep.clear();
             subLower = subUpper;
         }
@@ -719,7 +718,7 @@ public class PassThroughTransform extends AbstractMathTransform implements Seria
                     }
                 }
                 if (element != (i == j ? 1 : 0)) {
-                    // Found a dimension which perform some scaling or translation.
+                    // Found a dimension performing some scaling or translation outside the sub-transform.
                     return null;
                 }
             }
@@ -728,7 +727,7 @@ public class PassThroughTransform extends AbstractMathTransform implements Seria
     }
 
     /**
-     * Concatenates or pre-concatenates in an optimized way this transform with the given transform, if possible.
+     * Concatenates or pre-concatenates in an optimized way this transform with a neighbor, if possible.
      * This method applies the following special cases:
      *
      * <ul>
@@ -737,153 +736,168 @@ public class PassThroughTransform extends AbstractMathTransform implements Seria
      *  <li>If the other transform discards some dimensions, verify if we still need a {@code PassThroughTransform}.</li>
      * </ul>
      *
-     * @return the simplified transform, or {@code null} if no such optimization is available.
+     * @param  context  information about the neighbor transforms, and the object where to set the result.
      * @throws FactoryException if an error occurred while combining the transforms.
      *
-     * @since 1.0
+     * @since 1.5
      */
     @Override
-    protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other, final MathTransformFactory factory)
-            throws FactoryException
-    {
-        final MathTransformsOrFactory proxy = MathTransformsOrFactory.wrap(factory);
-        if (other instanceof PassThroughTransform) {
-            final PassThroughTransform opt = (PassThroughTransform) other;
-            if (opt.firstAffectedCoordinate == firstAffectedCoordinate && opt.numTrailingCoordinates == numTrailingCoordinates) {
-                final MathTransform sub = proxy.concatenate(applyOtherFirst, subTransform, opt.subTransform);
-                return proxy.passThrough(firstAffectedCoordinate, sub, numTrailingCoordinates);
+    protected void tryConcatenate(final Joiner context) throws FactoryException {
+        int relativeIndex = -1;     // Note: algorithm below needs to test (before, after) in that order.
+        MathTransform other;        // The transform immediately before or after, this transform.
+        Matrix m;                   // The matrix of `other`, or null if `other` is non-linear.
+        do {
+            other = context.getTransform(relativeIndex).orElse(null);
+            if (other instanceof PassThroughTransform) {
+                final PassThroughTransform opt = (PassThroughTransform) other;
+                if (opt.firstAffectedCoordinate == firstAffectedCoordinate && opt.numTrailingCoordinates == numTrailingCoordinates) {
+                    MathTransform tr = context.concatenate(subTransform, opt.subTransform, relativeIndex < 0);
+                    tr = context.factory.createPassThroughTransform(firstAffectedCoordinate, tr, numTrailingCoordinates);
+                    context.replace(relativeIndex, tr);
+                    return;
+                }
             }
-        }
-        final Matrix m = MathTransforms.getMatrix(other);
-        if (m != null) {
             /*
              * If the other transform is a linear transform and all passthrough coordinates are unchanged by the matrix,
              * we can move the matrix inside the passthrough transform. It reduces the number of dimension on which the
              * linear transform operate, and gives a chance for another optimization in the concatenation between that
              * linear transform and the sub-transform.
              */
-            final Matrix sub = toSubMatrix(applyOtherFirst, m);
-            if (sub != null) {
-                MathTransform tr = proxy.linear(sub);
-                tr = proxy.concatenate(applyOtherFirst, subTransform, tr);
-                return proxy.passThrough(firstAffectedCoordinate, tr, numTrailingCoordinates);
+            m = MathTransforms.getMatrix(other);
+            if (m != null) {
+                final Matrix sub = toSubMatrix(relativeIndex < 0, m);
+                if (sub != null) {
+                    MathTransform tr = context.factory.createAffineTransform(sub);
+                    tr = context.concatenate(subTransform, tr, relativeIndex < 0);
+                    tr = context.factory.createPassThroughTransform(firstAffectedCoordinate, tr, numTrailingCoordinates);
+                    context.replace(relativeIndex, tr);
+                    return;
+                }
             }
+        } while ((relativeIndex = -relativeIndex) >= 0);
+        /*
+         * The remaining code in this method shall be executed only for the transform AFTER this transform.
+         * The `other` and `m` variables, if non-null, have the values that we need because of the order in
+         * which above loop was executed.
+         */
+        if (m == null) {
             /*
-             * If this PassThroughTransform is followed by a matrix discarding some dimensions, identify which dimensions
-             * are discarded. If all dimensions computed by the sub-transform are discarded, then we no longer need it.
-             * If some pass-through dimensions are discarded, then we can reduce the number of pass-through dimensions.
+             * Do not invoke `super.tryConcatenate(context)`. We do not want to test if this transform
+             * is the inverse of the `other` transform because this check is costly and unnecessary.
+             * If the two transforms were the inverse of each other, then the concatenation of
+             * `this.subTransform` with `other.subTransform` done at the beginning of this method
+             * would have produced the identity transform already.
              */
-            if (!applyOtherFirst) {
-                final int dimension = m.getNumCol() - 1;            // Number of source dimensions (ignore translations column).
-                if (dimension <= Long.SIZE) {                       // Because retained dimensions stored as a mask on 64 bits.
-                    long retainedDimensions = 0;
-                    final int numRows = m.getNumRow();              // Number of target dimensions + 1.
-                    for (int i=0; i<dimension; i++) {
-                        for (int j=0; j<numRows; j++) {
-                            if (m.getElement(j,i) != 0) {
-                                retainedDimensions |= (1L << i);    // Found a source dimension which is required by target dimension.
-                                break;
-                            }
-                        }
-                    }
-                    /*
-                     * Verify if matrix discards the sub-transform. If it does not, then we need to keep all the sub-transform
-                     * dimensions (discarding them is a "all or nothing" operation). Other dimensions (leading and trailing)
-                     * can be keep or discarded on a case-by-case basis.
-                     */
-                    final long    fullTransformMask = maskLowBits(dimension);
-                    final long    subTransformMask  = maskLowBits(subTransform.getTargetDimensions()) << firstAffectedCoordinate;
-                    final boolean keepSubTransform  = (retainedDimensions & subTransformMask) != 0;
-                    if (keepSubTransform) {
-                        retainedDimensions |= subTransformMask;           // Ensure that we keep all sub-transform dimensions.
-                    }
-                    if (retainedDimensions != fullTransformMask) {
-                        final int change = subTransform.getSourceDimensions() - subTransform.getTargetDimensions();
-                        if (change == 0 && !keepSubTransform) {
-                            return other;                                 // Shortcut avoiding creation of new MathTransforms.
-                        }
-                        /*
-                         * We enter in this block if some dimensions can be discarded. We want to discard them before
-                         * the PassThroughTransform instead of after. The matrix for that purpose will be computed later.
-                         * Before that, the loop below modifies a copy of the `other` matrix as if those dimensions were
-                         * already removed.
-                         */
-                        MatrixSIS reduced = MatrixSIS.castOrCopy(m);
-                        long columnsToRemove = ~retainedDimensions & fullTransformMask;       // Cannot be 0 at this point.
-                        do {
-                            final int lower = Long.numberOfTrailingZeros(columnsToRemove);
-                            final int upper = Long.numberOfTrailingZeros(~(columnsToRemove | maskLowBits(lower)));
-                            reduced = reduced.removeColumns(lower, upper);
-                            columnsToRemove &= ~maskLowBits(upper);
-                            columnsToRemove >>>= (upper - lower);
-                        } while (columnsToRemove != 0);
-                        /*
-                         * Expands the `retainedDimensions` bitmask into a list of indices of dimensions to keep.   However
-                         * those indices are for dimensions to keep after the PassThroughTransform.  Because we rather want
-                         * indices for dimensions to keep before the PassThroughTransform, we need to adjust for difference
-                         * in number of dimensions. This change is represented by the `change` integer computed above.
-                         * We apply two strategies:
-                         *
-                         *    1) If we keep the sub-transform, then the loop while surely sees the `firstAffectedCoordinate`
-                         *       dimension since we ensured that we keep all sub-transform dimensions. When it happens, we
-                         *       add or remove bits at that point for the dimensionality changes.
-                         *
-                         *    2) If we do not keep the sub-transform, then code inside `if (dim == firstAffectedCoordinate)`
-                         *       should not have been executed. Instead, we will adjust the indices after the loop.
-                         */
-                        final long leadPassThroughMask = maskLowBits(firstAffectedCoordinate);
-                        final int numKeepAfter  = Long.bitCount(retainedDimensions & ~(leadPassThroughMask | subTransformMask));
-                        final int numKeepBefore = Long.bitCount(retainedDimensions & leadPassThroughMask);
-                        final int[] indices = new int[Long.bitCount(retainedDimensions) + change];
-                        for (int i=0; i<indices.length; i++) {
-                            int dim = Long.numberOfTrailingZeros(retainedDimensions);
-                            if (dim == firstAffectedCoordinate) {
-                                if (change < 0) {
-                                    retainedDimensions >>>= -change;                        // Discard dimensions to skip.
-                                    retainedDimensions &= ~leadPassThroughMask;             // Clear previous dimension flags.
-                                } else {
-                                    retainedDimensions <<= change;                          // Add dimensions.
-                                    retainedDimensions |= maskLowBits(change) << dim;       // Set flags for new dimensions.
-                                }
-                            }
-                            retainedDimensions &= ~(1L << dim);
-                            indices[i] = dim;
-                        }
-                        if (!keepSubTransform) {
-                            for (int i=indices.length; --i >= 0;) {
-                                final int dim = indices[i];
-                                if (dim <= firstAffectedCoordinate) break;
-                                indices[i] = dim - change;
-                            }
-                        }
-                        /*
-                         * Concatenate:
-                         *   1) An affine transform discarding some dimensions (no other operation).
-                         *   2) The passthrough transform with less input and output dimensions.
-                         *   3) The `other` transform with less input dimensions.
-                         */
-                        MathTransform tr = proxy.linear(Matrices.createDimensionSelect(dimension + change, indices));
-                        if (keepSubTransform) {
-                            tr = proxy.concatenate(tr, proxy.passThrough(numKeepBefore, subTransform, numKeepAfter));
-                        }
-                        tr = proxy.concatenate(tr, proxy.linear(reduced));
-                        return tr;
-                    }
+            return;
+        }
+        /*
+         * If this PassThroughTransform is followed by a matrix discarding some dimensions, identify which dimensions
+         * are discarded. If all dimensions computed by the sub-transform are discarded, then we no longer need it.
+         * If some pass-through dimensions are discarded, then we can reduce the number of pass-through dimensions.
+         */
+        final int dimension = m.getNumCol() - 1;        // Number of source dimensions (ignore translations column).
+        if (dimension > Long.SIZE) return;              // Because retained dimensions stored as a mask on 64 bits.
+        long retainedDimensions = 0;
+        final int numRows = m.getNumRow();              // Number of target dimensions + 1.
+        for (int i=0; i<dimension; i++) {
+            for (int j=0; j<numRows; j++) {
+                if (m.getElement(j,i) != 0) {
+                    retainedDimensions |= (1L << i);    // Found a source dimension which is required by target dimension.
+                    break;
+                }
+            }
+        }
+        /*
+         * Verify if matrix discards the sub-transform. If it does not, then we need to keep all the sub-transform
+         * dimensions (discarding them is a "all or nothing" operation). Other dimensions (leading and trailing)
+         * can be keep or discarded on a case-by-case basis.
+         */
+        final long    fullTransformMask = maskLowBits(dimension);
+        final long    subTransformMask  = maskLowBits(subTransform.getTargetDimensions()) << firstAffectedCoordinate;
+        final boolean keepSubTransform  = (retainedDimensions & subTransformMask) != 0;
+        if (keepSubTransform) {
+            retainedDimensions |= subTransformMask;           // Ensure that we keep all sub-transform dimensions.
+        }
+        if (retainedDimensions == fullTransformMask) {
+            return;                                           // Nothing to change.
+        }
+        final int change = subTransform.getSourceDimensions() - subTransform.getTargetDimensions();
+        if (change == 0 && !keepSubTransform) {
+            context.replace(relativeIndex, other);      // Shortcut avoiding creation of new MathTransforms.
+            return;
+        }
+        /*
+         * We enter in this block if some dimensions can be discarded. We want to discard them before
+         * the PassThroughTransform instead of after. The matrix for that purpose will be computed later.
+         * Before that, the loop below modifies a copy of the `other` matrix as if those dimensions were
+         * already removed.
+         */
+        MatrixSIS reduced = MatrixSIS.castOrCopy(m);
+        long columnsToRemove = ~retainedDimensions & fullTransformMask;       // Cannot be 0 at this point.
+        do {
+            final int lower = Long.numberOfTrailingZeros(columnsToRemove);
+            final int upper = Long.numberOfTrailingZeros(~(columnsToRemove | maskLowBits(lower)));
+            reduced = reduced.removeColumns(lower, upper);
+            columnsToRemove &= ~maskLowBits(upper);
+            columnsToRemove >>>= (upper - lower);
+        } while (columnsToRemove != 0);
+        /*
+         * Expands the `retainedDimensions` bitmask into a list of indices of dimensions to keep.   However
+         * those indices are for dimensions to keep after the PassThroughTransform.  Because we rather want
+         * indices for dimensions to keep before the PassThroughTransform, we need to adjust for difference
+         * in number of dimensions. This change is represented by the `change` integer computed above.
+         * We apply two strategies:
+         *
+         *    1) If we keep the sub-transform, then the loop while surely sees the `firstAffectedCoordinate`
+         *       dimension since we ensured that we keep all sub-transform dimensions. When it happens, we
+         *       add or remove bits at that point for the dimensionality changes.
+         *
+         *    2) If we do not keep the sub-transform, then code inside `if (dim == firstAffectedCoordinate)`
+         *       should not have been executed. Instead, we will adjust the indices after the loop.
+         */
+        final long leadPassThroughMask = maskLowBits(firstAffectedCoordinate);
+        final int numKeepAfter  = Long.bitCount(retainedDimensions & ~(leadPassThroughMask | subTransformMask));
+        final int numKeepBefore = Long.bitCount(retainedDimensions & leadPassThroughMask);
+        final int[] indices = new int[Long.bitCount(retainedDimensions) + change];
+        for (int i=0; i<indices.length; i++) {
+            int dim = Long.numberOfTrailingZeros(retainedDimensions);
+            if (dim == firstAffectedCoordinate) {
+                if (change < 0) {
+                    retainedDimensions >>>= -change;                        // Discard dimensions to skip.
+                    retainedDimensions &= ~leadPassThroughMask;             // Clear previous dimension flags.
+                } else {
+                    retainedDimensions <<= change;                          // Add dimensions.
+                    retainedDimensions |= maskLowBits(change) << dim;       // Set flags for new dimensions.
                 }
             }
+            retainedDimensions &= ~(1L << dim);
+            indices[i] = dim;
+        }
+        if (!keepSubTransform) {
+            for (int i=indices.length; --i >= 0;) {
+                final int dim = indices[i];
+                if (dim <= firstAffectedCoordinate) break;
+                indices[i] = dim - change;
+            }
         }
         /*
-         * Do not invoke super.tryConcatenate(applyOtherFirst, other, factory); we do not want to test if this transform
-         * is the inverse of the other transform as it is costly and unnecessary.  If it was the case, the concatenation
-         * of `this.subTransform` with `other.subTransform` done at the beginning of this method would have produced the
-         * identity transform already.
+         * Concatenate:
+         *   1) An affine transform discarding some dimensions (no other operation).
+         *   2) The passthrough transform with less input and output dimensions.
+         *   3) The `other` transform with less input dimensions.
          */
-        return null;
+        MathTransform tr = context.factory.createAffineTransform(Matrices.createDimensionSelect(dimension + change, indices));
+        if (keepSubTransform) {
+            tr = context.factory.createConcatenatedTransform(tr,
+                    context.factory.createPassThroughTransform(numKeepBefore, subTransform, numKeepAfter));
+        }
+        tr = context.factory.createConcatenatedTransform(tr, context.factory.createAffineTransform(reduced));
+        context.replace(relativeIndex, tr);
     }
 
     /**
      * Returns a mask for the {@code n} lowest bits. This is a convenience method for a frequently
-     * used operation in {@link #tryConcatenate(boolean, MathTransform, MathTransformFactory)}.
+     * used operation in {@link #tryConcatenate(Joiner)}.
      */
     private static long maskLowBits(final int n) {
         return Numerics.bitmask(n) - 1;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PolarToCartesian.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PolarToCartesian.java
index d8ae167572..8a94e1b656 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PolarToCartesian.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PolarToCartesian.java
@@ -78,7 +78,7 @@ final class PolarToCartesian extends CoordinateSystemTransform implements Serial
      * Input coordinates are in radians.
      */
     private PolarToCartesian() {
-        super("Polar to Cartesian", "Cylindrical to Cartesian", 2, new int[1], true);
+        super("Polar to Cartesian", "Cylindrical to Cartesian", 2, new int[1], (byte) +1);
         context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION)
                .convertBefore(1, DoubleDouble.DEGREES_TO_RADIANS, null);
     }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PowerTransform1D.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PowerTransform1D.java
index 6d998b4238..8f5a8411a1 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PowerTransform1D.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PowerTransform1D.java
@@ -19,7 +19,6 @@ package org.apache.sis.referencing.operation.transform;
 import java.io.Serializable;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
-import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.util.FactoryException;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.internal.Numerics;
@@ -30,8 +29,8 @@ import org.apache.sis.util.internal.Numerics;
  * needs of the {@link ExponentialTransform1D#concatenateLog(LogarithmicTransform1D, boolean)}.
  * Future version may expand on that.
  *
- * <p>Before to make this class public (if we do), we need to revisit the class name, define parameters
- * and improve the {@link #tryConcatenate(boolean, MathTransform, MathTransformFactory)} method.</p>
+ * <p>Before to make this class public (if we do), we need to revisit the class name,
+ * define parameters and improve the {@link #tryConcatenate(Joiner)} method.</p>
  *
  * <h2>Serialization</h2>
  * Serialized instances of this class are not guaranteed to be compatible with future SIS versions.
@@ -167,23 +166,20 @@ final class PowerTransform1D extends AbstractMathTransform1D implements Serializ
 
     /**
      * Concatenates in an optimized way a {@link MathTransform} {@code other} to this {@code MathTransform}.
-     *
-     * @param  applyOtherFirst  {@code true} if the transformation order is {@code other} followed by {@code this},
-     *                          or {@code false} if the transformation order is {@code this} followed by {@code other}.
-     * @param  other            the other math transform to (pre-)concatenate with this transform.
-     * @param  factory          the factory which is (indirectly) invoking this method, or {@code null} if none.
-     * @return the combined math transform, or {@code null} if no optimized combined transform is available.
      */
     @Override
-    protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
-            final MathTransformFactory factory) throws FactoryException
-    {
-        if (other instanceof PowerTransform1D) {
-            // Valid for both concatenation and pre-concatenation.
-            return create(power + ((PowerTransform1D) other).power);
-        }
+    protected void tryConcatenate(final Joiner context) throws FactoryException {
+        int relativeIndex = +1;
+        do {
+            final MathTransform other = context.getTransform(relativeIndex).orElse(null);
+            if (other instanceof PowerTransform1D) {
+                // Valid for both concatenation and pre-concatenation.
+                context.replace(relativeIndex, create(power + ((PowerTransform1D) other).power));
+                return;
+            }
+        } while ((relativeIndex = -relativeIndex) < 0);
         // TODO: more optimization could go here for logarithmic and exponential cases.
-        return super.tryConcatenate(applyOtherFirst, other, factory);
+        super.tryConcatenate(context);
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/SphericalToCartesian.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/SphericalToCartesian.java
index ddb0093bf9..d492785e83 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/SphericalToCartesian.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/SphericalToCartesian.java
@@ -76,7 +76,7 @@ final class SphericalToCartesian extends CoordinateSystemTransform implements Se
      * Input coordinates are in radians.
      */
     private SphericalToCartesian() {
-        super("Spherical to Cartesian", null, 3, new int[] {2}, true);
+        super("Spherical to Cartesian", null, 3, new int[] {2}, (byte) +1);
         context.normalizeGeographicInputs(0);                   // Convert (θ,Ω) from degrees to radians.
     }
 
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
index d995e21b07..e330ceffd0 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
@@ -23,7 +23,7 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.matrix.Matrices;
-import org.apache.sis.referencing.util.MathTransformsOrFactory;
+import org.apache.sis.referencing.util.ReferencingUtilities;
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
@@ -94,7 +94,7 @@ public class TransformSeparator {
     /**
      * The factory to use for creating new math transforms.
      */
-    final MathTransformsOrFactory factory;
+    final MathTransformFactory factory;
 
     /**
      * Whether {@link #separate()} is allowed to add new dimensions in {@link #sourceDimensions}
@@ -123,7 +123,7 @@ public class TransformSeparator {
     public TransformSeparator(final MathTransform transform, final MathTransformFactory factory) {
         ArgumentChecks.ensureNonNull("transform", transform);
         this.transform = transform;
-        this.factory   = MathTransformsOrFactory.wrap(factory);
+        this.factory   = ReferencingUtilities.nonNull(factory);
     }
 
     /**
@@ -544,7 +544,7 @@ public class TransformSeparator {
             final ConcatenatedTransform ctr = (ConcatenatedTransform) step;
             final MathTransform step1 = filterSourceDimensions(ctr.transform1, dimensions);
             final MathTransform step2 = filterSourceDimensions(ctr.transform2, targetDimensions);
-            return factory.concatenate(step1, step2);
+            return factory.createConcatenatedTransform(step1, step2);
             // Keep the `targetDimensions` computed by the last step.
         }
         /*
@@ -606,7 +606,7 @@ public class TransformSeparator {
             if (containsAll(dimensions, lower, subLower) && containsAll(dimensions, subUpper, upper)) {
                 final int offset = subDimensions[0];
                 assert containsAll(subDimensions, offset, offset + subDimensions.length) : offset;
-                return factory.passThrough(offset + subLower - lower, subTransform, Math.max(0, upper - subUpper));
+                return factory.createPassThroughTransform(offset + subLower - lower, subTransform, Math.max(0, upper - subUpper));
             }
         }
         /*
@@ -664,7 +664,7 @@ reduce:     for (int j=0; j <= numTgt; j++) {
                     return MathTransforms.identity(0);
                 }
                 elements = ArraysExt.resize(elements, startOfRow);
-                return factory.linear(Matrices.create(startOfRow / numFilteredColumns, numFilteredColumns, elements));
+                return factory.createAffineTransform(Matrices.create(startOfRow / numFilteredColumns, numFilteredColumns, elements));
             }
             /*
              * In an affine transform, the last row is not supposed to have dependency to any source dimension.
@@ -740,7 +740,7 @@ reduce:     for (int j=0; j <= numTgt; j++) {
             matrix.setElement(j, i, 1);
         }
         matrix.setElement(dimensions.length, numTgt, 1);
-        return factory.concatenate(step, factory.linear(matrix));
+        return factory.createConcatenatedTransform(step, factory.createAffineTransform(matrix));
     }
 
     /**
@@ -752,8 +752,7 @@ reduce:     for (int j=0; j <= numTgt; j++) {
      * <p>This method can process only linear transforms (potentially indirectly through a concatenated transform).
      * Actually it would be possible to also process pass-through transform followed by a linear transform, but this
      * case should have been optimized during transform concatenation. If it is not the case, consider improving the
-     * {@link PassThroughTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)} method instead of
-     * this one.</p>
+     * {@link PassThroughTransform#tryConcatenate(AbstractMathTransform.Joiner)} method instead of this one.</p>
      *
      * @param  head      the first transform of a transformation chain.
      * @param  required  sources to keep even if not necessary, or {@code null} if none.
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/UnitConversion.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/UnitConversion.java
index f7fbe346c6..78ccd7cf7a 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/UnitConversion.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/UnitConversion.java
@@ -20,7 +20,6 @@ import java.io.Serializable;
 import javax.measure.UnitConverter;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
-import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.referencing.internal.Resources;
@@ -125,19 +124,20 @@ final class UnitConversion extends AbstractMathTransform1D implements Serializab
 
     /**
      * Concatenates or pre-concatenates in an optimized way this math transform with the given one, if possible.
-     *
-     * @return the math transforms combined in an optimized way, or {@code null} if no such optimization is available.
      */
     @Override
-    protected MathTransform tryConcatenate(boolean applyOtherFirst, MathTransform other, MathTransformFactory factory)
-            throws FactoryException
-    {
-        if (other instanceof UnitConversion) {
-            final var that = (UnitConversion) other;
-            return create(applyOtherFirst
-                    ? that.converter.concatenate(this.converter)
-                    : this.converter.concatenate(that.converter));
-        }
-        return super.tryConcatenate(applyOtherFirst, other, factory);
+    protected void tryConcatenate(final Joiner context) throws FactoryException {
+        int relativeIndex = +1;
+        do {
+            final MathTransform other = context.getTransform(relativeIndex).orElse(null);
+            if (other instanceof UnitConversion) {
+                final var that = (UnitConversion) other;
+                context.replace(relativeIndex, create(relativeIndex < 0
+                        ? that.converter.concatenate(this.converter)
+                        : this.converter.concatenate(that.converter)));
+                return;
+            }
+        } while ((relativeIndex = -relativeIndex) < 0);
+        super.tryConcatenate(context);
     }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
index f259475296..7c7550a465 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
@@ -30,7 +30,6 @@ import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.matrix.Matrices;
-import org.apache.sis.referencing.util.MathTransformsOrFactory;
 import org.apache.sis.referencing.operation.provider.Wraparound;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
@@ -419,21 +418,29 @@ public class WraparoundTransform extends AbstractMathTransform implements Serial
      * Only dimensions that do not depend on {@link #wraparoundDimension} can be moved.
      */
     @Override
-    protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
-                                           final MathTransformFactory factory) throws FactoryException
-    {
-        /*
-         * If the other transform is also a `WraparoundTransform` for the same dimension,
-         * then there is no need to concatenate those two consecutive redudant transforms.
-         * Keep the first transform because it will be the last one (having precedence)
-         * in inverse transform.
-         */
-        if (other instanceof WraparoundTransform && equalsIgnoreInverse((WraparoundTransform) other)) {
-            return applyOtherFirst ? other : this;
-        }
-        final List<MathTransform> steps = MathTransforms.getSteps(other);
-        final int count = steps.size();
-        if (count >= 2) {
+    protected void tryConcatenate(final Joiner context) throws FactoryException {
+        int relativeIndex = +1;
+        do {
+            /*
+             * If the other transform is also a `WraparoundTransform` for the same dimension,
+             * then there is no need to concatenate those two consecutive redudant transforms.
+             * Keep the first transform because it will be the last one (having precedence)
+             * in inverse transform.
+             */
+            final boolean applyOtherFirst = (relativeIndex < 0);
+            final MathTransform other = context.getTransform(relativeIndex).orElse(null);
+            if (other instanceof WraparoundTransform && equalsIgnoreInverse((WraparoundTransform) other)) {
+                context.replace(relativeIndex, applyOtherFirst ? other : this);
+                return;
+            }
+            /*
+             * The rest of this method inspects a chain of 2 or more transforms before this transform.
+             */
+            final List<MathTransform> steps = MathTransforms.getSteps(other);
+            final int count = steps.size();
+            if (count < 2) {
+                continue;
+            }
             final MathTransform middleTr = steps.get(applyOtherFirst ? count - 1 : 0);
             Matrix middle = MathTransforms.getMatrix(middleTr);
             if (middle != null) try {
@@ -444,10 +451,9 @@ public class WraparoundTransform extends AbstractMathTransform implements Serial
                  * the matrix completely, which increase the chances to multiply (outside
                  * this method) with another matrix.
                  */
-                final MathTransformsOrFactory mf = MathTransformsOrFactory.wrap(factory);
                 boolean modified = false;
                 MathTransform step1 = this;
-                MathTransform move = movable(middleTr, middle, mf);
+                MathTransform move = movable(middleTr, middle, context.factory);
                 if (move != null) {
                     /*
                      * Update the middle matrix with everything that we could not put in `move`.
@@ -466,7 +472,7 @@ public class WraparoundTransform extends AbstractMathTransform implements Serial
                     final Matrix remaining = remaining(applyOtherFirst, move, middle);
                     final WraparoundTransform redim = redim(applyOtherFirst, remaining);
                     if (redim != null) {
-                        step1    = mf.concatenate(applyOtherFirst, move, redim);
+                        step1    = context.concatenate(move, redim, applyOtherFirst);
                         middle   = remaining;
                         modified = true;
                     }
@@ -479,12 +485,12 @@ public class WraparoundTransform extends AbstractMathTransform implements Serial
                 MathTransform step2 = steps.get(applyOtherFirst ? count - 2 : 1);
                 if (step2 instanceof WraparoundTransform) {
                     WraparoundTransform redim = (WraparoundTransform) step2;
-                    move = redim.movable(null, middle, mf);
+                    move = redim.movable(null, middle, context.factory);
                     if (move != null) {
                         final Matrix remaining = remaining(!applyOtherFirst, move, middle);
                         redim = redim.redim(!applyOtherFirst, remaining);
                         if (redim != null) {
-                            step2    = mf.concatenate(applyOtherFirst, redim, move);
+                            step2    = context.concatenate(redim, move, applyOtherFirst);
                             middle   = remaining;
                             modified = true;
                         }
@@ -513,28 +519,28 @@ public class WraparoundTransform extends AbstractMathTransform implements Serial
                  *         that matrix closer to an identity matrix.
                  */
                 if (modified) {
-                    MathTransform tr = mf.linear(middle);
+                    MathTransform tr = context.factory.createAffineTransform(middle);
                     if (tr.getClass() != middleTr.getClass()) {               // See above comment for rational.
-                        tr = mf.concatenate(applyOtherFirst, tr, step2);
-                        tr = mf.concatenate(applyOtherFirst, step1, tr);
+                        tr = context.concatenate(tr, step2, applyOtherFirst);
+                        tr = context.concatenate(step1, tr, applyOtherFirst);
                         if (applyOtherFirst) {
                             for (int i = count-2; --i >= 0;) {
-                                tr = mf.concatenate(steps.get(i), tr);
+                                tr = context.factory.createConcatenatedTransform(steps.get(i), tr);
                             }
                         } else {
                             for (int i = 2; i < count; i++) {
-                                tr = mf.concatenate(tr, steps.get(i));
+                                tr = context.factory.createConcatenatedTransform(tr, steps.get(i));
                             }
                         }
-                        return tr;
+                        context.replace(relativeIndex, tr);
+                        return;
                     }
                 }
             } catch (NoninvertibleTransformException e) {
                 // Should not happen. But if it is the case, just abandon the optimization effort.
                 Logging.recoverableException(LOGGER, getClass(), "tryConcatenate", e);
             }
-        }
-        return null;
+        } while ((relativeIndex = -relativeIndex) < 0);
     }
 
     /**
@@ -546,7 +552,7 @@ public class WraparoundTransform extends AbstractMathTransform implements Serial
      * @param  factory  wrapper of the factory given to {@link #tryConcatenate tryConcatenate(…)}.
      * @return a transform processing only the movable parts, or {@code null} if identity.
      */
-    private MathTransform movable(MathTransform tr, Matrix matrix, final MathTransformsOrFactory factory)
+    private MathTransform movable(MathTransform tr, Matrix matrix, final MathTransformFactory factory)
             throws FactoryException
     {
         final long moveAll = Numerics.bitmask(dimension) - 1;
@@ -590,7 +596,7 @@ public class WraparoundTransform extends AbstractMathTransform implements Serial
                 // OTherwise `matrix` can be used as-is.
             }
             if (!matrix.isIdentity()) {
-                return factory.linear(matrix);
+                return factory.createAffineTransform(matrix);
             }
         }
         return null;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/CoordinateOperations.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/CoordinateOperations.java
index 7653014f84..960cb87e4b 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/CoordinateOperations.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/CoordinateOperations.java
@@ -86,7 +86,7 @@ public final class CoordinateOperations extends Static {
     public static final ThreadLocal<Supplier<double[]>> CONSTANT_COORDINATES = new ThreadLocal<>();
 
     /**
-     * Cached values or {@link #wrapAroundChanges wrapAroundChanges(…)}, created when first needed.
+     * Cached values of {@link #wrapAroundChanges wrapAroundChanges(…)}, created when first needed.
      * Indices are bit masks computed by {@link #changes changes(…)}. Since the most common "wrap around" axes
      * are longitude at dimension 0 or 1, and some measurement of time (in climatology) at dimension 2 or 3,
      * then the most likely values are (binary digits):
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/MathTransformsOrFactory.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/MathTransformsOrFactory.java
deleted file mode 100644
index 1d24ddd1a3..0000000000
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/MathTransformsOrFactory.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * 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.util;
-
-import org.opengis.util.FactoryException;
-import org.opengis.referencing.operation.Matrix;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.MathTransformFactory;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
-import org.apache.sis.referencing.operation.transform.PassThroughTransform;
-
-
-/**
- * Proxy to {@link MathTransforms} method which can be redirected to a {@link MathTransformFactory}.
- * The method signature in this class mirrors the one in {@link MathTransforms}. We do not provide
- * this functionality as a {@link MathTransformFactory} implementation because we do not override
- * all methods.
- *
- * @author  Martin Desruisseaux (Geomatys)
- */
-public class MathTransformsOrFactory {
-    /**
-     * The unique instance to use when no {@link MathTransformFactory} is specified.
-     */
-    private static final MathTransformsOrFactory INSTANCE = new MathTransformsOrFactory();
-
-    /**
-     * Do not allow instantiation of this class, except the inner sub-class.
-     */
-    private MathTransformsOrFactory() {
-    }
-
-    /**
-     * Returns the instance to use for the given factory.
-     *
-     * @param  factory  the factory, which may be {@code null}.
-     * @return the instance to use.
-     */
-    public static MathTransformsOrFactory wrap(final MathTransformFactory factory) {
-        return (factory != null) ? new Specified(factory) : INSTANCE;
-    }
-
-    /**
-     * Creates an arbitrary linear transform from the specified matrix.
-     *
-     * @param  matrix  the matrix used to define the linear transform.
-     * @return the linear (usually affine) transform.
-     * @throws FactoryException if a factory is wrapped and cannot perform the operation.
-     */
-    public MathTransform linear(final Matrix matrix) throws FactoryException {
-        return MathTransforms.linear(matrix);
-    }
-
-    /**
-     * Creates a transform which passes through a subset of coordinates to another transform.
-     *
-     * @param  firstAffectedCoordinate  index of the first affected coordinate.
-     * @param  subTransform             the sub-transform to apply on modified coordinates.
-     * @param  numTrailingCoordinates   number of trailing coordinates to pass through.
-     * @return a pass-through transform, potentially as a {@link PassThroughTransform} instance but not necessarily.
-     * @throws FactoryException if a factory is wrapped and cannot perform the operation.
-     */
-    public MathTransform passThrough(int firstAffectedCoordinate, MathTransform subTransform, int numTrailingCoordinates) throws FactoryException {
-        return MathTransforms.passThrough(firstAffectedCoordinate, subTransform, numTrailingCoordinates);
-    }
-
-    /**
-     * Concatenates the two given transforms.
-     *
-     * @param  tr1  the first math transform.
-     * @param  tr2  the second math transform.
-     * @return the concatenated transform.
-     * @throws FactoryException if a factory is wrapped and cannot perform the operation.
-     */
-    public MathTransform concatenate(MathTransform tr1, MathTransform tr2) throws FactoryException {
-        return MathTransforms.concatenate(tr1, tr2);
-    }
-
-    /**
-     * Concatenates the two given transforms, switching their order if {@code applyOtherFirst} is {@code true}.
-     *
-     * @param  tr               the first math transform.
-     * @param  other            the second math transform.
-     * @param  applyOtherFirst  whether {@code other} should be first in the concatenation chain.
-     * @return the concatenated transform.
-     * @throws FactoryException if a factory is wrapped and cannot perform the operation.
-     */
-    public final MathTransform concatenate(boolean applyOtherFirst, MathTransform tr, MathTransform other) throws FactoryException {
-        if (applyOtherFirst) {
-            return concatenate(other, tr);
-        } else {
-            return concatenate(tr, other);
-        }
-    }
-
-    /**
-     * A {@link MathTransformsOrFactory} implementation which delegate method calls to a {@link MathTransformFactory}
-     * specified by the user.
-     */
-    private static final class Specified extends MathTransformsOrFactory {
-        /** The factory where to delegate method calls. */
-        private final MathTransformFactory factory;
-
-        /** Creates a new instance delegating transform creations to the given factory. */
-        Specified(final MathTransformFactory factory) {
-            this.factory = factory;
-        }
-
-        /** Delegate to {@link MathTransformFactory#createAffineTransform(Matrix)}. */
-        @Override public MathTransform linear(Matrix matrix) throws FactoryException {
-            return factory.createAffineTransform(matrix);
-        }
-
-        /** Delegate to {@link MathTransformFactory#createPassThroughTransform(int, MathTransform, int)}. */
-        @Override public MathTransform passThrough(int firstAffectedCoordinate, MathTransform subTransform, int numTrailingCoordinates) throws FactoryException {
-            return factory.createPassThroughTransform(firstAffectedCoordinate, subTransform, numTrailingCoordinates);
-        }
-
-        /** Delegate to {@link MathTransformFactory#createConcatenatedTransform(MathTransform, MathTransform)}. */
-        @Override public MathTransform concatenate(MathTransform tr, MathTransform other) throws FactoryException {
-            return factory.createConcatenatedTransform(tr, other);
-        }
-    }
-}
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/ReferencingUtilities.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/ReferencingUtilities.java
index fb136a72ac..d0bb28fc88 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/ReferencingUtilities.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/ReferencingUtilities.java
@@ -546,6 +546,19 @@ public final class ReferencingUtilities extends Static {
         return null;
     }
 
+    /**
+     * Returns the given factory instance if non-null, or a default instance otherwise.
+     *
+     * @param  factory  the factory, which may be {@code null}.
+     * @return the instance to use.
+     */
+    public static MathTransformFactory nonNull(MathTransformFactory factory) {
+        if (factory == null) {
+            factory = DefaultMathTransformFactory.provider().caching(false);
+        }
+        return factory;
+    }
+
     /**
      * Creates a context with source and target ellipsoids and coordinate systems inferred from the given CRS.
      * The ellipsoids will be non-null only if the given CRS is geodetic (geographic or geocentric).
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransformTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransformTest.java
index 7aa41dee82..10beeb15de 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransformTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransformTest.java
@@ -32,8 +32,7 @@ import org.apache.sis.measure.Units;
 
 // Test dependencies
 import org.junit.Test;
-import static org.junit.Assert.*;
-import static org.opengis.test.Assert.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.referencing.operation.provider.GeocentricTranslationTest;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.DependsOn;
@@ -242,7 +241,7 @@ public final class EllipsoidToCentricTransformTest extends MathTransformTestCase
     }
 
     /**
-     * Tests {@link EllipsoidToCentricTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}.
+     * Tests {@link EllipsoidToCentricTransform#tryConcatenate(AbstractMathTransform.Joiner)}.
      * The test creates <q>Geographic 3D to 2D conversion</q>, <q>Geographic/Geocentric conversions</q>
      * and <q>Geocentric translation</q> transforms, then concatenate them.
      *
@@ -259,25 +258,30 @@ public final class EllipsoidToCentricTransformTest extends MathTransformTestCase
         final Iterator<MathTransform> it = MathTransforms.getSteps(transform).iterator();
         MathTransform step;
 
-        assertInstanceOf("Degrees to radians", LinearTransform.class, step = it.next());
-        assertEquals("sourceDimensions", 2, step.getSourceDimensions());
-        assertEquals("tourceDimensions", 2, step.getTargetDimensions());
+        // Degrees to radians
+        assertInstanceOf(LinearTransform.class, step = it.next());
+        assertEquals(2, step.getSourceDimensions());
+        assertEquals(2, step.getTargetDimensions());
 
-        assertInstanceOf("Ellipsoid to geocentric", EllipsoidToCentricTransform.class, step = it.next());
-        assertEquals("sourceDimensions", 2, step.getSourceDimensions());
-        assertEquals("tourceDimensions", 3, step.getTargetDimensions());
+        // Ellipsoid to geocentric
+        assertInstanceOf(EllipsoidToCentricTransform.class, step = it.next());
+        assertEquals(2, step.getSourceDimensions());
+        assertEquals(3, step.getTargetDimensions());
 
-        assertInstanceOf("Datum shift", LinearTransform.class, step = it.next());
-        assertEquals("sourceDimensions", 3, step.getSourceDimensions());
-        assertEquals("tourceDimensions", 3, step.getTargetDimensions());
+        // Datum shift
+        assertInstanceOf(LinearTransform.class, step = it.next());
+        assertEquals(3, step.getSourceDimensions());
+        assertEquals(3, step.getTargetDimensions());
 
-        assertInstanceOf("Geocentric to ellipsoid", AbstractMathTransform.Inverse.class, step = it.next());
-        assertEquals("sourceDimensions", 3, step.getSourceDimensions());
-        assertEquals("tourceDimensions", 2, step.getTargetDimensions());
+        // Geocentric to ellipsoid
+        assertInstanceOf(AbstractMathTransform.Inverse.class, step = it.next());
+        assertEquals(3, step.getSourceDimensions());
+        assertEquals(2, step.getTargetDimensions());
 
-        assertInstanceOf("Degrees to radians", LinearTransform.class, step = it.next());
-        assertEquals("sourceDimensions", 2, step.getSourceDimensions());
-        assertEquals("tourceDimensions", 2, step.getTargetDimensions());
+        // Radians to degrees
+        assertInstanceOf(LinearTransform.class, step = it.next());
+        assertEquals(2, step.getSourceDimensions());
+        assertEquals(2, step.getTargetDimensions());
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
index f2dad366c9..526d850976 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
@@ -31,9 +31,10 @@ import org.apache.sis.util.ArraysExt;
 
 // Test dependencies
 import org.junit.Test;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.DependsOn;
+import static org.apache.sis.test.Assertions.assertMessageContains;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.test.CalculationType;
@@ -68,20 +69,13 @@ public final class PassThroughTransformTest extends MathTransformTestCase {
     @Test
     public void testIllegalArgument() {
         final MathTransform subTransform = MathTransforms.identity(1);
-        try {
-            MathTransforms.passThrough(-1, subTransform, 0);
-            fail("An illegal argument should have been detected");
-        } catch (IllegalArgumentException e) {
-            final String message = e.getMessage();
-            assertTrue(message, message.contains("firstAffectedCoordinate"));
-        }
-        try {
-            MathTransforms.passThrough(0, subTransform, -1);
-            fail("An illegal argument should have been detected");
-        } catch (IllegalArgumentException e) {
-            final String message = e.getMessage();
-            assertTrue(message, message.contains("numTrailingCoordinates"));
-        }
+        IllegalArgumentException e;
+
+        e = assertThrows(IllegalArgumentException.class, () -> MathTransforms.passThrough(-1, subTransform, 0));
+        assertMessageContains(e, "firstAffectedCoordinate");
+
+        e = assertThrows(IllegalArgumentException.class, () -> MathTransforms.passThrough(0, subTransform, -1));
+        assertMessageContains(e, "numTrailingCoordinates");
     }
 
     /**
@@ -167,15 +161,13 @@ public final class PassThroughTransformTest extends MathTransformTestCase {
                 final int numAdditionalOrdinates = firstAffectedCoordinate + numTrailingCoordinates;
                 transform = MathTransforms.passThrough(firstAffectedCoordinate, subTransform, numTrailingCoordinates);
                 if (numAdditionalOrdinates == 0) {
-                    assertSame("Failed to recognize that no passthrough was needed.", subTransform, transform);
+                    assertSame(subTransform, transform, "Failed to recognize that no passthrough was needed.");
                     continue;
                 }
                 assertNotSame(subTransform, transform);
-                assertTrue   ("Wrong transform class.", expectedClass.isInstance(transform));
-                assertEquals ("Wrong number of source dimensions.",
-                        subTransform.getSourceDimensions() + numAdditionalOrdinates, transform.getSourceDimensions());
-                assertEquals ("Wrong number of target dimensions.",
-                        subTransform.getTargetDimensions() + numAdditionalOrdinates, transform.getTargetDimensions());
+                assertInstanceOf(expectedClass, transform);
+                assertEquals(subTransform.getSourceDimensions() + numAdditionalOrdinates, transform.getSourceDimensions());
+                assertEquals(subTransform.getTargetDimensions() + numAdditionalOrdinates, transform.getTargetDimensions());
                 verifyTransform(subTransform, firstAffectedCoordinate);
             }
         }
@@ -250,7 +242,7 @@ public final class PassThroughTransformTest extends MathTransformTestCase {
          */
         final float[] sourceAsFloat = ArraysExt.copyAsFloats(passthroughData);
         final float[] targetAsFloat = verifyConsistency(sourceAsFloat);
-        assertEquals("Unexpected length of transformed array.", expectedData.length, targetAsFloat.length);
+        assertEquals(expectedData.length, targetAsFloat.length, "Unexpected length of transformed array.");
         /*
          * We use a relatively high tolerance threshold because result are
          * computed using inputs stored as float values.
@@ -264,50 +256,59 @@ public final class PassThroughTransformTest extends MathTransformTestCase {
     }
 
     /**
-     * Tests {@link PassThroughTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}.
-     * This tests creates a non-linear transform of 6→7 dimensions, then applies a filter keeping only
-     * target dimensions 1, 4 and 6 (corresponding to source dimensions 1 and 5).
+     * Tests {@link PassThroughTransform#tryConcatenate(AbstractMathTransform.Joiner)}.
+     * This tests creates a non-linear transform of 6→7 dimensions, then applies a filter
+     * keeping only target dimensions 1, 4 and 6 (corresponding to source dimensions 1 and 5).
      *
      * @throws FactoryException if an error occurred while combining the transforms.
      */
     @Test
     public void testTryConcatenate() throws FactoryException {
-        PassThroughTransform ps = new PassThroughTransform(2, new PseudoTransform(2, 3), 2);
-        MathTransform c = ps.tryConcatenate(false, MathTransforms.linear(Matrices.create(4, 8, new double[] {
+        Matrix m = Matrices.create(4, 8, new double[] {
                 0, 1, 0, 0, 0, 0, 0, 0,
                 0, 0, 0, 0, 1, 0, 0, 0,
                 0, 0, 0, 0, 0, 0, 1, 0,
-                0, 0, 0, 0, 0, 0, 0, 1})), null);
+                0, 0, 0, 0, 0, 0, 0, 1});
+
+        final var ps      = new PassThroughTransform(2, new PseudoTransform(2, 3), 2);
+        final var factory = DefaultMathTransformFactory.provider().caching(false);
+        final var context = new AbstractMathTransform.Joiner(List.of(ps, MathTransforms.linear(m)), 0, factory);
+        ps.tryConcatenate(context);
+        MathTransform c = context.replacement;
 
         final List<MathTransform> steps = MathTransforms.getSteps(c);
-        assertEquals("Number of steps", 3, steps.size());
+        assertEquals(3, steps.size());
         /*
          * We need to remove source dimensions 0, 2, 3 and 4. We cannot remove dimensions 2 and 3 before
          * pass-through because they are used by the sub-transform. It leaves us dimensions 0 and 4 which
          * can be removed here.
          */
-        assertMatrixEquals("Expected removal of dimensions 0 and 4 before pass-through", Matrices.create(5, 7, new double[] {
+        m = Matrices.create(5, 7, new double[] {
                 0, 1, 0, 0, 0, 0, 0,
                 0, 0, 1, 0, 0, 0, 0,
                 0, 0, 0, 1, 0, 0, 0,
                 0, 0, 0, 0, 0, 1, 0,
-                0, 0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(0)), null);
+                0, 0, 0, 0, 0, 0, 1});
+        assertMatrixEquals("Expected removal of dimensions 0 and 4 before pass-through",
+                           m, MathTransforms.getMatrix(steps.get(0)), null);
         /*
          * The number of pass-through dimensions have decreased from 2 to 1 on both sides of the sub-transform.
          */
         final PassThroughTransform reduced = (PassThroughTransform) steps.get(1);
-        assertEquals("firstAffectedCoordinate", 1, reduced.firstAffectedCoordinate);
-        assertEquals("numTrailingCoordinates",  1, reduced.numTrailingCoordinates);
-        assertSame  ("subTransform", ps.subTransform, reduced.subTransform);
+        assertEquals(1, reduced.firstAffectedCoordinate);
+        assertEquals(1, reduced.numTrailingCoordinates);
+        assertSame(ps.subTransform, reduced.subTransform);
         /*
          * We still have to remove source dimensions 2 and 3. Since we removed dimension 0 in previous step,
          * the indices of dimensions to removed have shifted to 1 and 2.
          */
-        assertMatrixEquals("Expected removal of dimensions 1 and 2 after pass-through", Matrices.create(4, 6, new double[] {
+        m = Matrices.create(4, 6, new double[] {
                 1, 0, 0, 0, 0, 0,
                 0, 0, 0, 1, 0, 0,
                 0, 0, 0, 0, 1, 0,
-                0, 0, 0, 0, 0, 1}), MathTransforms.getMatrix(steps.get(2)), null);
+                0, 0, 0, 0, 0, 1});
+        assertMatrixEquals("Expected removal of dimensions 1 and 2 after pass-through",
+                           m, MathTransforms.getMatrix(steps.get(2)), null);
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
index 6d7483902e..134d9e995f 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/TransformSeparatorTest.java
@@ -33,12 +33,12 @@ import org.apache.sis.geometry.GeneralDirectPosition;
 
 // Test dependencies
 import org.junit.Test;
-import static org.junit.Assert.*;
-import static org.opengis.test.Assert.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.DependsOnMethod;
+import static org.apache.sis.test.Assertions.assertMessageContains;
 import org.apache.sis.referencing.datum.HardCodedDatum;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -67,54 +67,43 @@ public final class TransformSeparatorTest extends TestCase {
     @Test
     public void testArgumentChecks() {
         final TransformSeparator s = new TransformSeparator(MathTransforms.identity(8));
-        try {
-            s.getSourceDimensions();
-            fail("Shall not return unspecified dimensions.");
-        } catch (IllegalStateException e) {
-            // This is the expected exception.
-        }
-        try {
-            s.getTargetDimensions();
-            fail("Shall not return unspecified dimensions.");
-        } catch (IllegalStateException e) {
-            // This is the expected exception.
-        }
+        RuntimeException e;
+        /*
+         * Shall not return unspecified dimensions.
+         */
+        e = assertThrows(IllegalStateException.class, () -> s.getSourceDimensions());
+        assertMessageContains(e);
+
+        e = assertThrows(IllegalStateException.class, () -> s.getTargetDimensions());
+        assertMessageContains(e);
+        /*
+         * Shall not accept non-increasing values. While the values tested below are
+         * in increasing order in each method call, they are not in increasing order
+         * when taking in account the previous call.
+         */
         s.addSourceDimensionRange(1, 4);
         s.addTargetDimensions(0, 3, 4);
-        assertArrayEquals("sourceDimensions", new int[] {1, 2, 3}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {0, 3, 4}, s.getTargetDimensions());
-        try {
-            s.addSourceDimensions(3, 4, 5);
-            fail("Shall not accept non-increasing value.");
-        } catch (IllegalArgumentException e) {
-            // This is the expected exception.
-            assertTrue(e.getMessage().contains("dimensions[0]"));
-        }
-        try {
-            s.addTargetDimensionRange(3, 5);
-            fail("Shall not accept non-increasing value.");
-        } catch (IllegalArgumentException e) {
-            // This is the expected exception.
-            assertTrue(e.getMessage().contains("lower"));
-        }
+        assertArrayEquals(new int[] {1, 2, 3}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {0, 3, 4}, s.getTargetDimensions());
+
+        e = assertThrows(IllegalArgumentException.class, () -> s.addSourceDimensions(3, 4, 5));
+        assertMessageContains(e, "dimensions[0]");
+
+        e = assertThrows(IllegalArgumentException.class, () -> s.addTargetDimensionRange(3, 5));
+        assertMessageContains(e, "lower");
+        /*
+         * Shall not accept values out of range.
+         */
         s.addSourceDimensions(4, 6);
         s.addTargetDimensionRange(6, 8);
-        assertArrayEquals("sourceDimensions", new int[] {1, 2, 3, 4, 6}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {0, 3, 4, 6, 7}, s.getTargetDimensions());
-        try {
-            s.addSourceDimensions(8);
-            fail("Shall not accept value out of range.");
-        } catch (IllegalArgumentException e) {
-            // This is the expected exception.
-            assertTrue(e.getMessage().contains("dimensions[0]"));
-        }
-        try {
-            s.addTargetDimensions(3, 8);
-            fail("Shall not accept value out of range.");
-        } catch (IllegalArgumentException e) {
-            // This is the expected exception.
-            assertTrue(e.getMessage().contains("dimensions[0]"));
-        }
+        assertArrayEquals(new int[] {1, 2, 3, 4, 6}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {0, 3, 4, 6, 7}, s.getTargetDimensions());
+
+        e = assertThrows(IllegalArgumentException.class, () -> s.addSourceDimensions(8));
+        assertMessageContains(e, "dimensions[0]");
+
+        e = assertThrows(IllegalArgumentException.class, () -> s.addTargetDimensions(3, 8));
+        assertMessageContains(e, "dimensions[0]");
     }
 
     /**
@@ -134,9 +123,9 @@ public final class TransformSeparatorTest extends TestCase {
         /*
          * Trivial case: no dimension specified, we should get the transform unchanged.
          */
-        assertSame("transform", s.transform, s.separate());
-        assertArrayEquals("sourceDimensions", new int[] {0, 1, 2}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {0, 1, 2}, s.getTargetDimensions());
+        assertSame(s.transform, s.separate());
+        assertArrayEquals(new int[] {0, 1, 2}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {0, 1, 2}, s.getTargetDimensions());
         /*
          * Filter only target dimensions. This is the easiest non-trivial case since we just
          * need to drop some rows. There is no analysis to perform on the matrix values.
@@ -150,8 +139,8 @@ public final class TransformSeparatorTest extends TestCase {
         s.addTargetDimensions(0, 2);
         s.addSourceDimensionRange(0, 3);
         assertMatrixEquals("transform", matrix, ((LinearTransform) s.separate()).getMatrix(), STRICT);
-        assertArrayEquals("sourceDimensions", new int[] {0, 1, 2}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {0, 2},    s.getTargetDimensions());
+        assertArrayEquals(new int[] {0, 1, 2}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {0, 2},    s.getTargetDimensions());
         /*
          * Filter only source dimensions. Do not specify any target dimensions for now.
          * TransformSeparator needs to examine the matrix values and drop all target dimensions
@@ -164,8 +153,8 @@ public final class TransformSeparatorTest extends TestCase {
         s.clear();
         s.addSourceDimensions(1, 2);
         assertMatrixEquals("transform", matrix, ((LinearTransform) s.separate()).getMatrix(), STRICT);
-        assertArrayEquals("sourceDimensions", new int[] {1, 2}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {1},    s.getTargetDimensions());
+        assertArrayEquals(new int[] {1, 2}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {1},    s.getTargetDimensions());
         /*
          * Filter both source and target dimensions. Source dimensions 0 and 2 allow the target dimensions 0 and 2
          * (target dimension 1 is discarded because it depends on source dimension 1).  Then the target dimensions
@@ -179,8 +168,8 @@ public final class TransformSeparatorTest extends TestCase {
         s.addSourceDimensions(0, 2);
         s.addTargetDimensions(0);
         assertMatrixEquals("transform", matrix, ((LinearTransform) s.separate()).getMatrix(), STRICT);
-        assertArrayEquals("sourceDimensions", new int[] {0, 2}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {0},    s.getTargetDimensions());
+        assertArrayEquals(new int[] {0, 2}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {0},    s.getTargetDimensions());
         /*
          * Try again, but with the addition of a target dimension that TransformSeparator cannot keep.
          * It shall cause an exception to be thrown.
@@ -203,8 +192,8 @@ public final class TransformSeparatorTest extends TestCase {
             0, 0, 0, 1
         });
         assertMatrixEquals("transform", matrix, ((LinearTransform) s.separate()).getMatrix(), STRICT);
-        assertArrayEquals("sourceDimensions", new int[] {0, 1, 2}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {0, 1},    s.getTargetDimensions());
+        assertArrayEquals(new int[] {0, 1, 2}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {0, 1},    s.getTargetDimensions());
     }
 
     /**
@@ -235,8 +224,8 @@ public final class TransformSeparatorTest extends TestCase {
             0, 0, 1);
 
         tr = s.separate();
-        assertArrayEquals("sourceDimensions", new int[] {0, 1}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {0, 1}, s.getTargetDimensions());
+        assertArrayEquals(new int[] {0, 1}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {0, 1}, s.getTargetDimensions());
         assertMatrixEquals("transform", matrix, ((LinearTransform) tr).getMatrix(), STRICT);
         /*
          * Below is the less intuitive case. When asking for the first two dimensions, we usually expect the
@@ -253,8 +242,8 @@ public final class TransformSeparatorTest extends TestCase {
             0, 0, 1});
 
         tr = s.separate();
-        assertArrayEquals("sourceDimensions", new int[] {0, 1   }, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {0, 1, 2}, s.getTargetDimensions());
+        assertArrayEquals(new int[] {0, 1   }, s.getSourceDimensions());
+        assertArrayEquals(new int[] {0, 1, 2}, s.getTargetDimensions());
         assertMatrixEquals("transform", matrix, ((LinearTransform) tr).getMatrix(), STRICT);
     }
 
@@ -295,9 +284,9 @@ public final class TransformSeparatorTest extends TestCase {
         s.addSourceDimensions(0, 1);
         s.addTargetDimensions(0, 1);
         final Iterator<MathTransform> it = MathTransforms.getSteps(s.separate()).iterator();
-        assertInstanceOf("normalize",   LinearTransform.class,             it.next());
-        assertInstanceOf("transform",   EllipsoidToCentricTransform.class, it.next());
-        assertInstanceOf("denormalize", LinearTransform.class,             it.next());
+        assertInstanceOf(LinearTransform.class,             it.next(), "normalize");
+        assertInstanceOf(EllipsoidToCentricTransform.class, it.next(), "transform");
+        assertInstanceOf(LinearTransform.class,             it.next(), "denormalize");
         assertFalse(it.hasNext());
     }
 
@@ -319,9 +308,9 @@ public final class TransformSeparatorTest extends TestCase {
         /*
          * Trivial case: no dimension specified, we should get the transform unchanged.
          */
-        assertSame("transform", s.transform, s.separate());
-        assertArrayEquals("sourceDimensions", new int[] {0, 1, 2, 3, 4, 5, 6},    s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {0, 1, 2, 3, 4, 5, 6, 7}, s.getTargetDimensions());
+        assertSame(s.transform, s.separate());
+        assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6},    s.getSourceDimensions());
+        assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6, 7}, s.getTargetDimensions());
         /*
          * Filter only target dimensions. If the requested indices overlap the pass-through transform,
          * TransformSeparator will just concatenate a matrix after the transform for dropping dimensions.
@@ -336,8 +325,8 @@ public final class TransformSeparatorTest extends TestCase {
         s.addTargetDimensions(1, 2, 7);
         s.addSourceDimensionRange(0, 7);
         MathTransform result = s.separate();
-        assertArrayEquals("sourceDimensions", new int[] {0, 1, 2, 3, 4, 5, 6}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {1, 2, 7}, s.getTargetDimensions());
+        assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {1, 2, 7}, s.getTargetDimensions());
         final Random random = TestUtilities.createRandomNumberGenerator();
         compare(s.transform, MathTransforms.linear(expected), result, random);
         /*
@@ -354,9 +343,9 @@ public final class TransformSeparatorTest extends TestCase {
         s.addTargetDimensions(1, 5, 7);
         s.addSourceDimensionRange(0, 7);
         result = s.separate();
-        assertArrayEquals ("sourceDimensions", new int[] {0, 1, 2, 3, 4, 5, 6}, s.getSourceDimensions());
-        assertArrayEquals ("targetDimensions", new int[] {1, 5, 7}, s.getTargetDimensions());
-        assertInstanceOf  ("separate()", LinearTransform.class, result);
+        assertArrayEquals (new int[] {0, 1, 2, 3, 4, 5, 6}, s.getSourceDimensions());
+        assertArrayEquals (new int[] {1, 5, 7}, s.getTargetDimensions());
+        assertInstanceOf  (LinearTransform.class, result);
         assertMatrixEquals("separate().transform2", expected, ((LinearTransform) result).getMatrix(), STRICT);
         /*
          * Filter source dimensions. If we ask only for dimensions not in the pass-through transform,
@@ -370,9 +359,9 @@ public final class TransformSeparatorTest extends TestCase {
         s.clear();
         s.addSourceDimensions(0, 6);
         result = s.separate();
-        assertArrayEquals ("sourceDimensions", new int[] {0, 6}, s.getSourceDimensions());
-        assertArrayEquals ("targetDimensions", new int[] {0, 7}, s.getTargetDimensions());
-        assertInstanceOf  ("separate()", LinearTransform.class, result);
+        assertArrayEquals (new int[] {0, 6}, s.getSourceDimensions());
+        assertArrayEquals (new int[] {0, 7}, s.getTargetDimensions());
+        assertInstanceOf  (LinearTransform.class, result);
         assertMatrixEquals("separate().transform2", expected, ((LinearTransform) result).getMatrix(), STRICT);
         /*
          * Filter source dimensions, now with overlapping in the pass-through transform.
@@ -381,19 +370,19 @@ public final class TransformSeparatorTest extends TestCase {
         s.clear();
         s.addSourceDimensions(1, 2, 3, 4, 5);
         result = s.separate();
-        assertArrayEquals("sourceDimensions", new int[] {1, 2, 3, 4, 5}, s.getSourceDimensions());
-        assertArrayEquals("targetDimensions", new int[] {1, 2, 3, 4, 5, 6}, s.getTargetDimensions());
-        assertInstanceOf ("separate()", PassThroughTransform.class, result);
-        assertSame  ("subTransform",  nonLinear, ((PassThroughTransform) result).subTransform);
-        assertEquals("firstAffectedCoordinate", 1, ((PassThroughTransform) result).firstAffectedCoordinate);
-        assertEquals("numTrailingCoordinates",  2, ((PassThroughTransform) result).numTrailingCoordinates);
+        assertArrayEquals(new int[] {1, 2, 3, 4, 5}, s.getSourceDimensions());
+        assertArrayEquals(new int[] {1, 2, 3, 4, 5, 6}, s.getTargetDimensions());
+        assertInstanceOf (PassThroughTransform.class, result);
+        assertSame  (nonLinear, ((PassThroughTransform) result).subTransform);
+        assertEquals(1, ((PassThroughTransform) result).firstAffectedCoordinate);
+        assertEquals(2, ((PassThroughTransform) result).numTrailingCoordinates);
     }
 
     /**
      * Compares coordinate computed by a reference with coordinates computed by the transform to test.
      * We use this method when we cannot easily analyze the {@link MathTransform} created by the test
      * case, for example because it may have been rearranged in arbitrary ways for optimization purpose
-     * (e.g. {@link PassThroughTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}).
+     * (e.g. {@link PassThroughTransform#tryConcatenate(AbstractMathTransform.Joiner)}).
      *
      * @param  tr1     first half of the transform to use as a reference.
      * @param  tr2     second half of the transform to use as a reference.
@@ -441,16 +430,16 @@ public final class TransformSeparatorTest extends TestCase {
                            MathTransforms.getMatrix(sep.separate()), STRICT);
         sep.clear();
         sep.addSourceDimensionRange(2, 5);
-        assertSame("subTransform", nonLinear, sep.separate());
+        assertSame(nonLinear, sep.separate());
 
         sep.clear();
         sep.addSourceDimensionRange(1, 5);
         MathTransform mt = sep.separate();
-        assertInstanceOf("separate()", PassThroughTransform.class, mt);
+        assertInstanceOf(PassThroughTransform.class, mt);
         final PassThroughTransform ps = ((PassThroughTransform) mt);
-        assertEquals("firstAffectedCoordinate", 1, ps.firstAffectedCoordinate);
-        assertEquals("numTrailingCoordinates",  0, ps.numTrailingCoordinates);
-        assertSame  ("subTransform",    nonLinear, ps.subTransform);
+        assertEquals(1, ps.firstAffectedCoordinate);
+        assertEquals(0, ps.numTrailingCoordinates);
+        assertSame(nonLinear, ps.subTransform);
     }
 
     /**
@@ -469,7 +458,7 @@ public final class TransformSeparatorTest extends TestCase {
         final TransformSeparator sep = new TransformSeparator(passthrough2);
         sep.addSourceDimensionRange(3, 7);
         MathTransform mt = sep.separate();
-        assertInstanceOf("separate()", ConcatenatedTransform.class, mt);
+        assertInstanceOf(ConcatenatedTransform.class, mt);
         assertMatrixEquals("Leading passthrough dimensions", Matrices.create(5, 5, new double[] {
             3, 0, 0, 0, 0,
             0, 2, 0, 0, 0,
@@ -478,11 +467,11 @@ public final class TransformSeparatorTest extends TestCase {
             0, 0, 0, 0, 1}), MathTransforms.getMatrix(((ConcatenatedTransform) mt).transform1), STRICT);
 
         mt = ((ConcatenatedTransform) mt).transform2;
-        assertInstanceOf("subTransform", PassThroughTransform.class, mt);
+        assertInstanceOf(PassThroughTransform.class, mt);
         final PassThroughTransform ps = ((PassThroughTransform) mt);
-        assertEquals("firstAffectedCoordinate", 1, ps.firstAffectedCoordinate);
-        assertEquals("numTrailingCoordinates",  0, ps.numTrailingCoordinates);
-        assertSame  ("subTransform",    nonLinear, ps.subTransform);
+        assertEquals(1, ps.firstAffectedCoordinate);
+        assertEquals(0, ps.numTrailingCoordinates);
+        assertSame(nonLinear, ps.subTransform);
     }
 
     /**
@@ -502,7 +491,7 @@ public final class TransformSeparatorTest extends TestCase {
          */
         TransformSeparator s = new TransformSeparator(tr);
         s.addSourceDimensionRange(0, tr.getSourceDimensions());
-        assertSame("No source dimensions should be trimmed if not requested.", tr, s.separate());
+        assertSame(tr, s.separate(), "No source dimensions should be trimmed if not requested.");
         assertArrayEquals(new int[] {0, 1, 2}, s.getSourceDimensions());
         assertArrayEquals(new int[] {0, 1   }, s.getTargetDimensions());
         /*
@@ -514,7 +503,7 @@ public final class TransformSeparatorTest extends TestCase {
             0,   0,     1);
         s.clear();
         MathTransform reduced = s.separate();
-        assertNotEquals("separate()", tr, reduced);
+        assertNotEquals(tr, reduced);
         assertArrayEquals(new int[] {0, 1}, s.getSourceDimensions());
         assertArrayEquals(new int[] {0, 1}, s.getTargetDimensions());
         assertMatrixEquals("separate()", expected, MathTransforms.getMatrix(reduced), STRICT);
@@ -528,7 +517,7 @@ public final class TransformSeparatorTest extends TestCase {
 
         s = new TransformSeparator(tr);
         reduced = s.separate();
-        assertNotEquals("separate()", tr, reduced);
+        assertNotEquals(tr, reduced);
         assertArrayEquals(new int[] {1, 2}, s.getSourceDimensions());
         assertArrayEquals(new int[] {0, 1}, s.getTargetDimensions());
         assertMatrixEquals("separate()", new Matrix3(
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/WraparoundTransformTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/WraparoundTransformTest.java
index 9422dfa562..bad4ebdda3 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/WraparoundTransformTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/WraparoundTransformTest.java
@@ -19,7 +19,6 @@ package org.apache.sis.referencing.operation.transform;
 import java.util.Map;
 import java.util.List;
 import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.util.WraparoundApplicator;
 import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
@@ -29,7 +28,7 @@ import org.apache.sis.referencing.cs.AxesConvention;
 
 // Test dependencies
 import org.junit.Test;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 
@@ -75,14 +74,14 @@ public final class WraparoundTransformTest extends TestCase {
             45, -100 + 360, 90,             // Wraparound shall be applied.
             60,  -80,       70,             // No wraparound because already in [-90 … 270]° range.
             12,  300 - 360, 33              // Wraparound in opposite direction.
-        }, targets, STRICT);
+        }, targets);
 
         inverse.transform(sources, 0, targets, 0, 3);
         assertArrayEquals(new double[] {
             45, -100 + 360, 90,             // Wraparound shall be applied.
             60,  -80 + 360, 70,             // Idem.
             12,  300,       33,             // No wraparound because already in [ 20 … 380]° range.
-        }, targets, STRICT);
+        }, targets);
     }
 
     /**
@@ -138,13 +137,13 @@ public final class WraparoundTransformTest extends TestCase {
              6, 260,
             18, 160,
             27, 200,
-             9,  40}, pts, STRICT);
+             9,  40}, pts);
     }
 
     /**
      * Tests wraparound on two axes. We expect two instances of {@link WraparoundTransform} without linear
      * transform between them. The absence of separation between the two {@link WraparoundTransform}s is an
-     * indirect test of {@link WraparoundTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}.
+     * indirect test of {@link WraparoundTransform#tryConcatenate(AbstractMathTransform.Joiner)}.
      *
      * @throws TransformException if a coordinate cannot be computed.
      */