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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 2fb91f995e Stronger consolidation of the paths by which a geometry can 
be transformed. This consolidation allows us to add an optional verification of 
transform domain.
2fb91f995e is described below

commit 2fb91f995ecb91c43aa1f118fe294fa7474c5765
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Oct 30 19:19:31 2026 +0100

    Stronger consolidation of the paths by which a geometry can be transformed.
    This consolidation allows us to add an optional verification of transform 
domain.
---
 .../org/apache/sis/feature/internal/Resources.java |   5 +
 .../sis/feature/internal/Resources.properties      |   1 +
 .../sis/feature/internal/Resources_fr.properties   |   1 +
 .../apache/sis/filter/BinaryGeometryFilter.java    |   7 +-
 .../org/apache/sis/filter/sqlmm/ST_Transform.java  |   2 +-
 .../org/apache/sis/filter/sqlmm/TwoGeometries.java |   7 +-
 .../sis/geometry/wrapper/GeometryWrapper.java      | 115 ++++++-------
 .../geometry/wrapper/SpatialOperationContext.java  | 177 ++++++++++-----------
 .../geometry/wrapper/jts/GeometryTransform.java    |   2 +
 .../org/apache/sis/geometry/wrapper/jts/JTS.java   |   4 +-
 .../apache/sis/geometry/wrapper/jts/Wrapper.java   |  19 ++-
 .../operation/AbstractCoordinateOperation.java     |   5 +-
 .../operation/projection/AuthalicMercator.java     |   5 +-
 .../referencing/operation/projection/Mercator.java |   8 +-
 .../operation/projection/NormalizedProjection.java |  42 ++---
 .../operation/projection/TransverseMercator.java   |  10 +-
 .../operation/projection/ZonedGridSystem.java      |   8 +-
 .../operation/transform/AbstractMathTransform.java |  12 +-
 .../operation/transform/DomainDefinition.java      |  69 ++++++--
 .../operation/transform/MathTransforms.java        |   8 +-
 .../operation/transform/package-info.java          |   4 +-
 .../org/apache/sis/geometries/adapter/Wrapper.java |  31 +---
 22 files changed, 292 insertions(+), 250 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
index 9981631559..cbe77b8165 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
@@ -198,6 +198,11 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short EmptyTileOrImageRegion = 20;
 
+        /**
+         * The geometry extends into a region where the “{0}” operation may 
not behave correctly.
+         */
+        public static final short GeometryOutOfTransformDomain_1 = 97;
+
         /**
          * Grid coordinates ({3}) are outside the coverage domain. The 
coordinate in the {0} dimension
          * shall be between {1,number} and {2,number} inclusive.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
index c7aa83a018..cee881ca6f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
@@ -45,6 +45,7 @@ DependencyNotFound_3              = Operation \u201c{0}\u201d 
requires a \u201c{
 DuplicatedSampleDimensionIndex_1  = Sample dimension index {0} is duplicated.
 EmptyImage                        = Image has zero pixel.
 EmptyTileOrImageRegion            = Empty tile or image region.
+GeometryOutOfTransformDomain_1    = The geometry extends into a region where 
the \u201c{0}\u201d operation may not behave correctly.
 GridCoordinateOutsideCoverage_4   = Grid coordinates ({3}) are outside the 
coverage domain. The coordinate in the {0} dimension shall be between 
{1,number} and {2,number} inclusive.
 GridEnvelopeMustBeNDimensional_1  = The grid envelope must have at least {0} 
dimensions.
 GridExtentsAreDisjoint_5          = The specified grid extent is outside the 
domain. Indices [{3,number} \u2026 {4,number}] specified in dimension {0} do 
not intersect the [{1,number} \u2026 {2,number}] grid extent.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
index d3d959c150..838e0a9270 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
@@ -50,6 +50,7 @@ DependencyNotFound_3              = L\u2019op\u00e9ration 
\u00ab\u202f{0}\u202f\
 DuplicatedSampleDimensionIndex_1  = L\u2019index de dimension 
d\u2019\u00e9chantillonnage {0} est r\u00e9p\u00e9t\u00e9.
 EmptyImage                        = L\u2019image a z\u00e9ro pixel.
 EmptyTileOrImageRegion            = La tuile ou la r\u00e9gion de l\u2019image 
est vide.
+GeometryOutOfTransformDomain_1    = La g\u00e9om\u00e9trie s\u2019\u00e9tend 
jusque dans une r\u00e9gion o\u00f9 l\u2019op\u00e9ration 
\u00ab\u202f{0}\u202f\u00bb risque de ne pas se comporter correctement.
 GridCoordinateOutsideCoverage_4   = Les coordonn\u00e9es de grille ({3}) sont 
en dehors du domaine de la grille. La coordonn\u00e9e de la dimension {0} doit 
\u00eatre entre {1,number} et {2,number} inclusivement.
 GridEnvelopeMustBeNDimensional_1  = L\u2019enveloppe de la grille doit avoir 
au moins {0} dimensions.
 GridExtentsAreDisjoint_5          = L\u2019\u00e9tendue de grille 
sp\u00e9cifi\u00e9e est en dehors du domaine. Les indices [{3,number} \u2026 
{4,number}] qui ont \u00e9t\u00e9 sp\u00e9cifi\u00e9es dans la dimension {0} 
n\u2019interceptent pas l\u2019\u00e9tendue [{1,number} \u2026 {2,number}] de 
la grille.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
index 6411aaf762..1dd43d7fa3 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
@@ -18,6 +18,8 @@ package org.apache.sis.filter;
 
 import java.util.List;
 import javax.measure.Unit;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
@@ -216,13 +218,14 @@ abstract class BinaryGeometryFilter<R> extends Node 
implements SpatialOperator<R
              */
             final GeometryWrapper geometry = wrapper.apply(null);
             if (geometry != null) try {
-                final GeometryWrapper transformed = 
geometry.transform(optimization.findExpectedCRS(other).orElse(null));
+                final CoordinateReferenceSystem crs = 
optimization.findExpectedCRS(other).orElse(null);
+                final GeometryWrapper transformed = geometry.transform(crs, 
true);
                 if (geometry != transformed) {
                     literal = Optimization.literal(transformed);
                     if (literal == effective1) effective1 = literal;
                     else effective2 = literal;
                 }
-            } catch (PropertyNotFoundException | TransformException e) {
+            } catch (PropertyNotFoundException | FactoryException | 
TransformException e) {
                 optimization.warning(e, false);
             }
             /*
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Transform.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Transform.java
index 2f01362a58..b399f6e4cf 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Transform.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Transform.java
@@ -130,7 +130,7 @@ final class ST_Transform<R> extends FunctionWithSRID<R> {
         final GeometryWrapper value = geometry.apply(input);
         if (value != null) try {
             // Note: `transform(…)` does nothing if the CRS is null.
-            return 
getGeometryLibrary().getGeometry(value.transform(getTargetCRS(input)));
+            return 
getGeometryLibrary().getGeometry(value.transform(getTargetCRS(input), false));
         } catch (BackingStoreException e) {
             final Throwable cause = e.getCause();
             warning((cause instanceof Exception) ? (Exception) cause : e);
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
index a42060b489..fee6aabe35 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
@@ -17,6 +17,8 @@
 package org.apache.sis.filter.sqlmm;
 
 import java.util.List;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.filter.Optimization;
 import org.apache.sis.geometry.wrapper.Geometries;
@@ -77,14 +79,15 @@ class TwoGeometries<R> extends SpatialFunction<R> {
         if (unwrap(geometry2) instanceof Literal<?,?>) {
             final GeometryWrapper literal = geometry2.apply(null);
             if (literal != null) try {
-                final GeometryWrapper transformed = 
literal.transform(optimization.findExpectedCRS(unwrap(geometry1)).orElse(null));
+                final CoordinateReferenceSystem crs = 
optimization.findExpectedCRS(unwrap(geometry1)).orElse(null);
+                final GeometryWrapper transformed = literal.transform(crs, 
true);
                 if (transformed != literal) {
                     @SuppressWarnings({"unchecked","rawtypes"})
                     final Expression<R,?>[] effective = 
getParameters().toArray(Expression[]::new);
                     effective[1] = Optimization.literal(transformed);
                     return recreate(effective);
                 }
-            } catch (PropertyNotFoundException | TransformException e) {
+            } catch (PropertyNotFoundException | FactoryException | 
TransformException e) {
                 optimization.warning(e, false);
             }
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
index fa9ab844a7..dfa4641402 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
@@ -30,12 +30,15 @@ import 
org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.util.FactoryException;
+import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.filter.sqlmm.SQLMM;
 import org.apache.sis.referencing.CRS;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.DomainDefinition;
+import org.apache.sis.referencing.operation.transform.AbstractMathTransform;
 import 
org.apache.sis.referencing.operation.transform.TransformFactoryException;
-import org.apache.sis.referencing.internal.shared.ReferencingUtilities;
+import 
org.apache.sis.referencing.operation.transform.GeometryTransformException;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Classes;
@@ -70,22 +73,10 @@ import org.opengis.coordinate.MismatchedDimensionException;
  * @see Geometries#wrap(Object)
  */
 public abstract class GeometryWrapper extends AbstractGeometry implements 
