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,