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 7af56f6285 Replace `ProjectionLimits` by a more reliable mechanism 
defined directly in `MathTransform` implementations. For now only `Mercator` 
and `TransverseMercator` defines those limit. More will be added in the future.
7af56f6285 is described below

commit 7af56f62851350055f9aa5956284f526eb423490
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Jul 1 16:43:19 2022 +0200

    Replace `ProjectionLimits` by a more reliable mechanism defined directly in 
`MathTransform` implementations.
    For now only `Mercator` and `TransverseMercator` defines those limit. More 
will be added in the future.
    
    https://issues.apache.org/jira/browse/SIS-550
---
 .../internal/map/coverage/ProjectionLimits.java    | 108 ---------
 .../sis/internal/map/coverage/RenderingData.java   |  10 +-
 .../referencing/operation/projection/Mercator.java |  20 ++
 .../operation/projection/TransverseMercator.java   |  23 +-
 .../operation/transform/AbstractMathTransform.java |  74 ++++++-
 .../operation/transform/ConcatenatedTransform.java |  22 +-
 .../operation/transform/DomainDefinition.java      | 244 +++++++++++++++++++++
 .../operation/transform/MathTransforms.java        |  29 ++-
 .../operation/transform/DomainDefinitionTest.java  |  52 +++++
 .../operation/transform/PseudoTransform.java       |  67 ++++--
 .../sis/test/suite/ReferencingTestSuite.java       |   3 +-
 11 files changed, 514 insertions(+), 138 deletions(-)

diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/ProjectionLimits.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/ProjectionLimits.java
deleted file mode 100644
index c955e3ef3a..0000000000
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/ProjectionLimits.java
+++ /dev/null
@@ -1,108 +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.internal.map.coverage;
-
-import org.opengis.geometry.Envelope;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.CoordinateOperation;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.geometry.GeneralEnvelope;
-import org.apache.sis.referencing.CRS;
-import org.apache.sis.referencing.operation.projection.Mercator;
-import org.apache.sis.referencing.operation.projection.NormalizedProjection;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
-
-
-/**
- * Map projection for which to apply a limit for avoiding rendering problems.
- * The most common case is the Mercator projection, for which we need to put
- * a limit for avoiding to reach the poles.
- *
- * <p>This is a first draft to be expanded progressively.</p>
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
- * @since   1.3
- * @module
- */
-final class ProjectionLimits {
-    /**
-     * List of rules for which we defines limits.
-     * This list may be expanded in future versions.
-     */
-    private static final ProjectionLimits[] RULES = {
-        new ProjectionLimits(Mercator.class)
-    };
-
-    /**
-     * The type of map projection for which this rule applies.
-     */
-    private final Class<? extends NormalizedProjection> target;
-
-    /**
-     * Creates a new rule for map projection limits.
-     *
-     * @param  target  the type of map projection for which this rule applies.
-     */
-    private ProjectionLimits(final Class<? extends NormalizedProjection> 
target) {
-        this.target = target;
-    }
-
-    /**
-     * Returns the map projection limits for rendering a map in the given 
objective CRS.
-     * The default implementation returns the CRS domain of validity, which is 
okay for
-     * the "World Mercator" projection but is often too conservative for other 
projections.
-     * For example in the case of UTM projection, we needs to allow both 
hemisphere and a larger zone.
-     *
-     * @param  objectiveCRS  the CRS used for rendering the map.
-     * @return limits where to crop the projected image in objective CRS, or 
{@code null} if none.
-     */
-    Envelope limits(final CoordinateReferenceSystem objectiveCRS) {
-        return CRS.getDomainOfValidity(objectiveCRS);
-    }
-
-    /**
-     * Returns the map projection limits for rendering a map after the 
specified "data to objective" transform.
-     *
-     * @param  changeOfCRS  the operation applied on data before rendering in 
objective CRS.
-     * @return limits where to crop the projected image in objective CRS, or 
{@code null} if none.
-     */
-    static Envelope find(final CoordinateOperation changeOfCRS) {
-        Envelope limits = null;
-        if (changeOfCRS != null) {
-            GeneralEnvelope intersection = null;
-            for (final MathTransform step : 
MathTransforms.getSteps(changeOfCRS.getMathTransform())) {
-                for (final ProjectionLimits rule : RULES) {
-                    if (rule.target.isInstance(step)) {
-                        final Envelope e = 
rule.limits(changeOfCRS.getTargetCRS());
-                        if (e != null) {
-                            if (limits == null) {
-                                limits = e;
-                            } else {
-                                if (intersection == null) {
-                                    limits = intersection = new 
GeneralEnvelope(limits);
-                                }
-                                intersection.intersect(e);
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        return limits;
-    }
-}
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
index 6135be896b..bb24a390cb 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
@@ -197,8 +197,8 @@ public class RenderingData implements Cloneable {
      * A value for {@link #domainOfValidity} meaning that there is no limits. 
Should not be modified.
      */
     private static final Rectangle NO_LIMITS = new Rectangle(
-            Integer.MIN_VALUE, Integer.MIN_VALUE,
-            Integer.MAX_VALUE, Integer.MAX_VALUE);
+            Integer.MIN_VALUE/2, Integer.MIN_VALUE/2,
+            Integer.MAX_VALUE,   Integer.MAX_VALUE);
 
     /**
      * Ranges of sample values in each band of {@link #data}. This is used for 
determining on which sample values
@@ -579,7 +579,7 @@ public class RenderingData implements Cloneable {
             return bounds;
         }
         if (domainOfValidity == null) {
-            Envelope domain = ProjectionLimits.find(changeOfCRS);
+            Envelope domain = 
MathTransforms.getDomain(changeOfCRS.getMathTransform().inverse()).orElse(null);
             if (domain == null) {
                 domainOfValidity = NO_LIMITS;
                 return bounds;
@@ -589,8 +589,8 @@ public class RenderingData implements Cloneable {
             double y = domain.getMinimum(1);
             double w = domain.getSpan(0);
             double h = domain.getSpan(1);
-            if (!(x >= Integer.MIN_VALUE)) x = Integer.MIN_VALUE;       // Use 
`!` for catching NaN.
-            if (!(y >= Integer.MIN_VALUE)) y = Integer.MIN_VALUE;
+            if (!(x >= Integer.MIN_VALUE)) x = Integer.MIN_VALUE/2;     // Use 
`!` for catching NaN.
+            if (!(y >= Integer.MIN_VALUE)) y = Integer.MIN_VALUE/2;
             if (!(h <= Integer.MAX_VALUE)) h = Integer.MAX_VALUE;
             if (!(w <= Integer.MAX_VALUE)) w = Integer.MAX_VALUE;
             domainOfValidity = new Rectangle(
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
index becd37175c..11d5b4b0fd 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
@@ -17,7 +17,9 @@
 package org.apache.sis.referencing.operation.projection;
 
 import java.util.EnumMap;
+import java.util.Optional;
 import java.util.regex.Pattern;
+import org.opengis.geometry.Envelope;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.referencing.operation.Matrix;
@@ -25,6 +27,7 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.internal.referencing.provider.Mercator1SP;
 import org.apache.sis.internal.referencing.provider.Mercator2SP;
 import org.apache.sis.internal.referencing.provider.MercatorSpherical;
@@ -36,6 +39,7 @@ import org.apache.sis.internal.util.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.transform.DomainDefinition;
 import org.apache.sis.referencing.operation.transform.ContextualParameters;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.util.resources.Errors;
@@ -390,6 +394,22 @@ subst:  if ((variant.spherical || eccentricity == 0) && 
getClass() == Mercator.c
         return context.completeTransform(factory, kernel);
     }
 
+    /**
+     * 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.
+     *
+     * @since 1.3
+     */
+    @Override
+    public final Optional<Envelope> getDomain(final DomainDefinition criteria) 
{
+        final double limit = (variant == Variant.MILLER) ? PI/2 : PI/2 * 
(84d/90);
+        final GeneralEnvelope domain = new GeneralEnvelope(2);
+        domain.setRange(0, NEGATIVE_INFINITY, POSITIVE_INFINITY);
+        domain.setRange(1, -limit, +limit);
+        return Optional.of(domain);
+    }
+
     /**
      * Converts the specified coordinate (implementation-specific units) and 
stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if 
{@code derivate} is {@code true}.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
index 9b95cacf96..cb227afa1f 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
@@ -17,15 +17,19 @@
 package org.apache.sis.referencing.operation.projection;
 
 import java.util.EnumMap;
+import java.util.Optional;
 import java.util.regex.Pattern;
+import org.opengis.geometry.Envelope;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.OperationMethod;
+import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.referencing.operation.matrix.Matrix2;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.transform.DomainDefinition;
 import org.apache.sis.referencing.operation.transform.ContextualParameters;
 import org.apache.sis.internal.referencing.provider.TransverseMercatorSouth;
 import org.apache.sis.internal.referencing.Resources;
@@ -66,7 +70,7 @@ import static 
org.apache.sis.internal.referencing.provider.TransverseMercator.*;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rémi Maréchal (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see Mercator
  * @see ObliqueMercator
@@ -369,6 +373,23 @@ public class TransverseMercator extends 
NormalizedProjection {
         return context.completeTransform(factory, kernel);
     }
 
+    /**
+     * 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).
+     *
+     * @since 1.3
+     */
+    @Override
+    public final Optional<Envelope> getDomain(final DomainDefinition criteria) 
{
+        final GeneralEnvelope domain = new GeneralEnvelope(2);
+        domain.setRange(0, -PI/2 * (40d/90), +PI/2 * (40d/90));
+        domain.setRange(1, -PI/2 * (84d/90), +PI/2 * (84d/90));
+        return Optional.of(domain);
+    }
+
     /**
      * Implementation of {@link #transform(double[], int, double[], int, 
boolean)} for points outside domain of validity.
      * Should be invoked only when the longitude is at more than 90° from 
central meridian, in which case result does not
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
index 424f495015..efe0307a20 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
@@ -18,7 +18,9 @@ package org.apache.sis.referencing.operation.transform;
 
 import java.util.List;
 import java.util.Arrays;
+import java.util.Optional;
 import org.opengis.util.FactoryException;
+import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.parameter.ParameterDescriptorGroup;
@@ -29,6 +31,7 @@ import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.referencing.operation.OperationMethod;
+import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.parameter.Parameterized;
 import org.apache.sis.referencing.operation.matrix.Matrices;
@@ -37,6 +40,7 @@ import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.internal.referencing.WKTUtilities;
 import org.apache.sis.internal.referencing.WKTKeywords;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.LenientComparable;
@@ -78,7 +82,7 @@ import static 
org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
  * running the same SIS version.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see DefaultMathTransformFactory
  * @see org.apache.sis.referencing.operation.AbstractCoordinateOperation
@@ -131,9 +135,9 @@ public abstract class AbstractMathTransform extends 
FormattableObject
     }
 
     /**
-     * Gets the dimension of input points.
+     * Returns the number of dimensions of input points.
      *
-     * @return the dimension of input points.
+     * @return the number of dimensions of input points.
      *
      * @see 
org.apache.sis.referencing.operation.DefaultOperationMethod#getSourceDimensions()
      */
@@ -141,15 +145,52 @@ public abstract class AbstractMathTransform extends 
FormattableObject
     public abstract int getSourceDimensions();
 
     /**
-     * Gets the dimension of output points.
+     * Returns the number of dimensions of output points.
      *
-     * @return the dimension of output points.
+     * @return the number of dimensions of output points.
      *
      * @see 
org.apache.sis.referencing.operation.DefaultOperationMethod#getTargetDimensions()
      */
     @Override
     public abstract int getTargetDimensions();
 
+    /**
+     * Returns the ranges of coordinate values which can be used as inputs.
+     * They are limits where the transform is mathematically and numerically 
applicable.
+     * 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.
+     *
+     * <p>This method is for allowing callers to crop their data for removing 
areas that may cause numerical problems.
+     * For example results of Mercator projection tend to infinity when the 
latitude value approaches a pole.
+     * For avoiding data structures with unreasonably large values or {@link 
Double#NaN},
+     * we commonly crop data to some arbitrary maximal latitude value 
(typically 80 or 84°) before projection.
+     * Those limits are arbitrary, the transform does not become suddenly 
invalid after a limit.
+     * The {@link DomainDefinition} gives some controls on the criteria for 
choosing a limit.</p>
+     *
+     * <p>Many transforms, in particular all affine transforms, have no 
mathematical limits.
+     * Consequently the default implementation returns an empty value.
+     * Again it does not mean that the {@linkplain 
org.apache.sis.referencing.operation.AbstractCoordinateOperation
+     * coordinate operation} has no geospatial domain of validity, but the 
latter is not the purpose of this method.
+     * This method is (for example) for preventing a viewer to crash when 
attempting to render a world-wide image.</p>
+     *
+     * <p>Callers do not need to search through {@linkplain 
MathTransforms#getSteps(MathTransform) transform steps}.
+     * SIS implementation of {@link MathTransforms#concatenate(MathTransform, 
MathTransform) concatenated transforms}
+     * do that automatically.</p>
+     *
+     * @param  criteria  controls the definition of transform domain.
+     * @return estimation of a domain where this transform is considered 
numerically applicable.
+     * @throws TransformException if the domain can not be estimated.
+     *
+     * @see MathTransforms#getDomain(MathTransform)
+     * @see 
org.opengis.referencing.operation.CoordinateOperation#getDomainOfValidity()
+     *
+     * @since 1.3
+     */
+    public Optional<Envelope> getDomain(DomainDefinition criteria) throws 
TransformException {
+        ArgumentChecks.ensureNonNull("criteria", criteria);
+        return Optional.empty();
+    }
+
     /**
      * Returns the parameter descriptors for this math transform, or {@code 
null} if unknown.
      *
@@ -1072,6 +1113,29 @@ public abstract class AbstractMathTransform extends 
FormattableObject
             return inverse().getSourceDimensions();
         }
 
+        /**
+         * Returns the ranges of coordinate values which can be used as inputs.
+         * The default implementation invokes {@code 
inverse().getDomain(criteria)}
+         * and transforms the returned envelope.
+         *
+         * @param  criteria  controls the definition of transform domain.
+         * @return estimation of a domain where this transform is considered 
numerically applicable.
+         * @throws TransformException if the domain can not be estimated.
+         *
+         * @since 1.3
+         */
+        @Override
+        public Optional<Envelope> getDomain(DomainDefinition criteria) throws 
TransformException {
+            final MathTransform inverse = inverse();
+            if (inverse instanceof AbstractMathTransform) {
+                final Optional<Envelope> domain = ((AbstractMathTransform) 
inverse).getDomain(criteria);
+                if (domain.isPresent()) {
+                    return Optional.of(Envelopes.transform(inverse, 
domain.get()));
+                }
+            }
+            return Optional.empty();
+        }
+
         /**
          * Gets the derivative of this transform at a point.
          * The default implementation computes the inverse of the matrix
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
index cbb120876d..e2d3a44810 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
@@ -18,8 +18,10 @@ package org.apache.sis.referencing.operation.transform;
 
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Optional;
 import java.io.Serializable;
 import org.opengis.util.FactoryException;
+import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.parameter.ParameterValueGroup;
@@ -60,7 +62,7 @@ import static java.util.logging.Logger.getLogger;
  * <p>Concatenated transforms are serializable if all their step transforms 
are serializable.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see 
org.opengis.referencing.operation.MathTransformFactory#createConcatenatedTransform(MathTransform,
 MathTransform)
  *
@@ -884,6 +886,24 @@ class ConcatenatedTransform extends AbstractMathTransform 
implements Serializabl
         }
     }
 
+    /**
+     * Returns the intersection of domains declared in transform steps.
+     * The result is in the units of input coordinates.
+     *
+     * <p>This method shall not be invoked recursively; the result would be in 
wrong units.
+     * The {@code estimateOnInverse(…)} method implementations performs {@code 
instanceof}
+     * checks for preventing that.</p>
+     *
+     * @param  criteria  domain builder passed to each transform steps.
+     */
+    @Override
+    public final Optional<Envelope> getDomain(final DomainDefinition criteria) 
throws TransformException {
+        final MathTransform head = transform1.inverse();            // == 
inverse().transform2
+        criteria.estimateOnInverse(transform2.inverse(), head);
+        criteria.estimateOnInverse(head);
+        return criteria.result();
+    }
+
     /**
      * Concatenates or pre-concatenates in an optimized way this transform 
with the given transform, if possible.
      * This method tries to delegate the concatenation to {@link #transform1} 
or {@link #transform2}.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DomainDefinition.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DomainDefinition.java
new file mode 100644
index 0000000000..32d198c066
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DomainDefinition.java
@@ -0,0 +1,244 @@
+/*
+ * 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.operation.transform;
+
+import java.util.Optional;
+import org.apache.sis.geometry.Envelopes;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.geometry.GeneralEnvelope;
+
+
+/**
+ * Specification about how to estimate a domain of validity for transforms.
+ * Contrarily 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 to
+ * not use it above some latitude threshold, typically 80° or 84°. The exact 
limit is arbitrary.
+ * 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>
+ *
+ * {@preformat java
+ *     AbstractMathTransform transform = …;
+ *     transform.getDomain(new DomainDefinition()).ifPresent((domain) -> {
+ *         // Do something here with the transform domain.
+ *     });
+ * }
+ *
+ * The {@link MathTransforms#getDomain(MathTransform)} convenience method can 
be used
+ * when the default implementation is sufficient.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ *
+ * @see MathTransforms#getDomain(MathTransform)
+ * @see AbstractMathTransform#getDomain(DomainDefinition)
+ * @see 
org.opengis.referencing.operation.CoordinateOperation#getDomainOfValidity()
+ *
+ * @since 1.3
+ * @module
+ */
+public class DomainDefinition {
+    /**
+     * Limits computed so far, or {@code null} if none.
+     */
+    private Envelope limits;
+
+    /**
+     * The envelope to use for computing intersection, created only if needed.
+     */
+    private GeneralEnvelope intersection;
+
+    /**
+     * If the transform to evaluate is a step in the middle of a chain of 
transforms,
+     * the transform to apply on the envelope computed by the step in order to 
get an
+     * envelope in domain units.
+     */
+    private ToDomain stepToDomain;
+
+    /**
+     * The transform to apply on the envelope computed by a transform step in 
order to get an envelope
+     * in the units of the requested domain. This is a node in a linked list, 
because there is potentially
+     * two or more transforms to concatenate if the transform chain is long.
+     *
+     * <p>This node lazily creates the concatenated transform when first 
requested, because it
+     * is needed only if an {@link #estimate(MathTransform)} call returned a 
non-empty value.</p>
+     */
+    private static final class ToDomain {
+        /** The first transform to apply on the envelope. */
+        private final MathTransform step;
+
+        /** The second transform to apply on the envelope, or {@code null} if 
none. */
+        private final ToDomain next;
+
+        /** Concatenation of {@link #step} followed by {@loink #next}, 
computed when first needed. */
+        private MathTransform concatenation;
+
+        /**
+         * Creates a new node in a chain of transform to potentially 
concatenate.
+         *
+         * @param  step  first transform to apply on the envelope.
+         * @param  next  second transform to apply on the envelope, or {@code 
null} if none.
+         */
+        ToDomain(final MathTransform step, final ToDomain next) {
+            this.step = step;
+            this.next = next;
+        }
+
+        /**
+         * Returns the transform to apply on domain envelope computed by a 
transform step.
+         * This is the concatenation of {@link #step} followed by all other 
steps that have
+         * been encountered while traversing a chain of transforms.
+         */
+        MathTransform concatenation() {
+            if (concatenation == null) {
+                if (next == null) {
+                    concatenation = step;
+                } else {
+                    concatenation = MathTransforms.concatenate(step, 
next.concatenation());
+                }
+            }
+            return concatenation;
+        }
+    }
+
+    /**
+     * Creates a new instance using default configuration.
+     */
+    public DomainDefinition() {
+    }
+
+    /**
+     * 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>
+     *
+     * @param  evaluated  the transform for which to estimate the domain.
+     * @throws TransformException if the domain can not be estimated.
+     */
+    public void estimate(final MathTransform evaluated) throws 
TransformException {
+        if (evaluated instanceof AbstractMathTransform) {
+            intersect(((AbstractMathTransform) 
evaluated).getDomain(this).orElse(null));
+        }
+    }
+
+    /**
+     * Estimates the domain using the inverse of a transform.
+     * The input ranges of original transform is the output ranges of inverse 
transform.
+     * Using the inverse is convenient because {@link 
ConcatenatedTransform#transform2}
+     * contains all transform steps down to the end of the chain. By contrast 
if we did not used inverse,
+     * we would have to concatenate ourselves all transform steps up to the 
beginning of the chain
+     * (because {@link ConcatenatedTransform} does not store information about 
what happened before)
+     * in order to convert the envelope provided by a step into the source 
units of the first step of the chain.
+     *
+     * <div class="note"><b>Note:</b> {@link ToDomain} records history and 
does concatenations, but it is
+     * for a corner case which would still exist in addition of the above if 
we didn't used inverse.</div>
+     *
+     * @param  inverse  inverse of the transform for which to compute domain.
+     * @throws TransformException if the domain can not be estimated.
+     */
+    final void estimateOnInverse(final MathTransform inverse) throws 
TransformException {
+        if (inverse instanceof ConcatenatedTransform) {
+            final ConcatenatedTransform ct = (ConcatenatedTransform) inverse;
+            estimateOnInverse(ct.transform2);
+            estimateOnInverse(ct.transform1, ct.transform2);
+        } else {
+            estimate(inverse.inverse());
+        }
+    }
+
+    /**
+     * Estimates the domain using the inverse of a transform and transform 
that domain using the given suffix.
+     * This method is invoked when the {@code inverse} transform is not the 
last step of a transform chain.
+     * The last steps shall be specified in the {@code tail} transform.
+     *
+     * @param  inverse  inverse of the transform for which to compute domain.
+     * @param  tail     transform to use for transforming the domain envelope.
+     * @throws TransformException if the domain can not be estimated.
+     */
+    final void estimateOnInverse(final MathTransform inverse, final 
MathTransform tail) throws TransformException {
+        final ToDomain previous = stepToDomain;
+        try {
+            stepToDomain = new ToDomain(tail, stepToDomain);
+            estimateOnInverse(inverse);
+        } finally {
+            stepToDomain = previous;
+        }
+    }
+
+    /**
+     * Sets the domain to the intersection of current domain with the 
specified envelope.
+     * The envelope coordinates shall be in units of the inputs of the {@link 
MathTransform} given to
+     * {@link #estimate(MathTransform)}. If that method is invoked recursively 
in a chain of transforms,
+     * then this method will automatically transform the given envelope to the 
units of the inputs of the
+     * first transform in the chain. If the given domain is {@code null}, then 
this method does nothing.
+     *
+     * @param  domain  the domain to intersect with, or {@code null} if none.
+     * @throws TransformException if the envelope can not be transformed to 
the domain of the first step
+     *         in a chain of transforms.
+     */
+    public void intersect(Envelope domain) throws TransformException {
+        if (domain != null) {
+            if (stepToDomain != null) {
+                domain = Envelopes.transform(stepToDomain.concatenation(), 
domain);
+            }
+            if (limits == null) {
+                limits = domain;
+            } else {
+                if (intersection == null) {
+                    limits = intersection = new GeneralEnvelope(limits);
+                }
+                intersection.intersect(domain);
+            }
+        }
+    }
+
+    /**
+     * Returns the domain computed so far by this instance. The envelope is in 
units of the
+     * inputs of the transform given in the first call to {@link 
#estimate(MathTransform)}.
+     *
+     * @return the domain of the transform being evaluated.
+     */
+    public Optional<Envelope> result() {
+        return Optional.ofNullable(limits);
+    }
+
+    /**
+     * Returns a string representation for debugging purposes.
+     *
+     * @return string representation of current domain.
+     */
+    @Override
+    public String toString() {
+        return (limits != null) ? limits.toString() : "empty";
+    }
+}
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
index 7446511218..65af836c6c 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@ -19,6 +19,7 @@ package org.apache.sis.referencing.operation.transform;
 import java.util.Map;
 import java.util.List;
 import java.util.Collections;
+import java.util.Optional;
 import java.awt.geom.AffineTransform;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
@@ -56,7 +57,7 @@ import org.apache.sis.util.Static;
  * GeoAPI factory interfaces instead.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see MathTransformFactory
  *
@@ -762,4 +763,30 @@ public final class MathTransforms extends Static {
         assert tangent.getTargetDimensions() == tgtDim;
         return tangent;
     }
+
+    /**
+     * Returns source coordinate values where the transform is mathematically 
and numerically applicable.
+     * 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 is for allowing callers to crop their data for removing 
areas that may cause numerical problems,
+     * for example latitudes too close to a pole before 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>
+     *
+     * @param  evaluated  transform for which to evaluate a domain, or {@code 
null}.
+     * @return estimation of a domain where this transform is considered 
numerically applicable.
+     * @throws TransformException if the domain can not be estimated.
+     *
+     * @see AbstractMathTransform#getDomain(DomainDefinition)
+     * @see 
org.opengis.referencing.operation.CoordinateOperation#getDomainOfValidity()
+     *
+     * @since 1.3
+     */
+    public static Optional<Envelope> getDomain(final MathTransform evaluated) 
throws TransformException {
+        if (evaluated instanceof AbstractMathTransform) {
+            return ((AbstractMathTransform) evaluated).getDomain(new 
DomainDefinition());
+        }
+        return Optional.empty();
+    }
 }
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/DomainDefinitionTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/DomainDefinitionTest.java
new file mode 100644
index 0000000000..2d25e0dc20
--- /dev/null
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/DomainDefinitionTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.operation.transform;
+
+import org.apache.sis.geometry.Envelope2D;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.apache.sis.test.ReferencingAssert.*;
+
+
+/**
+ * Tests the {@link DomainDefinition} class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+public final strictfp class DomainDefinitionTest extends TestCase {
+    /**
+     * Tests domain transformation when the domain is provided by a step in a 
chain of transforms.
+     *
+     * @throws TransformException should never happen.
+     */
+    @Test
+    public void testTransformChain() throws TransformException {
+        final AbstractMathTransform transform = new ConcatenatedTransform(new 
ConcatenatedTransform(
+                new AffineTransform2D(2, 0, 0, 4, 0, 0),  new 
PseudoTransform(2, 2)),
+                new AffineTransform2D(9, 0, 0, 9, 0, 0)); // This one should 
have no effect.
+
+        final Envelope domain = MathTransforms.getDomain(transform).get();
+        assertEnvelopeEquals(new Envelope2D(null, 0, 0, 1/2d, 1/4d), domain, 
STRICT);
+    }
+}
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PseudoTransform.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PseudoTransform.java
index ae0ba43887..73293c5428 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PseudoTransform.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PseudoTransform.java
@@ -16,8 +16,12 @@
  */
 package org.apache.sis.referencing.operation.transform;
 
+import java.util.Optional;
+import org.opengis.geometry.Envelope;
 import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.geometry.GeneralEnvelope;
 
 import static java.lang.StrictMath.*;
 
@@ -38,10 +42,10 @@ import static java.lang.StrictMath.*;
  *     3003.3
  * }
  *
- * This transform is not invertible and can not compute {@linkplain 
#derivative derivative}.
+ * This inverse transform is not effective and this transform can not compute 
{@linkplain #derivative derivative}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.5
  * @module
  */
@@ -49,7 +53,7 @@ strictfp class PseudoTransform extends AbstractMathTransform {
     /**
      * The source and target dimensions.
      */
-    protected final int sourceDimension, targetDimension;
+    protected final int sourceDimensions, targetDimensions;
 
     /**
      * Temporary buffer for copying the coordinates of a single source point.
@@ -60,29 +64,41 @@ strictfp class PseudoTransform extends 
AbstractMathTransform {
     /**
      * Creates a transform for the given dimensions.
      *
-     * @param  sourceDimension  the source dimension.
-     * @param  targetDimension  the target dimension.
+     * @param  sourceDimensions  the source dimension.
+     * @param  targetDimensions  the target dimension.
      */
-    public PseudoTransform(final int sourceDimension, final int 
targetDimension) {
-        this.sourceDimension = sourceDimension;
-        this.targetDimension = targetDimension;
-        this.buffer = new double[sourceDimension];
+    public PseudoTransform(final int sourceDimensions, final int 
targetDimensions) {
+        this.sourceDimensions = sourceDimensions;
+        this.targetDimensions = targetDimensions;
+        this.buffer = new double[sourceDimensions];
     }
 
     /**
-     * Returns the source dimension.
+     * Returns the number of source dimensions.
      */
     @Override
     public int getSourceDimensions() {
-        return sourceDimension;
+        return sourceDimensions;
     }
 
     /**
-     * Returns the target dimension.
+     * Returns the number of target dimensions.
      */
     @Override
     public int getTargetDimensions() {
-        return targetDimension;
+        return targetDimensions;
+    }
+
+    /**
+     * Returns the domain of input coordinates.
+     */
+    @Override
+    public final Optional<Envelope> getDomain(DomainDefinition criteria) {
+        final GeneralEnvelope domain = new GeneralEnvelope(sourceDimensions);
+        for (int i=0; i<sourceDimensions; i++) {
+            domain.setRange(i, 0, 1);
+        }
+        return Optional.of(domain);
     }
 
     /**
@@ -96,12 +112,31 @@ strictfp class PseudoTransform extends 
AbstractMathTransform {
                             final double[] dstPts, final int dstOff,
                             final boolean derivate) throws TransformException
     {
-        System.arraycopy(srcPts, srcOff, buffer, 0, sourceDimension);
-        for (int i=0; i<targetDimension; i++) {
-            double v = buffer[i % sourceDimension];
+        System.arraycopy(srcPts, srcOff, buffer, 0, sourceDimensions);
+        for (int i=0; i<targetDimensions; i++) {
+            double v = buffer[i % sourceDimensions];
             v += (i+1)*1000 + round(v * 1000);
             dstPts[dstOff + i] = v;
         }
         return null;
     }
+
+    /**
+     * Returns a dummy inverse transform. That transform can not apply any 
operation.
+     */
+    @Override
+    public MathTransform inverse() {
+        return new Inverse() {
+            @Override public MathTransform inverse() {
+                return PseudoTransform.this;
+            }
+
+            @Override public Matrix transform(double[] srcPts, int srcOff,
+                                              double[] dstPts, int dstOff, 
boolean derivate)
+                    throws TransformException
+            {
+                throw new TransformException("Not supported yet.");
+            }
+        };
+    }
 }
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
index 4fc71d699b..2e5bbe50ee 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
@@ -25,7 +25,7 @@ import org.junit.BeforeClass;
  * All tests from the {@code sis-referencing} module, in rough dependency 
order.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -149,6 +149,7 @@ import org.junit.BeforeClass;
     org.apache.sis.referencing.operation.transform.CartesianToPolarTest.class,
     
org.apache.sis.referencing.operation.transform.CoordinateSystemTransformTest.class,
     
org.apache.sis.referencing.operation.transform.SpecializableTransformTest.class,
+    org.apache.sis.referencing.operation.transform.DomainDefinitionTest.class,
     org.apache.sis.referencing.operation.DefaultFormulaTest.class,
     org.apache.sis.referencing.operation.DefaultOperationMethodTest.class,
     
org.apache.sis.referencing.operation.transform.OperationMethodSetTest.class,

Reply via email to