Geometry {
-    /**
-     * Whether to compare the <abbr>CRS</abbr> of the geometry with the source 
<abbr>CRS</abbr>
-     * of the coordinate operation. For performance reasons, this flag should 
be {@code false}
-     * when the caller already knows that these <abbr>CRS</abbr>s are 
equivalent.
-     *
-     * <p>This is fixed to {@code false} for now because all callers made the 
above-cited check
-     * in the current version of Apache <abbr>SIS</abbr>. But this flag may 
become an option in
-     * the future if transforms become used in different contexts.</p>
-     */
-    protected static final boolean VERIFY_GEOMETRY_CRS_BEFORE_OPERATION = 
false;
-
     /**
      * The coordinate reference system, or {@code null} if unspecified.
      * The value of this field should be set by subclass constructors.
      *
-     * <h4>Where this value come from</h4>
      * This information is sometime redundant with information stored in the 
geometry itself.
      * However, even when the geometry library supports some kind of 
<abbr>SRID</abbr> field,
      * the value in geometry instances is sometime 0 or null. In such case, 
this {@code crs}
@@ -273,9 +264,10 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
             throws FactoryException, TransformException, 
IncommensurableException
     {
         final var geometries = new GeometryWrapper[] {this, other};
-        if (context.transform(geometries)) {
+        final var commonUnit = new Unit<?>[1];
+        if (context.transform(geometries, commonUnit)) {
             double dv = distance.getValue().doubleValue();
-            final Unit<?> unit = 
ReferencingUtilities.getUnit(context.commonCRS);
+            final Unit<?> unit = commonUnit[0];
             if (unit != null) {
                 dv = distance.getUnit().getConverterToAny(unit).convert(dv);
             }
@@ -310,7 +302,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
             throws FactoryException, TransformException, 
IncommensurableException
     {
         final var geometries = new GeometryWrapper[] {this, other};
-        if (context.transform(geometries)) {
+        if (context.transform(geometries, null)) {
             return geometries[0].predicateSameCRS(type, geometries[1]);
         }
         /*
@@ -503,47 +495,20 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
         throw new 
UnconvertibleObjectException(Errors.format(Errors.Keys.CanNotConvertFromType_2, 
geometry.getClass(), type));
     }
 
-    /**
-     * Transforms this geometry using the given coordinate operation.
-     * The caller is responsible to ensure that the source <abbr>CRS</abbr> of 
the given {@code operation}
-     * is equivalent to {@link #getCoordinateReferenceSystem()}. This is not 
verified by this method.
-     *
-     * <p>This method is preferred to the {@link 
#transform(CoordinateReferenceSystem)} method
-     * because not all geometry libraries store the <abbr>CRS</abbr> of their 
objects.</p>
-     *
-     * @param  operation  the coordinate operation to apply.
-     * @return the transformed geometry (may be the same geometry instance, 
but never {@code null}).
-     * @throws UnsupportedOperationException if this operation is not 
supported for current implementation.
-     * @throws FactoryException if a problem happened while building a 
<abbr>CRS</abbr> from a <abbr>SRID</abbr>.
-     * @throws TransformException if the geometry cannot be transformed.
-     * @throws BackingStoreException if the operation failed because of 
another checked exception.
-     */
-    protected GeometryWrapper transform(final CoordinateOperation operation)
-            throws FactoryException, TransformException
-    {
-        MathTransform transform = operation.getMathTransform();
-        if (VERIFY_GEOMETRY_CRS_BEFORE_OPERATION && crs != null) {
-            CoordinateOperation step = CRS.findOperation(crs, 
operation.getSourceCRS(), null);
-            transform = MathTransforms.concatenate(step.getMathTransform(), 
transform);
-        }
-        final GeometryWrapper wrapper = transform(transform);
-        wrapper.setCoordinateReferenceSystem(operation.getTargetCRS());
-        return wrapper;
-    }
-
     /**
      * Transforms this geometry using the given transform.
-     * If the transform is identity, then the geometry is returned unchanged.
-     * Otherwise, a new geometry is returned without <abbr>CRS</abbr>.
+     * If the transform is identity, then the geometry is returned unchanged 
(with the same <abbr>CRS</abbr>).
+     * Otherwise, a new geometry is returned with the given target 
<abbr>CRS</abbr>.
      *
      * @param  transform  the math transform to apply.
-     * @return the transformed geometry (may be the same geometry instance, 
but never {@code null}).
+     * @param  targetCRS  <abbr>CRS</abbr> to set on the returned geometry, or 
{@code null} if none.
+     * @return the transformed geometry (may be the same geometry wrapper 
instance, but never {@code null}).
      * @throws UnsupportedOperationException if this operation is not 
supported for current implementation.
      * @throws TransformException if the geometry cannot be transformed.
-     * @throws FactoryException should never happen (declared because some 
constructors declare this exception).
+     * @throws FactoryException if a problem happened while mapping a 
<abbr>CRS</abbr> from/to a <abbr>SRID</abbr>.
      * @throws BackingStoreException if the operation failed because of 
another checked exception.
      */
-    protected GeometryWrapper transform(final MathTransform transform)
+    protected GeometryWrapper transform(final MathTransform transform, final 
CoordinateReferenceSystem targetCRS)
             throws FactoryException, TransformException
     {
         if (transform.isIdentity()) {
@@ -556,6 +521,8 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * Transforms this geometry to the specified Coordinate Reference System 
(<abbr>CRS</abbr>).
      * If the given <abbr>CRS</abbr> is {@code null} or the same as the 
current <abbr>CRS</abbr>,
      * or if the geometry has no <abbr>CRS</abbr>, then this wrapper is 
returned unchanged.
+     * In the latter case, the geometry <abbr>CRS</abbr> is unchanged (not 
replaced by {@code targetCRS})
+     * in order to avoid modifying an existing instance potentially used 
elsewhere.
      *
      * <p>Consider using {@link 
SpatialOperationContext#transform(GeometryWrapper)} instead of this method,
      * for performance reasons, accuracy reasons (domain of validity) and 
because not all geometry libraries
@@ -572,7 +539,40 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      */
     @Override
     public final GeometryWrapper transform(final CoordinateReferenceSystem 
targetCRS) throws TransformException {
+        try {
+            return transform(targetCRS, false);
+        } catch (FactoryException e) {
+            throw new TransformFactoryException(e);
+        }
+    }
+
+    /**
+     * Transforms this geometry to the specified <abbr>CRS</abbr> with an 
optional check of the domain of validity.
+     * If the given <abbr>CRS</abbr> is {@code null} or the same as the 
current <abbr>CRS</abbr>,
+     * or if the geometry has no <abbr>CRS</abbr>, then this wrapper is 
returned unchanged.
+     * In the latter case, the geometry <abbr>CRS</abbr> is unchanged (not 
replaced by {@code targetCRS})
+     * in order to avoid modifying an existing instance potentially used 
elsewhere.
+     *
+     * <p>Consider using {@link 
SpatialOperationContext#transform(GeometryWrapper)} instead of this method,
+     * for performance reasons, accuracy reasons (domain of validity) and 
because not all geometry libraries
+     * store information about the <abbr>CRS</abbr> in their geometric 
objects.</p>
+     *
+     * @param  targetCRS     the target coordinate reference system, or {@code 
null}.
+     * @param  verifyDomain  whether to verify the transform domain of 
validity.
+     * @return the transformed geometry (may be the same geometry but never 
{@code null}).
+     * @throws UnsupportedOperationException if this operation is not 
supported for current implementation.
+     * @throws FactoryException if an error occured while creating the 
coordinate operation.
+     * @throws TransformException if the given geometry has no 
<abbr>CRS</abbr> or cannot be transformed.
+     * @throws BackingStoreException if the operation failed because of 
another checked exception.
+     *
+     * @see #getCoordinateReferenceSystem()
+     * @see SpatialOperationContext#transform(GeometryWrapper)
+     */
+    public final GeometryWrapper transform(final CoordinateReferenceSystem 
targetCRS, final boolean verifyDomain)
+            throws FactoryException, TransformException
+    {
         if (targetCRS == null || targetCRS == crs || crs == null) {
+            // Do not invoke `setCoordinateReferenceSystem(targetCRS)` — see 
above Javadoc.
             return this;
         }
         DefaultGeographicBoundingBox bbox = null;
@@ -586,11 +586,18 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
                 Logging.ignorableException(Geometries.LOGGER, 
GeometryWrapper.class, "transform", ex);
             }
         }
-        try {
-            return transform(CRS.findOperation(crs, targetCRS, bbox));
-        } catch (FactoryException e) {
-            throw new TransformFactoryException(e);
+        final CoordinateOperation operation = CRS.findOperation(crs, 
targetCRS, bbox);
+        final MathTransform transform = operation.getMathTransform();
+        if (verifyDomain && transform instanceof AbstractMathTransform) {
+            final var atr = (AbstractMathTransform) transform;
+            final var builder = new DomainDefinition();
+            builder.canApproachAsymptotes(true);
+            final AbstractEnvelope domain = 
AbstractEnvelope.castOrCopy(atr.getDomain(builder).orElse(null));
+            if (domain != null && !domain.contains(getEnvelope())) {
+                throw new 
GeometryTransformException(Resources.format(Resources.Keys.GeometryOutOfTransformDomain_1,
 operation.getName().getCode()));
+            }
         }
+        return transform(transform, operation.getTargetCRS());
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/SpatialOperationContext.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/SpatialOperationContext.java
index 18d18c930d..aa4e108deb 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/SpatialOperationContext.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/SpatialOperationContext.java
@@ -32,6 +32,8 @@ import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.OperationMethod;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.ImmutableIdentifier;
 import org.apache.sis.referencing.CRS;
@@ -86,8 +88,8 @@ public final class SpatialOperationContext implements 
Serializable {
 
     /**
      * If the CRS needs to be in some units of measurement, the {@link 
Unit#getSystemUnit()} value.
-     * For example, is units need to be linear, then {@code systemUnit} shall 
be {@link Units#METRE}.
-     * Note that it does not mean that the units of measurement must be 
meters; only that they must
+     * For example, if units need to be linear, then {@code systemUnit} shall 
be {@link Units#METRE}.
+     * Note that it does not mean that the units of measurement must be 
meters, only that they must
      * be compatible with meters.
      */
     @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
@@ -99,12 +101,6 @@ public final class SpatialOperationContext implements 
Serializable {
      */
     private final int skipIndex;
 
-    /**
-     * The common CRS found by {@link #transform(GeometryWrapper[])}. May be 
null.
-     */
-    @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
-    CoordinateReferenceSystem commonCRS;
-
     /**
      * Creates a new context.
      *
@@ -151,8 +147,7 @@ public final class SpatialOperationContext implements 
Serializable {
 
     /**
      * Transforms the specified geometry to the computation <abbr>CRS</abbr>.
-     * Geometries with unspecified CRS will be used as-is, without 
transformation.
-     * The common CRS is stored in the {@link #commonCRS} field.
+     * Geometries with unspecified <abbr>CRS</abbr> will be used as-is, 
without transformation.
      *
      * @param  geometry  the geometry to transform.
      * @return the transformed geometry, or the same instance if no 
transformation was applied.
@@ -163,7 +158,9 @@ public final class SpatialOperationContext implements 
Serializable {
         if (computationCRS != null) {
             final CoordinateReferenceSystem sourceCRS = 
to2D(geometry.getCoordinateReferenceSystem());
             if (sourceCRS != null && !CRS.equivalent(computationCRS, 
sourceCRS)) {
-                geometry = geometry.transform(CRS.findOperation(sourceCRS, 
computationCRS, areaOfInterest));
+                final CoordinateOperation operation = 
CRS.findOperation(sourceCRS, computationCRS, areaOfInterest);
+                final MathTransform transform = operation.getMathTransform();
+                geometry = geometry.transform(transform, 
operation.getTargetCRS());
             }
         }
         return geometry;
@@ -172,107 +169,105 @@ public final class SpatialOperationContext implements 
Serializable {
     /**
      * Transforms the specified geometries to the common <abbr>CRS</abbr>.
      * If {@link #computationCRS} is {@code null}, then this method tries to 
infer one automatically.
-     * Geometries with unspecified CRS will be used as-is, without 
transformation.
-     * The common CRS is stored in the {@link #commonCRS} field.
+     * Geometries with unspecified <abbr>CRS</abbr> will be used as-is, 
without coordinate operation.
+     * The unit of measurement is stored in the {@code commonUnit} array if 
non-null.
      *
      * @param  geometries  the geometries to transform. Results will be stored 
in-place.
+     * @param  commonUnit  where to store the unit of measurement of the 
common CRS, or {@code null}.
      * @return whether this method has been able to find a common CRS.
      * @throws FactoryException if an error occurred while fetching a CRS.
      * @throws TransformException if a coordinate conversion was required but 
failed.
      * @throws IncommensurableException if a geographic CRS does not use 
angular units (should not happen).
      */
-    final boolean transform(final GeometryWrapper[] geometries)
+    final boolean transform(final GeometryWrapper[] geometries, final 
Unit<?>[] commonUnit)
             throws FactoryException, TransformException, 
IncommensurableException
     {
-        final CoordinateReferenceSystem[] allCRS = new 
CoordinateReferenceSystem[geometries.length];
-        try {
-            for (int i = allCRS.length; --i >= 0;) {
-                allCRS[i] = (i == skipIndex) ? computationCRS : 
to2D(geometries[i].getCoordinateReferenceSystem());
-            }
-        } catch (BackingStoreException e) {
-            throw e.unwrapOrRethrow(FactoryException.class);
-        }
-        /*
-         * Search for an arbitrary non-null CRS. Then check in the next loop 
if all other CRS are equal,
-         * ignoring metadata and ignoring null CRS. If this is the case, then 
we do not transform anything.
-         */
-        {
-            int i = allCRS.length;
-            do if (--i < 0) return true;
-            while ((commonCRS = allCRS[i]) == null);
-            /*
-             * Found a CRS potentially common to all geometries (this will be 
checked in next loop).
-             * But if units of measurement are restricted to some kind, we 
will accept that common CRS
-             * only if its unit is of the requested type.
-             */
-            boolean reject = false;
-            if (systemUnit != null && commonCRS != computationCRS) {
-                reject = isCompatibleUnit(commonCRS, systemUnit);
-            }
-            if (!reject) {
-                // Potential common CRS accepted, verify if all other 
geometries use the same CRS.
-                CoordinateReferenceSystem crs;
-                do {
-                    if (--i < 0) return true;       // All geometries are in 
common CRS.
-                    crs = allCRS[i];
-                } while (crs == null || CRS.equivalent(commonCRS, crs));
+        CoordinateReferenceSystem commonCRS = null;
+apply:  {
+            final var allCRS = new 
CoordinateReferenceSystem[geometries.length];
+            try {
+                for (int i = allCRS.length; --i >= 0;) {
+                    allCRS[i] = (i == skipIndex) ? computationCRS : 
to2D(geometries[i].getCoordinateReferenceSystem());
+                }
+                int i = allCRS.length;
+                do if (--i < 0) break apply;
+                while ((commonCRS = allCRS[i]) == null);
+                /*
+                 * Found an arbitrary non-null CRS. If that CRS is compatible 
with `systemUnit`,
+                 * then verify if all other geometries use the same CRS. If 
yes, we are done (exit).
+                 * Otherwise, continue with the rest of this method for 
searching another common CRS.
+                 */
+                if (systemUnit == null || commonCRS == computationCRS || 
isCompatibleUnit(commonCRS, systemUnit)) {
+                    CoordinateReferenceSystem crs;
+                    do {
+                        if (--i < 0) break apply;   // All geometries are in 
common CRS.
+                        crs = allCRS[i];
+                    } while (crs == null || CRS.equivalent(commonCRS, crs));
+                }
+            } catch (BackingStoreException e) {
+                throw e.unwrapOrRethrow(FactoryException.class);
             }
-        }
-        /*
-         * At least one geometry needs to be transformed. Get a CRS which can 
be a common target
-         * for the specified geometries. The `if` block should be executed 
only if both operands
-         * are dynamic (non-literal) values. The rules applied inside the 
block are arbitrary
-         * and may change in any future version.
-         */
-        commonCRS = computationCRS;
-select: if (commonCRS == null) {
             /*
-             * If there is a restriction on the unit of measurement, check if 
an existing CRS
-             * met that criterion. We do this check before invoking 
`suggestCommonTarget(…)`
-             * because that method may replace `ProjectedCRS` by 
`GeographicCRS` in order to
-             * cover a larger area, and we usually want the `ProjectedCRS`.
+             * At least one geometry needs to be transformed. Get a CRS which 
can be a common target
+             * for the specified geometries. The `if` block should be executed 
only if both operands
+             * are dynamic (non-literal) values. The rules applied inside the 
block are arbitrary
+             * and may change in any future version.
              */
-            if (systemUnit != null) {
-                for (int i=allCRS.length; --i >= 0;) {
-                    commonCRS = allCRS[i];
-                    final Unit<?> unit = 
ReferencingUtilities.getUnit(commonCRS);
-                    if (unit != null && 
systemUnit.equals(unit.getSystemUnit())) {
-                        break select;       // Use the `commonCRS` we just 
found.
+            commonCRS = computationCRS;
+select:     if (commonCRS == null) {
+                /*
+                 * If there is a restriction on the unit of measurement, check 
if an existing CRS
+                 * met that criterion. We do this check before invoking 
`suggestCommonTarget(…)`
+                 * because that method may replace `ProjectedCRS` by 
`GeographicCRS` in order to
+                 * cover a larger area, and we usually want the `ProjectedCRS`.
+                 */
+                if (systemUnit != null) {
+                    for (int i=allCRS.length; --i >= 0;) {
+                        commonCRS = allCRS[i];
+                        final Unit<?> unit = 
ReferencingUtilities.getUnit(commonCRS);
+                        if (unit != null && 
systemUnit.equals(unit.getSystemUnit())) {
+                            break select;       // Use the `commonCRS` we just 
found.
+                        }
                     }
                 }
+                /*
+                 * If there are no restrictions on units of measurement, or if 
no geometry CRS met that restriction,
+                 * request a CRS which may be different than the CRS of all 
geometries. The search takes in account
+                 * the CRS domains of validity. The CRS found may be derived 
in order to be made compatible with the
+                 * desired units of measurement.
+                 */
+                commonCRS = CRS.suggestCommonTarget(areaOfInterest, allCRS);
+                if (commonCRS == null) {
+                    return false;           // Geometries are in incompatible 
CRS.
+                }
+                if (systemUnit != null) {
+                    for (int i = allCRS.length; --i >= 0;) {
+                        final CoordinateReferenceSystem crs = allCRS[i];
+                        if (crs != null) {
+                            commonCRS = usingSystemUnit(geometries[i], crs, 
commonCRS, systemUnit);
+                            break select;
+                        }
+                    }
+                    return false;           // Unable to use the desired units 
of measurement.
+                }
             }
             /*
-             * If there are no restrictions on units of measurement, or if no 
geometry CRS met that restriction,
-             * request a CRS which may be different than the CRS of all 
geometries. The search takes in account
-             * the CRS domains of validity. The CRS found may be derived in 
order to be made compatible with the
-             * desired units of measurement.
+             * At this point, `commonCRS` is the CRS to use for converting all 
geometries.
+             * Perform the conversions in-place.
              */
-            commonCRS = CRS.suggestCommonTarget(areaOfInterest, allCRS);
-            if (commonCRS == null) {
-                return false;                       // Geometries are in 
incompatible CRS.
-            }
-            if (systemUnit != null) {
-                for (int i = allCRS.length; --i >= 0;) {
-                    final CoordinateReferenceSystem crs = allCRS[i];
-                    if (crs != null) {
-                        commonCRS = usingSystemUnit(geometries[i], crs, 
commonCRS, systemUnit);
-                        break select;
+            for (int i=0; i < allCRS.length; i++) {
+                if (i != skipIndex) {
+                    final CoordinateReferenceSystem sourceCRS = allCRS[i];
+                    if (!CRS.equivalent(commonCRS, sourceCRS)) {
+                        final CoordinateOperation operation = 
CRS.findOperation(sourceCRS, commonCRS, areaOfInterest);
+                        final MathTransform transform = 
operation.getMathTransform();
+                        geometries[i] = geometries[i].transform(transform, 
operation.getTargetCRS());
                     }
                 }
-                return false;               // Unable to use the desired units 
of measurement.
             }
         }
-        /*
-         * At this point, `commonCRS` is the CRS to use for converting all 
geometries.
-         * Perform the conversions in-place.
-         */
-        for (int i=0; i < allCRS.length; i++) {
-            if (i != skipIndex) {
-                final CoordinateReferenceSystem sourceCRS = allCRS[i];
-                if (!CRS.equivalent(commonCRS, sourceCRS)) {
-                    geometries[i] = 
geometries[i].transform(CRS.findOperation(sourceCRS, commonCRS, 
areaOfInterest));
-                }
-            }
+        if (commonUnit != null) {
+            commonUnit[0] = ReferencingUtilities.getUnit(commonCRS);
         }
         return true;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java
index df0b24a0ee..0396188b6b 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java
@@ -205,6 +205,8 @@ public abstract class GeometryTransform {
 
     /**
      * Transforms the given sequence of coordinate tuples, producing a new 
sequence of tuples.
+     * Note that the {@link CoordinateSequence} interface has a {@code 
setOrdinate(…)} method,
+     * so it may be necessary to copy the coordinate values even for an 
identity transform.
      *
      * @param  sequence   sequence of coordinate tuples to transform.
      * @param  minPoints  minimum number of points to preserve.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
index 487ee3ac60..770fa2d0cf 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
@@ -158,6 +158,7 @@ public final class JTS {
     /**
      * Transforms the given geometry using the given math transform.
      * If the geometry or the transform is null or identity, then the geometry 
is returned unchanged.
+     * The <abbr>CRS</abbr> of the geometry is unchanged, caller may need to 
set that <abbr>CRS</abbr>.
      *
      * @param  geometry   the geometry to transform, or {@code null}.
      * @param  transform  the transform to apply, or {@code null}.
@@ -166,8 +167,7 @@ public final class JTS {
      */
     public static Geometry transform(Geometry geometry, final MathTransform 
transform) throws TransformException {
         if (geometry != null && transform != null && !transform.isIdentity()) {
-            final var gct = new GeometryCoordinateTransform(transform, 
geometry.getFactory());
-            geometry = gct.transform(geometry);
+            geometry = new GeometryCoordinateTransform(transform, 
geometry.getFactory()).transform(geometry);
         }
         return geometry;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
index d18d28329b..c3b24feff0 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
@@ -701,17 +701,24 @@ add:    for (Geometry next = geometry;;) {
 
     /**
      * Transforms this geometry using the given transform.
-     * If the transform is {@code null}, then the geometry is returned 
unchanged.
-     * Otherwise, a new geometry is returned without CRS.
+     * If the transform is {@code null} or identity, this wrapper is returned 
unchanged (same <abbr>CRS</abbr>).
+     * Otherwise, a new geometry is returned without <abbr>CRS</abbr>.
      *
      * @param  transform  the math transform to apply, or {@code null}.
-     * @return the transformed geometry (may be the same geometry instance, 
but never {@code null}).
-     * @throws FactoryException should never happen (declared because the 
constructor declares this exception).
+     * @param  targetCRS  <abbr>CRS</abbr> to set on the returned geometry, or 
{@code null} if none.
+     * @return the transformed geometry (may be the same geometry wrapper 
instance, but never {@code null}).
+     * @throws FactoryException if a problem happened while mapping a 
<abbr>CRS</abbr> from/to a <abbr>SRID</abbr>.
      * @throws TransformException if the geometry cannot be transformed.
      */
     @Override
-    protected GeometryWrapper transform(final MathTransform transform) throws 
FactoryException, TransformException {
-        return rewrap(JTS.transform(geometry, transform));
+    protected GeometryWrapper transform(final MathTransform transform, final 
CoordinateReferenceSystem targetCRS)
+            throws FactoryException, TransformException
+    {
+        final Wrapper result = rewrap(JTS.transform(geometry, transform));
+        if (targetCRS != null && result != this) {
+            result.setCoordinateReferenceSystem(targetCRS);
+        }
+        return result;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
index 5b2c3a7256..f75668a0a3 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
@@ -94,7 +94,6 @@ import org.opengis.coordinate.CoordinateSet;
  *   <li>An estimation of the {@linkplain #getCoordinateOperationAccuracy() 
operation accuracy}.</li>
  * </ul>
  *
- * <h2>Instantiation</h2>
  * This class is conceptually <i>abstract</i>, even if it is technically 
possible to instantiate it.
  * Typical applications should create instances of the most specific subclass 
prefixed by {@code Default} instead.
  * An exception to this rule may occur when it is not possible to identify the 
exact operation type.
@@ -103,8 +102,8 @@ import org.opengis.coordinate.CoordinateSet;
  * This base class is immutable and thus thread-safe if the property 
<em>values</em> (not necessarily the map itself)
  * given to the constructor are also immutable. Most SIS subclasses and 
related classes are immutable under similar
  * conditions. This means that unless otherwise noted in the javadoc, {@code 
CoordinateOperation} instances created
- * using only SIS factories and static constants can be shared by many objects 
and passed between threads without
- * synchronization.
+ * using only <abbr>SIS</abbr> factories and static constants can be shared by 
many objects and passed between threads
+ * without synchronization.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @version 1.7
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
index d0a4560af9..ef6e7bfbef 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
@@ -23,7 +23,6 @@ import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.matrix.Matrix2;
 import org.apache.sis.referencing.operation.transform.DomainDefinition;
-import org.apache.sis.geometry.Envelope2D;
 import static org.apache.sis.math.MathFunctions.atanh;
 
 
@@ -59,9 +58,7 @@ final class AuthalicMercator extends AuthalicConversion {
      */
     @Override
     public Optional<Envelope> getDomain(final DomainDefinition criteria) {
-        return Optional.of(new Envelope2D(null,
-                 -LARGE_LONGITUDE_LIMIT,  -POLAR_AREA_LIMIT,
-                2*LARGE_LONGITUDE_LIMIT, 2*POLAR_AREA_LIMIT));
+        return Optional.of(domain(criteria.canApproachAsymptotes()));
     }
 
     /**
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 968c7b1f68..45808405c7 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
@@ -29,7 +29,6 @@ import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.referencing.operation.provider.Mercator1SP;
 import org.apache.sis.referencing.operation.provider.Mercator2SP;
 import org.apache.sis.referencing.operation.provider.MercatorSpherical;
@@ -366,8 +365,8 @@ subst:  if (variant.spherical || eccentricity == 0) {
 
     /**
      * Returns the domain of input coordinates. For a Mercator projection 
other than Miller variant,
-     * the limit is arbitrarily set to 84° of latitude North and South. This 
is consistent with the
-     * "World Mercator" domain of validity defined by EPSG:3395, which is 80°S 
to 84°N.
+     * the limit exclude polar regions by default. This is consistent with the 
<q>World Mercator</q>
+     * domain of validity defined by <abbr>EPSG</abbr>:3395, which is 80°S to 
84°N.
      *
      * <p>The range of longitude values is set to an arbitrary range larger 
than −180° … +180°,
      * because the Mercator projection is mathematically capable to handle 
coordinates beyond that range
@@ -376,8 +375,7 @@ subst:  if (variant.spherical || eccentricity == 0) {
      */
     @Override
     public Optional<Envelope> getDomain(final DomainDefinition criteria) {
-        final double limit = (variant == Variant.MILLER) ? -PI/2 : 
-POLAR_AREA_LIMIT;
-        return Optional.of(new Envelope2D(null, -LARGE_LONGITUDE_LIMIT, limit, 
2*LARGE_LONGITUDE_LIMIT, -2*limit));
+        return Optional.of(domain(variant == Variant.MILLER || 
criteria.canApproachAsymptotes()));
     }
 
     /**
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 22873ea359..225aef63e6 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
@@ -37,6 +37,9 @@ import org.opengis.util.FactoryException;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.internal.shared.Constants;
+import org.apache.sis.util.internal.shared.Numerics;
+import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.metadata.iso.citation.Citations;
@@ -50,8 +53,6 @@ import 
org.apache.sis.referencing.operation.transform.TransformJoiner;
 import org.apache.sis.referencing.operation.provider.MapProjection;
 import org.apache.sis.referencing.internal.shared.Formulas;
 import org.apache.sis.system.Modules;
-import org.apache.sis.util.internal.shared.Constants;
-import org.apache.sis.util.internal.shared.Numerics;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.metadata.Identifier;
@@ -164,22 +165,6 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
      */
     static final int MAXIMUM_ITERATIONS = Formulas.MAXIMUM_ITERATIONS;
 
-    /**
-     * Arbitrary latitude threshold (in radians) for considering that a point 
is in the polar area.
-     * This is used for implementations of the {@link 
#getDomain(DomainDefinition)} method.
-     */
-    static final double POLAR_AREA_LIMIT = PI/2 * (84d/90);
-
-    /**
-     * An arbitrarily large longitude value (in radians) for map projections 
capable to have infinite
-     * extent in the east-west direction. This is the case of {@link Mercator} 
projection for example.
-     * Longitudes have no real world meaning outside −180° … +180° range 
(unless wraparound is applied),
-     * but we nevertheless accept large values without wraparound because they 
make envelope projection easier.
-     * This is used for implementations of the {@link 
#getDomain(DomainDefinition)} method,
-     * which use arbitrary limits anyway.
-     */
-    static final double LARGE_LONGITUDE_LIMIT = 100*PI;
-
     /**
      * The internal parameter descriptors. Keys are implementation classes.  
Values are parameter descriptor groups
      * containing at least a parameter for the {@link #eccentricity} value, 
and optionally other internal parameter
@@ -680,6 +665,25 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
                 
context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION), false);
     }
 
+    /**
+     * Returns an arbitrary world-wide domain of validity (in radians) for 
Mercator-like projections.
+     * These projections can have an infinite extent in the east-west 
direction and generally exclude polar regions.
+     * Longitudes have no real world meaning outside the [−180° … +180°] range 
(unless wraparounds are applied),
+     * but we nevertheless accept out of bounds values because they facilitate 
calculations on projected envelopes.
+     * This is used for {@link #getDomain(DomainDefinition)} implementations, 
which return arbitrary limits anyway.
+     *
+     * <p>The current implementation sets a longitude range of ±200° (i.e. the 
world plus a margin)
+     * and a latitude range from 84°S to 84°N by default.
+     * These limits are arbitrary and may change in any future versions.</p>
+     *
+     * @param  includePoles  whether to extent to range of latitude values to 
±90°.
+     * @return proposed value to return in {@link 
#getDomain(DomainDefinition)} implementations.
+     */
+    static Envelope2D domain(final boolean includePoles) {
+        final double limit = includePoles ? -PI/2 : -PI/2 * (84d/90);
+        return new Envelope2D(null, -100*PI, limit, 2*100*PI, -2*limit);
+    }
+
     /*
      * TODO: consider adding a sqrt1ms(x) method for sqrt(1 - x*x), which 
could be implemented as sqrt(fma(x, -x, 1)).
      * The use of Math.fma(…) in this context would be valuable especially 
when x is close to 1 (to be verified).
@@ -916,7 +920,7 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
         if (!super.equals(object, mode)) {
             return false;
         }
-        final NormalizedProjection that = (NormalizedProjection) object;
+        final var that = (NormalizedProjection) object;
         switch (mode) {
             case STRICT:
             case BY_CONTRACT: {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/TransverseMercator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/TransverseMercator.java
index d92e5f149b..1efd6abd30 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/TransverseMercator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/TransverseMercator.java
@@ -375,16 +375,14 @@ public class TransverseMercator extends 
NormalizedProjection {
      * Returns the domain of input coordinates.
      * The limits defined by this method are arbitrary and may change in any 
future implementation.
      * Current implementation sets a limit at 40° of longitude on each side of 
the central meridian
-     * (this limit is mentioned in EPSG guidance notes)
-     * and a limit at 84° of latitude (same as {@link Mercator} projection).
+     * (this limit is given in <abbr>EPSG</abbr> guidance notes) and a range 
of latitude values which
+     * exclude polar regions by default.
      */
     @Override
     public Optional<Envelope> getDomain(final DomainDefinition criteria) {
-        final Envelope2D domain = new Envelope2D();
+        final Envelope2D domain = domain(criteria.canApproachAsymptotes());
         domain.x = -PI/2 * (40d/90);
-        domain.y = -POLAR_AREA_LIMIT;
-        domain.width  = -2 * domain.x;
-        domain.height = -2 * domain.y;
+        domain.width = -2 * domain.x;
         return Optional.of(domain);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
index b9f2d4a150..181dde5a6d 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
@@ -153,12 +153,14 @@ public class ZonedGridSystem extends 
AbstractMathTransform2D implements Serializ
      * Returns the domain of input coordinates.
      * The limits defined by this method are arbitrary and may change in any 
future implementation.
      * Current implementation sets a longitude range of ±180° (i.e. the world) 
and a latitude range
-     * from 84°S to 84°N.
+     * which excludes polar regions by default.
      */
     @Override
     public Optional<Envelope> getDomain(final DomainDefinition criteria) {
-        final double y = -NormalizedProjection.POLAR_AREA_LIMIT;
-        return Optional.of(new Envelope2D(null, -PI, y, 2*PI, -2*y));
+        final Envelope2D domain = 
NormalizedProjection.domain(criteria.canApproachAsymptotes());
+        domain.x     =  -PI;
+        domain.width = 2*PI;
+        return Optional.of(domain);
     }
 
     /**
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 f96326aa89..c6407475f1 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
@@ -162,26 +162,24 @@ public abstract class AbstractMathTransform extends 
FormattableObject
     public abstract int getTargetDimensions();
 
     /**
-     * Returns the ranges of source coordinate values where this transform is 
mathematically applicable.
+     * Returns the ranges of source coordinate values where this transform is 
considered well-behaved.
      * This is <em>not</em> the domain of validity for which a coordinate 
reference system has been defined,
      * because this method ignores real world considerations such as datum and 
country boundaries.
-     * This method can be used for cropping data in order to exclude areas 
that may cause numerical problems.
+     * Instead this method estimates, for example, where the function is 
monotonic and reasonably far from infinity.
+     *
+     * <p>This method can be used for cropping data in order to exclude areas 
that may cause numerical problems.
      * For example, the results of a Mercator projection tend to infinity when 
the latitude values approach a pole.
      * For avoiding data structures with unreasonably large values or {@link 
Double#NaN} values,
      * an arbitrary maximal latitude value (typically 80° or 84°) can be 
enforced before projection.
      * Those limits are arbitrary, the transform does not become suddenly 
invalid after a limit.
      * But enforcing such limits can (for example) avoid chaotic viewer 
behavior when attempting
-     * to render a world-wide image.
+     * to render a world-wide image.</p>
      *
      * <p>Many transforms, in particular affine transforms, have no 
mathematical limits.
      * Therefore, the default implementation returns an empty value, but it 
does not mean that the associated
      * {@linkplain 
org.apache.sis.referencing.operation.AbstractCoordinateOperation coordinate 
operation}
      * has no geospatial domain of validity.</p>
      *
-     * <p>Callers do not need to search through {@linkplain 
MathTransforms#getSteps(MathTransform) transform steps}.
-     * The Apache <abbr>SIS</abbr> implementation of {@link 
MathTransforms#concatenate(MathTransform, MathTransform)
-     * concatenated transforms} does that automatically.</p>
-     *
      * @param  criteria  controls the definition of transform domain.
      * @return estimation of ranges of source coordinate values where the 
transform is numerically applicable.
      * @throws TransformException if the domain cannot be estimated.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DomainDefinition.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DomainDefinition.java
index 2996b1a548..9d34cf0073 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DomainDefinition.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DomainDefinition.java
@@ -28,7 +28,7 @@ import org.apache.sis.geometry.GeneralEnvelope;
 
 
 /**
- * Builder of the mathematical domain of validity of a {@code MathTransform}.
+ * Builder of a {@code MathTransform} domain of validity where the function is 
considered well-behaved.
  * Contrary to {@linkplain CRS#getDomainOfValidity(CoordinateReferenceSystem) 
CRS domain of validity},
  * this class estimates a domain based on mathematical behavior only, not on 
real world considerations.
  * For example, the Mercator projection tends to infinity when approaching 
poles, so it is recommended
@@ -36,14 +36,9 @@ import org.apache.sis.geometry.GeneralEnvelope;
  * This is different than the domain of validity of CRS, which is often 
limited to a particular country.
  * In general, the CRS domain of validity is much smaller than the domain 
computed by this class.
  *
- * <p>Current implementation does not yet provide ways to describe how a 
domain is decided.
- * A future version may, for example, allows to specify a maximal deformation 
tolerated for map projections.
- * In current implementation, the estimation can be customized by overriding 
the
- * {@link #estimate(MathTransform)} or {@link #intersect(Envelope)} 
methods.</p>
- *
- * <p>Each {@code DomainDefinition} instance should be used only once for an 
{@link AbstractMathTransform}
- * instance, unless that transform is a chain of concatenated transforms (this 
case is handled automatically
- * by Apache SIS). Usage example:</p>
+ * <p>Each {@code DomainDefinition} instance should be used only once for an 
{@link AbstractMathTransform} instance,
+ * unless that transform is a chain of concatenated transforms (handled 
automatically by Apache <abbr>SIS</abbr>).
+ * Usage example:</p>
  *
  * {@snippet lang="java" :
  *     AbstractMathTransform transform = …;
@@ -56,7 +51,7 @@ import org.apache.sis.geometry.GeneralEnvelope;
  * when the default implementation is sufficient.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.8
  *
  * @see MathTransforms#getDomain(MathTransform)
  * @see AbstractMathTransform#getDomain(DomainDefinition)
@@ -65,6 +60,12 @@ import org.apache.sis.geometry.GeneralEnvelope;
  * @since 1.3
  */
 public class DomainDefinition {
+    /**
+     * Whether the domain is allowed to approach input values producing 
infinite output values.
+     * By default, the domain is cropped at values considered reasonably far 
from asymptotes.
+     */
+    private boolean canApproachAsymptotes;
+
     /**
      * Limits computed so far, or {@code null} if none.
      */
@@ -134,13 +135,57 @@ public class DomainDefinition {
     public DomainDefinition() {
     }
 
+    /**
+     * Returns whether the domain is allowed to approach input values 
producing infinite output values.
+     * For example, the Mercator projection has asymptotes at 90°N and 90°S, 
because the projection outputs
+     * tend toward infinity as the latitude approaches a pole.
+     * If this method returns {@code false} (the default), the domain is 
cropped at an arbitrary distance
+     * from the asymptotes (for example, 84° of latitude).
+     *
+     * @return whether to allow the domain to approach asymptotes.
+     *
+     * @since 1.8
+     */
+    public boolean canApproachAsymptotes() {
+        return canApproachAsymptotes;
+    }
+
+    /**
+     * Sets whether the domain is allowed to approach input values producing 
infinite output values.
+     * The default value is {@code false}, which crops the domain at some 
distance from asymptotes.
+     * For example, Mercator projections may limit their domain to 84° of 
latitude North and South.
+     * Setting this flag to {@code true} raises the limit (in the Mercator 
projection case) to 90°.
+     *
+     * <p>A value of {@code true} does not mean that the domain will extend to 
the asymptotes,
+     * only that it is <em>allowed</em> to do so. A transform may choose to 
restrict the domain
+     * anyway, for example because the function is not stable that far.</p>
+     *
+     * <h4>Guidelines for approaching (or not) the asymptotes</h4>
+     * <p>The domain of validity should stay relatively far from asymptotes 
({@code allowed = false})
+     * when the domain is used for cropping map bounds at rendering time. For 
example, a world-wide map
+     * rendered in a Mercator projection needs to exclude polar regions when 
computing the initial zoom,
+     * otherwise most of Earth's surface would be squashed by the near 
infinite values of polar regions.</p>
+     *
+     * <p>It can be useful to let the domain of validity extent to infinity 
({@code allowed = true})
+     * when the domain is used for {@linkplain 
GeneralEnvelope#contains(Envelope) testing inclusions}.
+     * As long as the function is monotonic, operations based on comparisons 
of numerical values stay
+     * generally reliable.</p>
+     *
+     * @param  allowed  whether to allow the domain to approach asymptotes.
+     *
+     * @since 1.8
+     */
+    public void canApproachAsymptotes(final boolean allowed) {
+        canApproachAsymptotes = allowed;
+    }
+
     /**
      * Estimates the domain of the given math transform and intersects it with 
previously computed domains.
      * The result can be obtained by a call to {@link #result()}.
      *
      * <p>The default implementation invokes {@link 
AbstractMathTransform#getDomain(DomainDefinition)} if possible,
-     * or does nothing otherwise. The domain provided by the transform is 
given to {@link #intersect(Envelope)}.
-     * Subclasses can override for modifying this behavior.</p>
+     * or does nothing otherwise. The transform implementation should callback 
the {@link #intersect(Envelope)} method.
+     * {@code DomainDefinition} Subclasses can override for modifying the 
default behavior.</p>
      *
      * @param  evaluated  the transform for which to estimate the domain.
      * @throws TransformException if the domain cannot be estimated.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
index 3880e732a9..81f0645bb5 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@ -786,14 +786,12 @@ public final class MathTransforms {
     }
 
     /**
-     * Returns the ranges of source coordinate values where the given 
transform is mathematically applicable.
+     * Returns the ranges of source coordinate values where the given 
transform is considered well-behaved.
      * This is <em>not</em> the domain of validity for which a coordinate 
reference system has been defined,
      * because this method ignores real world considerations such as datum and 
country boundaries.
+     * Instead this method estimates, for example, where the function is 
monotonic and reasonably far from infinity.
      * This method is for allowing callers to crop their data for removing 
areas that may cause numerical problems,
-     * for example latitudes too close to a pole used as inputs of a Mercator 
projection.
-     *
-     * <p>See {@link AbstractMathTransform#getDomain(DomainDefinition)} for 
more information.
-     * This static method delegates to above-cited method if possible, or 
returns an empty value otherwise.</p>
+     * such as latitudes too close to a pole used as inputs of a Mercator 
projection.
      *
      * @param  evaluated  transform for which to evaluate a domain, or {@code 
null}.
      * @return estimation of ranges of source coordinate values where the 
transform is numerically applicable.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/package-info.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/package-info.java
index ab8dadcf18..19d5cdd63f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/package-info.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/package-info.java
@@ -49,12 +49,12 @@
  * transform factory}.
  *
  *
- * <h2>Non-spatial coordinates</h2>
+ * <h2>Transform between values that are not coordinates</h2>
  * {@code MathTransform} usually performs conversions or transformations from 
points given in a
  * {@linkplain 
org.apache.sis.referencing.operation.AbstractCoordinateOperation#getSourceCRS()
  * source coordinate reference system} to coordinate values for the same 
points in the
  * {@linkplain 
org.apache.sis.referencing.operation.AbstractCoordinateOperation#getTargetCRS()
- * target coordinate reference system}. However, the conversions are not 
necessarily between CRS;
+ * target coordinate reference system}. However, the conversions are not 
necessarily between <abbr>CRS</abbr>s.
  * a {@code MathTransform} can also be used for converting the sample values 
in a raster for example.
  * Such kind of transforms are named {@linkplain 
org.apache.sis.referencing.operation.transform.TransferFunction
  * transfer functions}.
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java
index 617a40325e..8dfe89e7d4 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java
@@ -30,10 +30,8 @@ import org.opengis.filter.DistanceOperatorName;
 import org.opengis.filter.SpatialOperatorName;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
-import org.opengis.util.FactoryException;
 import org.apache.sis.filter.sqlmm.SQLMM;
 import org.apache.sis.geometries.Curve;
 import org.apache.sis.geometries.DE9IM;
@@ -60,8 +58,6 @@ import org.apache.sis.maths.Vectors;
 import org.apache.sis.maths.Vector;
 import org.apache.sis.measure.Quantities;
 import org.apache.sis.measure.Units;
-import org.apache.sis.referencing.CRS;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.UnconvertibleObjectException;
@@ -634,40 +630,23 @@ add:    for (Geometry next = geometry;;) {
         return 
GeometryFactory.DEFAULT.createLinearRing(GeometryFactory.DEFAULT.createDataPoints(source.getDataPoints()));
     }
 
-    /**
-     * Transforms this geometry using the given coordinate operation.
-     * If the operation is {@code null}, then the geometry is returned 
unchanged.
-     *
-     * @param  operation    the coordinate operation to apply.
-     * @throws FactoryException if transformation to the target CRS cannot be 
found.
-     * @throws TransformException if the geometry cannot be transformed.
-     */
-    @Override
-    protected GeometryWrapper transform(final CoordinateOperation operation)
-            throws FactoryException, TransformException
-    {
-        MathTransform mt = operation.getMathTransform();
-        if (VERIFY_GEOMETRY_CRS_BEFORE_OPERATION && crs != null) {
-            final CoordinateOperation step = CRS.findOperation(crs, 
operation.getSourceCRS(), null);
-            mt = MathTransforms.concatenate(step.getMathTransform(), mt);
-        }
-        return new Wrapper(new GeometryProcessor().transform(geometry, 
operation.getTargetCRS(), mt));
-    }
-
     /**
      * Transforms this geometry using the given transform.
      * If the transform is {@code null}, then the geometry is returned 
unchanged.
      *
      * @param  transform  the math transform to apply.
+     * @param  targetCRS  <abbr>CRS</abbr> to set on the returned geometry, or 
{@code null} if none.
      * @return the transformed geometry (may be the same geometry instance, 
but never {@code null}).
      * @throws TransformException if the geometry cannot be transformed.
      */
     @Override
-    protected GeometryWrapper transform(final MathTransform transform) throws 
TransformException {
+    protected GeometryWrapper transform(final MathTransform transform, final 
CoordinateReferenceSystem targetCRS)
+            throws TransformException
+    {
         if (transform.isIdentity()) {
             return this;
         }
-        return new Wrapper(this, new GeometryProcessor().transform(geometry, 
null, transform));
+        return new Wrapper(this, new GeometryProcessor().transform(geometry, 
targetCRS, transform));
     }
 
     /**

Reply via email to