You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2026/01/17 19:28:25 UTC
(sis) branch geoapi-4.0 updated: Add `MissingSourceDimensionsException` telling which axes of the target CRS cannot be mapped to a source CRS. Add `CoordinateOperationContext.getConstantCoordinates()` as a public API in replacement of `CoordinateOperations.CONSTANT_COORDINATES` internal thread-local. Add `CRS.findOperation(…) expecting `CoordinateOperationContext` argument for making possible to use the new API for constant coordinates. Redirect some loggings to user-supplied filter, for future use by data stores.
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 c440daa7c2 Add `MissingSourceDimensionsException` telling which axes of the target CRS cannot be mapped to a source CRS. Add `CoordinateOperationContext.getConstantCoordinates()` as a public API in replacement of `CoordinateOperations.CONSTANT_COORDINATES` internal thread-local. Add `CRS.findOperation(…) expecting `CoordinateOperationContext` argument for making possible to use the new API for constant coordinates. Redirect some loggings to user-supplied filter, for future use by [...]
c440daa7c2 is described below
commit c440daa7c25e588bd4399c7d0e7664ed4e053530
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 17 16:02:24 2026 +0100
Add `MissingSourceDimensionsException` telling which axes of the target CRS cannot be mapped to a source CRS.
Add `CoordinateOperationContext.getConstantCoordinates()` as a public API in replacement of `CoordinateOperations.CONSTANT_COORDINATES` internal thread-local.
Add `CRS.findOperation(…) expecting `CoordinateOperationContext` argument for making possible to use the new API for constant coordinates.
Redirect some loggings to user-supplied filter, for future use by data stores.
---
.../coverage/grid/CoordinateOperationFinder.java | 136 +++------
.../org/apache/sis/coverage/grid/GridGeometry.java | 22 +-
.../apache/sis/coverage/grid/GridGeometryTest.java | 13 +-
.../main/org/apache/sis/portrayal/Canvas.java | 2 +-
.../org/apache/sis/portrayal/CanvasContext.java | 6 +-
.../main/org/apache/sis/geometry/Envelopes.java | 55 ++--
.../main/org/apache/sis/referencing/CRS.java | 200 ++++++++++---
.../sis/referencing/MultiRegisterOperations.java | 1 +
.../sis/referencing/cs/CoordinateSystems.java | 4 +-
.../apache/sis/referencing/internal/Resources.java | 2 +-
.../sis/referencing/internal/Resources.properties | 2 +-
.../referencing/internal/Resources_fr.properties | 2 +-
.../internal/shared/CoordinateOperations.java | 8 -
.../operation/CoordinateOperationContext.java | 257 ++++++++++++----
.../operation/CoordinateOperationFinder.java | 66 +++--
.../operation/CoordinateOperationRegistry.java | 92 +++---
.../operation/InverseOperationMethod.java | 6 +-
.../operation/MismatchedDatumException.java | 8 +-
.../MissingSourceDimensionsException.java | 121 ++++++++
.../referencing/operation/SubOperationInfo.java | 323 ++++++++++++---------
.../operation/TransformedCoordinateSet.java | 5 +-
.../sis/referencing/operation/matrix/Matrices.java | 19 +-
.../matrix/NoninvertibleMatrixException.java | 8 +-
.../matrix/UnderdeterminedMatrixException.java | 81 ++++++
.../referencing/operation/matrix/MatricesTest.java | 5 +-
.../sis/storage/geotiff/MultiResolutionImage.java | 8 +-
.../main/org/apache/sis/util/Localized.java | 2 +-
.../main/org/apache/sis/util/logging/Logging.java | 2 +-
28 files changed, 982 insertions(+), 474 deletions(-)
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
index d9c7c484b5..5d8d8aacc3 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
@@ -17,10 +17,8 @@
package org.apache.sis.coverage.grid;
import java.util.Arrays;
-import java.util.function.Supplier;
import javax.measure.Quantity;
import javax.measure.quantity.Length;
-import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.util.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -30,17 +28,15 @@ import org.opengis.referencing.operation.TransformException;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.referencing.CRS;
-import org.apache.sis.referencing.internal.shared.CoordinateOperations;
import org.apache.sis.referencing.internal.shared.WraparoundApplicator;
-import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.referencing.operation.CoordinateOperationContext;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.WraparoundTransform;
import org.apache.sis.geometry.AbstractDirectPosition;
-import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.image.ImageProcessor;
import org.apache.sis.measure.Quantities;
import org.apache.sis.measure.Units;
-import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.internal.shared.Numerics;
@@ -73,7 +69,7 @@ import org.apache.sis.util.internal.shared.Numerics;
* @author Alexis Manin (Geomatys)
* @author Martin Desruisseaux (Geomatys)
*/
-final class CoordinateOperationFinder implements Supplier<double[]> {
+final class CoordinateOperationFinder extends CoordinateOperationContext {
/**
* Whether the operation is between cell centers or cell corners.
*
@@ -86,15 +82,6 @@ final class CoordinateOperationFinder implements Supplier<double[]> {
*/
private final GridGeometry source, target;
- /**
- * The target coordinate values, computed only if needed. This is computed by {@link #get()}, which is
- * itself invoked indirectly by {@link org.apache.sis.referencing.operation.CoordinateOperationFinder}.
- * The value is cached in case {@code get()} is invoked many times during the same finder execution.
- *
- * @see #get()
- */
- private double[] coordinates;
-
/**
* The coordinate operation from source to target CRS, computed when first needed.
* May be {@code null} if there is no information about the source and target CRS.
@@ -225,7 +212,7 @@ final class CoordinateOperationFinder implements Supplier<double[]> {
private boolean isWraparoundDisabled;
/**
- * Whether the {@link #get()} method should accept dimensions that are not slices in a multi-dimensional cube.
+ * Whether this {@link DirectPosition} should accept dimensions that are not slices in a multi-dimensional cube.
* If 0 (the default), the dimensions are required to be slices. If non-zero, the requirement is relaxed, with
* -1 meaning to use the low grid coordinate and +1 meaning to use the high grid coordinate.
*
@@ -270,25 +257,25 @@ final class CoordinateOperationFinder implements Supplier<double[]> {
*/
final void setAnchor(final PixelInCell newValue) {
/*
- * If `coordinates` is non-null, it means that `CoordinateOperationContext.getConstantCoordinates()`
- * has been invoked during previous `gridToCRS()` execution, which implies that `changeOfCRS` depends
- * on the `anchor` value. In such case we will need to recompute all fields that depend on `changeOfCRS`.
+ * If `hasUsedConstantValues()` is true, it means that `CoordinateOperationContext.getConstantCoordinates()`
+ * has been invoked during previous `gridToCRS()` execution, which implies that `changeOfCRS` depends on the
+ * `anchor` value. In such case, we will need to recompute all fields that depend on `changeOfCRS`.
*/
anchor = newValue;
gridToCRS = null;
crsToGrid = null;
if (hasUsedConstantValues()) {
- coordinates = null;
- changeOfCRS = null;
- forwardChangeOfCRS = null;
- inverseChangeOfCRS = null;
- knowChangeOfCRS = false;
- // Do not clear `isWraparoundNeeded`; its value is still valid.
+ super.setConstantCoordinates(null);
+ changeOfCRS = null;
+ forwardChangeOfCRS = null;
+ inverseChangeOfCRS = null;
+ knowChangeOfCRS = false;
+ // Do not clear `isWraparoundNeeded` as its value is still valid.
}
}
/**
- * Sets whether the {@link #get()} method should accept dimensions that are not slices in a multi-dimensional cube.
+ * Sets whether this {@link DirectPosition} should accept dimensions that are not slices in a multi-dimensional cube.
*
* @param mode 0 to require slices, -1 for using low grid coordinates, or +1 for high grid coordinates.
*/
@@ -338,22 +325,17 @@ final class CoordinateOperationFinder implements Supplier<double[]> {
* Returns the coordinate operation from source CRS to target CRS. It may be the identity operation.
* We try to take envelopes in account because the operation choice may depend on the geographic area.
*
- * @todo Specify also the desired resolution, computed from target grid geometry. It will require
- * more direct use of {@link org.apache.sis.referencing.operation.CoordinateOperationContext}.
- * As a side effect, we could remove {@link CoordinateOperations#CONSTANT_COORDINATES} hack.
+ * @todo Specify also the desired resolution, computed from target grid geometry.
*
* @return operation from source CRS to target CRS, or {@code null} if a CRS is not specified.
* @throws FactoryException if no operation can be found between the source and target CRS.
* @throws TransformException if some coordinates cannot be transformed to the specified target.
+ *
+ * @see #getConstantCoordinates()
*/
private CoordinateOperation changeOfCRS() throws FactoryException, TransformException {
if (!knowChangeOfCRS) {
- try {
- CoordinateOperations.CONSTANT_COORDINATES.set(this);
- changeOfCRS = changeOfCRS(source, target);
- } finally {
- CoordinateOperations.CONSTANT_COORDINATES.remove();
- }
+ changeOfCRS = changeOfCRS(source, target, this);
knowChangeOfCRS = true;
}
return changeOfCRS;
@@ -362,39 +344,27 @@ final class CoordinateOperationFinder implements Supplier<double[]> {
/**
* Computes the change of <abbr>CRS</abbr> between the given pair of grid geometries.
*
- * @param source the grid geometry which is the source of the coordinate operation to find.
- * @param target the grid geometry which is the target of the coordinate operation to find.
+ * @param source the grid geometry which is the source of the coordinate operation to find.
+ * @param target the grid geometry which is the target of the coordinate operation to find.
+ * @param context contains coordinate values to use as constant if a source CRS is missing.
* @return coordinate operation from source to target CRS, or {@code null} if none.
* @throws FactoryException if no operation can be found between the source and target CRS.
* @throws TransformException if some coordinates cannot be transformed to the specified target.
*/
- private static CoordinateOperation changeOfCRS(final GridGeometry source, final GridGeometry target)
+ private static CoordinateOperation changeOfCRS(final GridGeometry source, final GridGeometry target,
+ final CoordinateOperationContext context)
throws FactoryException, TransformException
{
CoordinateOperation changeOfCRS = null;
- final Envelope sourceEnvelope = source.envelope;
- final Envelope targetEnvelope = target.envelope;
- try {
- if (sourceEnvelope != null && targetEnvelope != null) {
- changeOfCRS = Envelopes.findOperation(sourceEnvelope, targetEnvelope);
- }
- if (changeOfCRS == null && source.isDefined(GridGeometry.CRS) && target.isDefined(GridGeometry.CRS)) {
- final CoordinateReferenceSystem sourceCRS = source.getCoordinateReferenceSystem();
- /*
- * Unconditionally create operation even if CRS are the same. A non-null operation trig
- * the check for wraparound axes, which is necessary even if the transform is identity.
- */
- DefaultGeographicBoundingBox areaOfInterest = null;
- if (sourceEnvelope != null || targetEnvelope != null) try {
- areaOfInterest = new DefaultGeographicBoundingBox();
- areaOfInterest.setBounds(targetEnvelope != null ? targetEnvelope : sourceEnvelope);
- } catch (TransformException e) {
- areaOfInterest = null;
- recoverableException("changeOfCRS", e);
- }
- changeOfCRS = CRS.findOperation(sourceCRS, target.getCoordinateReferenceSystem(), areaOfInterest);
- }
- } catch (BackingStoreException e) { // May be thrown by getConstantCoordinates().
+ if (source.isDefined(GridGeometry.CRS) && target.isDefined(GridGeometry.CRS)) try {
+ /*
+ * Unconditionally create the operation even if the CRS are equivalent. A non-null operation
+ * trigs the check for wraparound axes, which is necessary even if the transform is identity.
+ */
+ context.addAreaOfInterest(source.envelope);
+ context.addAreaOfInterest(target.envelope);
+ changeOfCRS = CRS.findOperation(source.getCoordinateMetadata(), target.getCoordinateMetadata(), context);
+ } catch (BackingStoreException e) {
throw e.unwrapOrRethrow(TransformException.class);
}
return changeOfCRS;
@@ -482,7 +452,7 @@ apply: if (forwardChangeOfCRS == null) {
/**
* Computes the transform from “geospatial coordinates of the target” to “grid coordinates of the source”.
* This is similar to invoking {@link MathTransform#inverse()} on {@link #gridToCRS()}, except in the way
- * wraparounds are handled.
+ * that wraparounds are handled.
*
* @return operation from target geospatial coordinates to source grid indices.
* @throws FactoryException if no operation can be found between the source and target CRS.
@@ -498,7 +468,7 @@ apply: if (forwardChangeOfCRS == null) {
inverseChangeOfCRS = changeOfCRS.getMathTransform().inverse();
}
} else {
- final CoordinateOperation inverse = changeOfCRS(target, source);
+ final CoordinateOperation inverse = changeOfCRS(target, source, new CoordinateOperationContext());
if (inverse != null) {
sourceCRS = inverse.getTargetCRS();
inverseChangeOfCRS = inverse.getMathTransform();
@@ -692,7 +662,7 @@ apply: if (forwardChangeOfCRS == null) {
* @param grid the source or target grid providing the point of interest.
* @param changeOfCRS transform from source CRS to target CRS, or {@code null} if none.
*/
- private static DirectPosition median(final GridGeometry grid, final MathTransform changeOfCRS) throws TransformException {
+ private static DirectPosition median(final GridGeometry grid, final MathTransform changeOfCRS) {
if (!grid.isDefined(GridGeometry.EXTENT | GridGeometry.GRID_TO_CRS)) {
return null;
}
@@ -739,21 +709,17 @@ apply: if (forwardChangeOfCRS == null) {
* For example, this is invoked during the conversion from (<var>x</var>, <var>y</var>)
* coordinates to (<var>x</var>, <var>y</var>, <var>t</var>). If constant values can
* be given to the missing dimensions, then those values are returned.
- * Otherwise this method returns {@code null}.
- *
- * <p>The returned array has a length equals to the number of dimensions in the target <abbr>CRS</abbr>.
- * Only coordinates in dimensions without source (<var>t</var> in the above example) will be used.
- * All other coordinate values will be ignored.</p>
+ * Otherwise, this method returns an empty array.
*
- * @see org.apache.sis.referencing.operation.CoordinateOperationContext#getConstantCoordinates()
+ * @see #changeOfCRS()
+ * @see CoordinateOperationContext#getConstantCoordinates()
*/
@Override
- @SuppressWarnings("ReturnOfCollectionOrArrayField")
- public double[] get() {
+ public DirectPosition getConstantCoordinates() {
+ DirectPosition coordinates = super.getConstantCoordinates();
if (coordinates == null && target.isDefined(GridGeometry.EXTENT | GridGeometry.GRID_TO_CRS)) {
final MathTransform tr = target.getGridToCRS(anchor);
- coordinates = new double[tr.getTargetDimensions()];
- double[] gc = new double[tr.getSourceDimensions()];
+ final double[] gc = new double[tr.getSourceDimensions()];
Arrays.fill(gc, Double.NaN);
final GridExtent extent = target.getExtent();
for (int i=0; i<gc.length; i++) {
@@ -776,20 +742,22 @@ apply: if (forwardChangeOfCRS == null) {
* will have non-NaN coordinate values only if they do not depend on any dimension
* other than the one having a grid size of 1.
*/
+ final var gp = new GeneralDirectPosition(tr.getTargetDimensions());
try {
- tr.transform(gc, 0, coordinates, 0, 1);
+ tr.transform(gc, 0, gp.coordinates, 0, 1);
} catch (TransformException e) {
- throw new BackingStoreException(e);
+ throw new IllegalGridGeometryException(e, "gridToCRS");
}
+ super.setConstantCoordinates(coordinates = gp);
}
return coordinates;
}
/**
- * Returns {@code true} if {@link #get()} has been invoked and computed coordinates.
+ * Returns {@code true} if {@link #getConstantCoordinates()} has been invoked and computed coordinates.
*/
final boolean hasUsedConstantValues() {
- return coordinates != null;
+ return super.getConstantCoordinates() != null;
}
/**
@@ -816,14 +784,4 @@ apply: if (forwardChangeOfCRS == null) {
processor.setPositionalAccuracyHints(hints); // Null elements will be ignored.
}
}
-
- /**
- * Invoked when an ignorable exception occurred.
- *
- * @param caller the method where the exception occurred.
- * @param e the ignorable exception.
- */
- private static void recoverableException(final String caller, final Exception e) {
- Logging.recoverableException(GridExtent.LOGGER, CoordinateOperationFinder.class, caller, e);
- }
}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index 3f8e687a46..a4d4d39e06 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
@@ -30,6 +30,7 @@ import org.opengis.util.FactoryException;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.geometry.Envelope;
+import org.opengis.coordinate.CoordinateMetadata;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
@@ -47,7 +48,7 @@ import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.AbstractEnvelope;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.ImmutableEnvelope;
-import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.coordinate.DefaultCoordinateMetadata;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.crs.AbstractCRS;
import org.apache.sis.referencing.operation.matrix.Matrices;
@@ -59,6 +60,7 @@ import org.apache.sis.referencing.internal.shared.ExtendedPrecisionMatrix;
import org.apache.sis.referencing.internal.shared.DirectPositionView;
import org.apache.sis.referencing.internal.shared.TemporalAccessor;
import org.apache.sis.referencing.internal.shared.AxisDirections;
+import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
import org.apache.sis.metadata.internal.shared.ReferencingServices;
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.util.LenientComparable;
@@ -984,6 +986,20 @@ public class GridGeometry implements LenientComparable, Serializable {
* force developers to think about whether they want a gridToCRS transform locating pixel corner or center.
*/
+ /**
+ * Returns the "real world" coordinate reference system together with the data epoch if any.
+ * This method is preferable to {@link #getCoordinateReferenceSystem()} when the <abbr>CRS</abbr> may be dynamic.
+ *
+ * @return the coordinate reference system (never {@code null}) together with the data epoch if any.
+ * @throws IncompleteGridGeometryException if this grid geometry has no <abbr>CRS</abbr> —
+ * i.e. <code>{@linkplain #isDefined isDefined}({@linkplain #CRS})</code> returned {@code false}.
+ *
+ * @since 1.6
+ */
+ public CoordinateMetadata getCoordinateMetadata() {
+ return new DefaultCoordinateMetadata(getCoordinateReferenceSystem(), null);
+ }
+
/**
* Returns the coordinate reference system of the given envelope if defined, or {@code null} if none.
* Contrarily to {@link #getCoordinateReferenceSystem()}, this method does not throw exception.
@@ -994,9 +1010,11 @@ public class GridGeometry implements LenientComparable, Serializable {
/**
* Returns the "real world" coordinate reference system.
+ * This method assumes that the <abbr>CRS</abbr> is static.
+ * If the <abbr>CRS</abbr> may be dynamic, use {@link #getCoordinateMetadata()} instead.
*
* @return the coordinate reference system (never {@code null}).
- * @throws IncompleteGridGeometryException if this grid geometry has no CRS —
+ * @throws IncompleteGridGeometryException if this grid geometry has no <abbr>CRS</abbr> —
* i.e. <code>{@linkplain #isDefined isDefined}({@linkplain #CRS})</code> returned {@code false}.
*/
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
index b5958df5c7..1cb5e7bf9b 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
@@ -16,16 +16,18 @@
*/
package org.apache.sis.coverage.grid;
+import java.util.Set;
import org.opengis.util.FactoryException;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.spatial.DimensionNameType;
+import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.crs.DerivedCRS;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
-import org.opengis.referencing.operation.OperationNotFoundException;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.referencing.operation.MissingSourceDimensionsException;
import org.apache.sis.referencing.operation.matrix.Matrix2;
import org.apache.sis.referencing.operation.matrix.Matrix3;
import org.apache.sis.referencing.operation.matrix.Matrix4;
@@ -41,6 +43,7 @@ import static org.junit.jupiter.api.Assertions.*;
import org.apache.sis.test.TestCase;
import org.apache.sis.referencing.crs.HardCodedCRS;
import org.apache.sis.referencing.operation.HardCodedConversions;
+import static org.apache.sis.test.Assertions.assertSetEquals;
import static org.apache.sis.test.Assertions.assertMessageContains;
import static org.apache.sis.referencing.Assertions.assertMatrixEquals;
import static org.apache.sis.referencing.Assertions.assertEnvelopeEquals;
@@ -547,9 +550,11 @@ public final class GridGeometryTest extends TestCase {
new double[] { 1, -3.0, 56}), envelope);
// Cannot map time to gravity-related height.
- Throwable e = assertThrows(TransformException.class, () -> grid.getEnvelope(HardCodedCRS.GEOID_3D)).getCause();
- e = assertInstanceOf(OperationNotFoundException.class, e);
- assertMessageContains(e, "WGS 84 + time", "WGS 84 + height");
+ final var e = assertInstanceOf(
+ MissingSourceDimensionsException.class,
+ assertThrows(TransformException.class, () -> grid.getEnvelope(HardCodedCRS.GEOID_3D)).getCause());
+ assertMessageContains(e, "WGS 84 + time", "MSL height");
+ assertSetEquals(Set.of(AxisDirection.UP), e.getMissingAxes());
// Ellipsoidal height is a special case, with default height of 0.
envelope = grid.getEnvelope(HardCodedCRS.WGS84_3D);
diff --git a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Canvas.java b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Canvas.java
index 89ea01640d..d0ab1fbd45 100644
--- a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Canvas.java
+++ b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/Canvas.java
@@ -408,7 +408,7 @@ public class Canvas extends Observable implements Localized {
displayBounds = new GeneralEnvelope(displayCRS);
displayBounds.setToNaN();
coordinateOperationFactory = DefaultCoordinateOperationFactory.provider();
- operationContext = new CanvasContext();
+ operationContext = new CanvasContext(locale);
}
/**
diff --git a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/CanvasContext.java b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/CanvasContext.java
index f1af2c773e..d104b6eefd 100644
--- a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/CanvasContext.java
+++ b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/portrayal/CanvasContext.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.portrayal;
+import java.util.Locale;
import java.util.Optional;
import java.util.OptionalDouble;
import org.opengis.metadata.extent.GeographicBoundingBox;
@@ -87,8 +88,11 @@ final class CanvasContext extends CoordinateOperationContext {
/**
* Creates a new context.
+ *
+ * @param locale the locale for error messages.
*/
- CanvasContext() {
+ CanvasContext(final Locale locale) {
+ setLocale(locale);
}
/**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
index 4901fe52db..85cdcf3162 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
@@ -43,16 +43,16 @@ import org.opengis.referencing.operation.OperationNotFoundException;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.util.FactoryException;
import org.opengis.metadata.extent.GeographicBoundingBox;
-import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
-import org.apache.sis.metadata.internal.shared.ReferencingServices;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.operation.CoordinateOperationContext;
import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
import org.apache.sis.referencing.operation.transform.AbstractMathTransform;
import org.apache.sis.referencing.operation.transform.WraparoundTransform;
import org.apache.sis.referencing.internal.shared.CoordinateOperations;
import org.apache.sis.referencing.internal.shared.DirectPositionView;
import org.apache.sis.referencing.internal.shared.TemporalAccessor;
+import org.apache.sis.coordinate.DefaultCoordinateMetadata;
import org.apache.sis.system.Loggers;
import org.apache.sis.util.internal.shared.Numerics;
import org.apache.sis.util.logging.Logging;
@@ -234,13 +234,13 @@ public final class Envelopes {
* GeographicBoundingBox)} CRS.findOperation(source.getCoordinateReferenceSystem(), target.getCoordinateReferenceSystem(),
* <var>areaOfInterest</var>)</code></blockquote>
*
- * If at least one envelope is null or has no CRS, then this method returns {@code null}.
+ * If at least one envelope is null or has no <abbr>CRS</abbr>, then this method returns {@code null}.
*
* @param source the source envelope, or {@code null}.
* @param target the target envelope, or {@code null}.
- * @return the mathematical operation from {@code source} CRS to {@code target} CRS,
- * or {@code null} if at least one argument is null or has no CRS.
- * @throws OperationNotFoundException if no operation was found between the given pair of CRS.
+ * @return the mathematical operation from the <abbr>CRS</abbr> of the {@code source} envelope to the <abbr>CRS</abbr>
+ * of the {@code target} envelope, or {@code null} if at least one argument is null or has no <abbr>CRS</abbr>.
+ * @throws OperationNotFoundException if no operation was found between the <abbr>CRS</abbr>s of the given envelopes.
* @throws FactoryException if the operation cannot be created for another reason.
*
* @see CRS#findOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, GeographicBoundingBox)
@@ -249,39 +249,18 @@ public final class Envelopes {
*/
public static CoordinateOperation findOperation(final Envelope source, final Envelope target) throws FactoryException {
if (source != null && target != null) {
- final CoordinateReferenceSystem sourceCRS, targetCRS;
- if ((sourceCRS = source.getCoordinateReferenceSystem()) != null &&
- (targetCRS = target.getCoordinateReferenceSystem()) != null)
- {
- /*
- * Computing the area of interest (AOI) unconditionally would be harmless, but it is useless if the CRS
- * are the same since the result will be identity anyway. Conversely we could skip AOI computation more
- * often for example with the following condition instead of !=:
- *
- * !Utilities.deepEquals(sourceCRS, targetCRS, ComparisonMode.ALLOW_VARIANT)
- *
- * but it would not be very advantageous if testing the condition is almost as long as computing the AOI.
- * For now we keep != as a very cheap test which will work quite often.
- */
- DefaultGeographicBoundingBox areaOfInterest = null;
- if (sourceCRS != targetCRS) try {
- final DefaultGeographicBoundingBox targetAOI;
- final ReferencingServices converter = ReferencingServices.getInstance();
- areaOfInterest = converter.setBounds(source, null, "findOperation"); // Should be first.
- targetAOI = converter.setBounds(target, null, "findOperation");
- if (areaOfInterest == null) {
- areaOfInterest = targetAOI;
- } else if (targetAOI != null) {
- areaOfInterest.add(targetAOI);
- }
- } catch (TransformException e) {
- /*
- * Note: we may succeed to transform `source` and fail to transform `target` to geographic bounding box,
- * but the opposite is unlikely because `source` should not have less dimensions than `target`.
- */
- Logging.recoverableException(LOGGER, Envelopes.class, "findOperation", e);
+ final CoordinateReferenceSystem sourceCRS = source.getCoordinateReferenceSystem();
+ if (sourceCRS != null) {
+ final CoordinateReferenceSystem targetCRS = target.getCoordinateReferenceSystem();
+ if (targetCRS != null && !CRS.equivalent(sourceCRS, targetCRS)) {
+ final var context = new CoordinateOperationContext();
+ context.addAreaOfInterest(source);
+ context.addAreaOfInterest(target);
+ return CRS.findOperation(
+ new DefaultCoordinateMetadata(sourceCRS, null),
+ new DefaultCoordinateMetadata(targetCRS, null),
+ context);
}
- return CRS.findOperation(sourceCRS, targetCRS, areaOfInterest);
}
}
return null;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index 8fb46bdba5..a55de2d6ad 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
@@ -86,6 +86,7 @@ import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
import org.apache.sis.referencing.operation.DefaultConversion;
import org.apache.sis.referencing.factory.GeodeticObjectFactory;
import org.apache.sis.referencing.factory.UnavailableFactoryException;
+import org.apache.sis.coordinate.DefaultCoordinateMetadata;
import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
import org.apache.sis.metadata.iso.extent.Extents;
import org.apache.sis.util.ArgumentChecks;
@@ -105,20 +106,21 @@ import org.opengis.coordinate.CoordinateMetadata;
/**
- * Static methods working on {@linkplain CoordinateReferenceSystem Coordinate Reference Systems}.
+ * Static methods working on Coordinate Reference System (<abbr>CRS</abbr>) objects.
* The methods defined in this class can be grouped in three categories:
*
* <ul>
* <li>Factory methods, the most notable one being {@link #forCode(String)}.</li>
* <li>Methods providing information, like {@link #isHorizontalCRS(CoordinateReferenceSystem)}.</li>
- * <li>Finding coordinate operations between a source and a target CRS.</li>
+ * <li>Finding coordinate operations between a source and a target <abbr>CRS</abbr>.</li>
* </ul>
*
* <h2>Usage example</h2>
* The most frequently used methods in this class are {@link #forCode forCode(…)}, {@link #fromWKT fromWKT(…)}
- * and {@link #findOperation findOperation(…)}. An usage example is like below
- * (see the <a href="https://sis.apache.org/tables/CoordinateReferenceSystems.html">Apache SIS™ Coordinate
- * Reference System (CRS) codes</a> page for the complete list of EPSG codes):
+ * and {@link #findOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, GeographicBoundingBox) findOperation(…)}.
+ * An usage example is like below
+ * (see the <a href="https://sis.apache.org/tables/CoordinateReferenceSystems.html">Apache <abbr>SIS</abbr>™
+ * Coordinate Reference System (<abbr>CRS</abbr>) codes</a> page for the complete list of EPSG codes):
*
* {@snippet lang="java" :
* CoordinateReferenceSystem source = CRS.forCode("EPSG:4326"); // WGS 84
@@ -134,7 +136,7 @@ import org.opengis.coordinate.CoordinateMetadata;
* System.out.println(position);
* }
*
- * <h2>Note on kinds of CRS</h2>
+ * <h2>Note on kinds of <abbr>CRS</abbr></h2>
* The {@link #getSingleComponents(CoordinateReferenceSystem)} method decomposes an arbitrary CRS into a flat
* list of single components. In such flat list, vertical and temporal components can easily be identified by
* {@code instanceof} checks. But identifying the horizontal component is not as easy. The list below suggests
@@ -673,35 +675,35 @@ public final class CRS {
* @throws FactoryException if the operation cannot be created for another reason.
*
* @since 1.5
+ *
+ * @deprecated Replaced by {@link #findOperation(CoordinateMetadata, CoordinateMetadata, CoordinateOperationContext)}.
+ * This method will be removed for avoiding ambiguity when the last argument is null.
*/
+ @Deprecated(since = "1.6", forRemoval = true)
public static CoordinateOperation findOperation(final CoordinateMetadata source,
final CoordinateMetadata target,
final GeographicBoundingBox areaOfInterest)
throws FactoryException
{
- // TODO: take epoch in account.
- if (source.getCoordinateEpoch().isPresent() || target.getCoordinateEpoch().isPresent()) {
- throw new FactoryException("This version of Apache SIS does not yet support coordinate epoch.");
- }
- return findOperation(source.getCoordinateReferenceSystem(), target.getCoordinateReferenceSystem(), areaOfInterest);
+ return findOperation(source, target, CoordinateOperationContext.fromBoundingBox(areaOfInterest));
}
/**
- * Finds a mathematical operation that transforms or converts coordinates from the given source to the
- * given target coordinate reference system. If an estimation of the geographic area containing the points
- * to transform is known, it can be specified for helping this method to find a better suited operation.
+ * Finds a mathematical operation that transform coordinates from the given source to the given target <abbr>CRS</abbr>.
+ * If an estimation of the geographic area containing the points to transform is known,
+ * it can be specified for helping this method to find a better suited operation.
* If no area of interest is specified, then the current default is the widest
* {@linkplain DefaultObjectDomain#getDomainOfValidity() domain of validity}.
* A future Apache SIS version may also take the country of current locale in account.
*
- * <div class="note"><b>Note:</b>
- * the area of interest is just one aspect that may affect the coordinate operation.
- * Other aspects are the time of interest (because some coordinate operations take in account the
- * plate tectonics movement) or the desired accuracy. For more control on the coordinate operation
- * to create, see {@link CoordinateOperationContext}.</div>
+ * <p>Note that the area of interest is only one aspect that may affect the coordinate operation.
+ * Other aspects are the data epochs (because some coordinate operations take in account the plate tectonics movement)
+ * or the desired accuracy. For more control on the coordinate operation to create, see the
+ * {@linkplain #findOperation(CoordinateMetadata, CoordinateMetadata, CoordinateOperationContext) method below}.
+ * Alternatively, the area of interest can also be {@linkplain Envelopes#findOperation(Envelope, Envelope) specified as envelopes}.</p>
*
- * After the caller received a {@code CoordinateOperation} instance, the following methods can be invoked
- * for checking if the operation suits the caller's needs:
+ * <p>After the caller received a {@code CoordinateOperation} instance,
+ * the following methods can be invoked for checking if the operation suits the caller's needs:</p>
*
* <ul>
* <li>{@link #getGeographicBoundingBox(CoordinateOperation)}
@@ -710,26 +712,37 @@ public final class CRS {
* for checking if the operation has sufficient accuracy for caller's purpose.</li>
* </ul>
*
- * If the source and target CRS are equivalent, then this method returns an operation backed by an
- * {@linkplain org.opengis.referencing.operation.MathTransform#isIdentity() identity} transform.
- * If there is no known operation between the given pair of CRS, then this method throws an
- * {@link OperationNotFoundException}.
+ * <p>If the source and target <abbr>CRS</abbr> are equivalent, then this method returns an operation
+ * backed by an {@linkplain org.opengis.referencing.operation.MathTransform#isIdentity() identity} transform.
+ * If there is no known operation between the given pair of <abbr>CRS</abbr>s,
+ * then this method throws an {@link OperationNotFoundException}.</p>
*
- * <p>Note that <code>CRS.findOperation(<var>B</var>, <var>A</var>, <var>aoi</var>)</code> is not necessarily
+ * <h4>Inverse operation</h4>
+ * Note that <code>CRS.findOperation(<var>B</var>, <var>A</var>, <var>aoi</var>)</code> is not necessarily
* the exact converse of <code>CRS.findOperation(<var>A</var>, <var>B</var>, <var>aoi</var>)</code>.
- * Some deviations may exist for example because of different paths explored in the geodetic database.
- * For the inverse of an existing {@link CoordinateOperation}, using
- * {@link org.opengis.referencing.operation.MathTransform#inverse()} is preferable.</p>
- *
- * @param sourceCRS the CRS of source coordinates.
- * @param targetCRS the CRS of target coordinates.
+ * Some deviations may exist, for example because of different paths explored in the geodetic database.
+ * For the mathematical inverse of an existing {@link CoordinateOperation}, using
+ * {@link org.opengis.referencing.operation.MathTransform#inverse()} is preferable.
+ *
+ * <h4>Advanced usages</h4>
+ * It is not recommended to use the returned coordinate operation for transforming bounding boxes simply by
+ * transforming the four corners. Use the {@linkplain Envelopes#transform(Envelope, CoordinateReferenceSystem)
+ * envelope transformation methods} instead. These methods will take care of line curvature, envelope spanning
+ * the anti-meridian (longitude wraparound) and singularity points at the poles.
+ *
+ * <p>Likewise, for finding an operation for transforming pixel coordinates between two rasters, use the
+ * {@linkplain org.apache.sis.coverage.grid.GridGeometry#createTransformTo grid geometry method} instead.
+ * That latter method will take care of rasters spanning the anti-meridian, contrarily to the operations
+ * returned by this {@code CRS} class.</p>
+ *
+ * @param sourceCRS the <abbr>CRS</abbr> of source coordinates.
+ * @param targetCRS the <abbr>CRS</abbr> of target coordinates.
* @param areaOfInterest the area of interest, or {@code null} if none.
* @return the mathematical operation from {@code sourceCRS} to {@code targetCRS}.
* @throws OperationNotFoundException if no operation was found between the given pair of <abbr>CRS</abbr>s.
* @throws FactoryException if the operation cannot be created for another reason.
*
* @see Envelopes#findOperation(Envelope, Envelope)
- * @see DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext)
*
* @since 0.7
*/
@@ -740,7 +753,66 @@ public final class CRS {
{
ArgumentChecks.ensureNonNull("sourceCRS", sourceCRS);
ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
- final CoordinateOperationContext context = CoordinateOperationContext.fromBoundingBox(areaOfInterest);
+ return findOperation(
+ new DefaultCoordinateMetadata(sourceCRS, null),
+ new DefaultCoordinateMetadata(targetCRS, null),
+ CoordinateOperationContext.fromBoundingBox(areaOfInterest));
+ }
+
+ /**
+ * Finds a mathematical operation that transforms coordinates between the given <abbr>CRS</abbr>s and epochs.
+ * If many operations exist between the specified pair of <abbr>CRS</abbr>s, an operation is selected using the
+ * area of interest (<abbr>AOI</abbr>) and desired accuracy specified in the optional {@code context} argument:
+ * the operation having a {@linkplain DefaultObjectDomain#getDomainOfValidity() domain of validity} resulting in
+ * the widest intersection with the <abbr>AOI</abbr> is preferred. If many operations result in the same intersection,
+ * then the operation having an accuracy just sufficient for the desired accuracy is preferred.
+ *
+ * <p>After the caller received a {@code CoordinateOperation} instance,
+ * the following methods can be invoked for checking if the operation suits the caller's needs:</p>
+ *
+ * <ul>
+ * <li>{@link #getGeographicBoundingBox(CoordinateOperation)}
+ * for checking if the operation is valid in the caller's area of interest.</li>
+ * <li>{@link #getLinearAccuracy(CoordinateOperation)}
+ * for checking if the operation has sufficient accuracy for caller's purpose.</li>
+ * </ul>
+ *
+ * <p>If the source and target <abbr>CRS</abbr> are equivalent, then this method returns an operation
+ * backed by an {@linkplain org.opengis.referencing.operation.MathTransform#isIdentity() identity} transform.
+ * If there is no known operation between the given pair of <abbr>CRS</abbr>s,
+ * then this method throws an {@link OperationNotFoundException}.</p>
+ *
+ * <h4>Inverse operation</h4>
+ * Note that <code>CRS.findOperation(<var>B</var>, <var>A</var>, <var>context</var>)</code> is not necessarily
+ * the exact converse of <code>CRS.findOperation(<var>A</var>, <var>B</var>, <var>context</var>)</code>.
+ * Some deviations may exist, for example because of different paths explored in the geodetic database.
+ * For the mathematical inverse of an existing {@link CoordinateOperation}, using
+ * {@link org.opengis.referencing.operation.MathTransform#inverse()} is preferable.
+ *
+ * @param source the <abbr>CRS</abbr> and epoch of source coordinates.
+ * @param target the <abbr>CRS</abbr> and epoch of target coordinates.
+ * @param context area of interest, desired accuracy and other options, or {@code null} if none.
+ * @return the mathematical operation from {@code source} to {@code target}.
+ * @throws OperationNotFoundException if no operation was found between the given pair of <abbr>CRS</abbr>s and epochs.
+ * @throws FactoryException if the operation cannot be created for another reason.
+ *
+ * @see DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext)
+ *
+ * @since 1.6
+ */
+ public static CoordinateOperation findOperation(final CoordinateMetadata source,
+ final CoordinateMetadata target,
+ final CoordinateOperationContext context)
+ throws FactoryException
+ {
+ ArgumentChecks.ensureNonNull("source", source);
+ ArgumentChecks.ensureNonNull("target", target);
+ if (source.getCoordinateEpoch().isPresent() || target.getCoordinateEpoch().isPresent()) {
+ throw new FactoryException("This version of Apache SIS does not yet support coordinate epoch.");
+ }
+ // TODO: take epoch in account.
+ final CoordinateReferenceSystem sourceCRS = source.getCoordinateReferenceSystem();
+ final CoordinateReferenceSystem targetCRS = target.getCoordinateReferenceSystem();
/*
* In principle following code should just delegate to factory.createOperation(…). However, that operation
* may fail if a connection to the EPSG database has been found, but the EPSG tables do not yet exist in
@@ -763,21 +835,22 @@ public final class CRS {
}
/**
- * Finds mathematical operations that transform or convert coordinates from the given source to the
- * given target coordinate reference system. If at least one operation exists, they are returned in
- * preference order: the operation having the widest intersection between its
- * {@linkplain DefaultObjectDomain#getDomainOfValidity() domain of validity}
- * and the given area of interest are returned first.
- *
- * @param sourceCRS the CRS of source coordinates.
- * @param targetCRS the CRS of target coordinates.
+ * Finds mathematical operations that transform coordinates from the given source to the given target <abbr>CRS</abbr>
+ * in a given area of interest. If many operations exist, they are returned in preference order: the operation having
+ * the widest intersection between its {@linkplain DefaultObjectDomain#getDomainOfValidity() domain of validity} and
+ * the given area of interest are returned first.
+ *
+ * <p>This is a convenience method for static <abbr>CRS</abbr>s and a context defined only by the area of interest.
+ * For an alternative allowing to specify data epochs (for dynamic <abbr>CRS</abbr>s) and desired accuracy, see the
+ * {@linkplain #findOperations(CoordinateMetadata, CoordinateMetadata, CoordinateOperationContext) method below}.</p>
+ *
+ * @param sourceCRS the <abbr>CRS</abbr> of source coordinates.
+ * @param targetCRS the <abbr>CRS</abbr> of target coordinates.
* @param areaOfInterest the area of interest, or {@code null} if none.
* @return mathematical operations from {@code sourceCRS} to {@code targetCRS}.
- * @throws OperationNotFoundException if no operation was found between the given pair of CRS.
+ * @throws OperationNotFoundException if no operation was found between the given pair of <abbr>CRS</abbr>s.
* @throws FactoryException if the operation cannot be created for another reason.
*
- * @see DefaultCoordinateOperationFactory#createOperations(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext)
- *
* @since 1.0
*/
public static List<CoordinateOperation> findOperations(final CoordinateReferenceSystem sourceCRS,
@@ -787,7 +860,42 @@ public final class CRS {
{
ArgumentChecks.ensureNonNull("sourceCRS", sourceCRS);
ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
- final CoordinateOperationContext context = CoordinateOperationContext.fromBoundingBox(areaOfInterest);
+ return findOperations(
+ new DefaultCoordinateMetadata(sourceCRS, null),
+ new DefaultCoordinateMetadata(targetCRS, null),
+ CoordinateOperationContext.fromBoundingBox(areaOfInterest));
+ }
+
+ /**
+ * Finds mathematical operations that transforms coordinates between the given <abbr>CRS</abbr>s and epochs.
+ * If many operations exist, they are sorted in preference order according criteria specified by {@code context}:
+ * best matches with the area of interest (<abbr>AOI</abbr>) are first, then operations matching <abbr>AOI</abbr>
+ * equally well are sorted by best matches with the desired accuracy.
+ *
+ * @param source the <abbr>CRS</abbr> and epoch of source coordinates.
+ * @param target the <abbr>CRS</abbr> and epoch of target coordinates.
+ * @param context area of interest, desired accuracy and other options, or {@code null} if none.
+ * @return mathematical operations from {@code source} to {@code target} <abbr>CRS</abbr> and epoch.
+ * @throws OperationNotFoundException if no operation was found between the given pair of <abbr>CRS</abbr>s.
+ * @throws FactoryException if the operation cannot be created for another reason.
+ *
+ * @see DefaultCoordinateOperationFactory#createOperations(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext)
+ *
+ * @since 1.6
+ */
+ public static List<CoordinateOperation> findOperations(final CoordinateMetadata source,
+ final CoordinateMetadata target,
+ final CoordinateOperationContext context)
+ throws FactoryException
+ {
+ ArgumentChecks.ensureNonNull("source", source);
+ ArgumentChecks.ensureNonNull("target", target);
+ if (source.getCoordinateEpoch().isPresent() || target.getCoordinateEpoch().isPresent()) {
+ throw new FactoryException("This version of Apache SIS does not yet support coordinate epoch.");
+ }
+ // TODO: take epoch in account.
+ final CoordinateReferenceSystem sourceCRS = source.getCoordinateReferenceSystem();
+ final CoordinateReferenceSystem targetCRS = target.getCoordinateReferenceSystem();
final DefaultCoordinateOperationFactory factory = DefaultCoordinateOperationFactory.provider();
try {
return factory.createOperations(sourceCRS, targetCRS, context);
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java
index 75d861363b..fbab73afa0 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java
@@ -27,6 +27,7 @@ import org.opengis.util.Factory;
import org.opengis.util.FactoryException;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.referencing.AuthorityFactory;
+import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
index f497b1bc42..fdb305898d 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
@@ -46,6 +46,7 @@ import org.apache.sis.referencing.internal.shared.AxisDirections;
import org.apache.sis.referencing.internal.shared.Formulas;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.matrix.UnderdeterminedMatrixException;
/**
@@ -334,7 +335,8 @@ next: for (final CoordinateSystem cs : targets) {
* @param targetCS the target coordinate system.
* @return the conversion from {@code sourceCS} to {@code targetCS} as an affine transform.
* Only axis direction and units are taken in account.
- * @throws IllegalArgumentException if the CS are not of the same type, or axes do not match.
+ * @throws IllegalArgumentException if the <abbr>CS</abbr> are not of the same type, or axes do not match.
+ * @throws UnderdeterminedMatrixException if {@code targetCS} has an axis that {@code sourceCS} does not have.
* @throws IncommensurableException if the units are not compatible, or the conversion is non-linear.
*
* @see Matrices#createTransform(AxisDirection[], AxisDirection[])
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
index dd2f78b00b..641280441a 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
@@ -188,7 +188,7 @@ public class Resources extends IndexedResourceBundle {
public static final short ConformanceMeansDatumShift = 11;
/**
- * A constant value is required for the coordinate at index {0}.
+ * A constant value is required for coordinate axis “{0}”.
*/
public static final short ConstantCoordinateValueRequired_1 = 109;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
index a602475cb3..836bbae563 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
@@ -71,7 +71,7 @@ CanNotTransformEnvelopeToGeodetic = Cannot transform envelope to a geodetic refe
CanNotTransformGeometry = Cannot transform the given geometry.
CanNotUseGeodeticParameters_2 = Cannot use the {0} geodetic parameters. Caused by: {1}
ColinearAxisDirections_2 = Axis directions {0} and {1} are colinear.
-ConstantCoordinateValueRequired_1 = A constant value is required for the coordinate at index {0}.
+ConstantCoordinateValueRequired_1 = A constant value is required for coordinate axis \u201c{0}\u201d.
CoordinateOperationNotFound_2 = Coordinate operation from system \u201c{0}\u201d to \u201c{1}\u201d has not been found.
DatumChangesDirectory_1 = Datum shift files are searched in the \u201c{0}\u201d directory.
DatumChangeNotFound_2 = No information about how to change from datum \u201c{0}\u201d to \u201c{1}\u201d.
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
index 981a56dd6b..751d994bc7 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
@@ -76,7 +76,7 @@ CanNotTransformEnvelopeToGeodetic = Ne peut pas transformer l\u2019enveloppe ver
CanNotTransformGeometry = Ne peut pas transformer la g\u00e9om\u00e9trie donn\u00e9e.
CanNotUseGeodeticParameters_2 = Ne peut pas utiliser les param\u00e8tres g\u00e9od\u00e9siques {0}. La cause est\u202f: {1}
ColinearAxisDirections_2 = Les directions d\u2019axes {0} et {1} sont colin\u00e9aires.
-ConstantCoordinateValueRequired_1 = Une valeur constante est requise pour la coordonn\u00e9e \u00e0 l\u2019index {0}.
+ConstantCoordinateValueRequired_1 = Une valeur constante est requise pour la coordonn\u00e9e de l\u2019axe \u00ab\u202f{0}\u202f\u00bb.
CoordinateOperationNotFound_2 = L\u2019op\u00e9ration sur les coordonn\u00e9es du syst\u00e8me \u00ab\u202f{0}\u202f\u00bb vers \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e.
DatumChangesDirectory_1 = Les fichiers de changements de r\u00e9f\u00e9rentiel sont cherch\u00e9s dans le dossier \u00ab\u202f{0}\u202f\u00bb.
DatumChangeNotFound_2 = Il n\u2019y a pas d\u2019information sur la fa\u00e7on de passer du r\u00e9f\u00e9rentiel \u00ab\u202f{0}\u202f\u00bb vers \u00ab\u202f{1}\u202f\u00bb.
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
index 47f2423d40..83640f532b 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
@@ -19,7 +19,6 @@ package org.apache.sis.referencing.internal.shared;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
-import java.util.function.Supplier;
import java.util.logging.Logger;
import javax.measure.UnitConverter;
import javax.measure.IncommensurableException;
@@ -87,13 +86,6 @@ public final class CoordinateOperations {
*/
public static final String OPERATION_TYPE_KEY = "operationType";
- /**
- * Value of {@link org.apache.sis.referencing.operation.CoordinateOperationContext#getConstantCoordinates()}.
- * This thread-local is used as a workaround for the fact that we do not yet provide a public API for this
- * functionality. This workaround should be deleted after a public API is defined.
- */
- public static final ThreadLocal<Supplier<double[]>> CONSTANT_COORDINATES = new ThreadLocal<>();
-
/**
* Cached values of {@link #wrapAroundChanges wrapAroundChanges(…)}, created when first needed.
* Indices are bit masks computed by {@link #changes changes(…)}. Since the most common "wrap around" axes
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
index cc0c608536..1df1f3f0d0 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
@@ -16,22 +16,25 @@
*/
package org.apache.sis.referencing.operation;
-import java.io.Serializable;
-import java.util.function.Supplier;
+import java.util.Locale;
+import java.util.logging.Filter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.logging.LogRecord;
import java.util.function.Predicate;
+import org.opengis.geometry.Envelope;
+import org.opengis.geometry.DirectPosition;
import org.opengis.metadata.extent.Extent;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.TransformException;
-import org.opengis.referencing.operation.OperationNotFoundException;
-import org.apache.sis.referencing.internal.shared.CoordinateOperations;
import org.apache.sis.metadata.iso.extent.DefaultExtent;
import org.apache.sis.metadata.iso.extent.Extents;
import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Localized;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.measure.Latitude;
import org.apache.sis.measure.Longitude;
-import org.apache.sis.util.collection.BackingStoreException;
/**
@@ -40,14 +43,14 @@ import org.apache.sis.util.collection.BackingStoreException;
*
* <ul>
* <li>The geographic area where the transformation will be used.</li>
- * <li>The desired accuracy. A coarser accuracy may allow SIS to choose a faster transformation method.</li>
+ * <li>The desired accuracy. A coarser accuracy may allow <abbr>SIS</abbr> to choose a faster transformation method.</li>
* </ul>
*
* While optional, those information can help {@link DefaultCoordinateOperationFactory}
* to choose the most suitable coordinate transformation between two CRS.
*
- * <p>{@code CoordinateOperationContext} is part of the API
- * used by SIS for implementing the <i>late binding</i> model.
+ * <p>{@code CoordinateOperationContext} is part of the <abbr>API</abbr>
+ * used by <abbr>SIS</abbr> for implementing the <i>late binding</i> model.
* See {@linkplain org.apache.sis.referencing.operation package javadoc}
* for a note on early binding versus late binding implementations.</p>
*
@@ -59,24 +62,18 @@ import org.apache.sis.util.collection.BackingStoreException;
* to Texas, but not suitable to the rest of North-America.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.6
* @since 0.7
*
* @todo Should also take the country of a {@link java.util.Locale}. The EPSG database contains ISO2 and ISO3
* identifiers that we can use.
*/
-public class CoordinateOperationContext implements Serializable {
- /**
- * For cross-version compatibility.
- */
- private static final long serialVersionUID = -6944460471653277973L;
-
+public class CoordinateOperationContext implements Localized {
/**
* The spatiotemporal area of interest, or {@code null} if none. This instance may be updated or
* replaced by other methods in this class, or (indirectly) by {@link CoordinateOperationFinder}.
*/
- @SuppressWarnings("serial") // Most SIS implementations are serializable.
- private Extent areaOfInterest;
+ private DefaultExtent areaOfInterest;
/**
* The desired accuracy in metres, or 0 for the best accuracy available.
@@ -84,6 +81,23 @@ public class CoordinateOperationContext implements Serializable {
*/
private double desiredAccuracy;
+ /**
+ * Coordinate values that can be considered as constant, or {@code null} if none.
+ *
+ * @see #getConstantCoordinates()
+ */
+ private DirectPosition constantCoordinates;
+
+ /**
+ * The locale for error messages, or {@code null} for the default locale.
+ */
+ private Locale locale;
+
+ /**
+ * Whether to log a warning.
+ */
+ private transient Filter logFilter;
+
/**
* Creates a new context with no area of interest and the best accuracy available.
*/
@@ -151,38 +165,73 @@ public class CoordinateOperationContext implements Serializable {
*
* @param area the spatiotemporal area of interest, or {@code null} if none.
*/
- public void setAreaOfInterest(Extent area) {
- if (area != null) {
- area = new DefaultExtent(area);
- }
- areaOfInterest = area;
+ public void setAreaOfInterest(final Extent area) {
+ areaOfInterest = (area != null) ? new DefaultExtent(area) : null;
}
/**
- * Sets the geographic component of the area of interest, or {@code null} if none.
- * This convenience method set the bounding box into the spatiotemporal {@link Extent}.
+ * Sets the geographic element of the area of interest, or {@code null} if none.
+ * This convenience method sets the given bounding box into the spatiotemporal {@link Extent}.
*
* <p>The reverse operation can be done with <code>{@linkplain Extents#getGeographicBoundingBox(Extent)
* Extents.getGeographicBoundingBox}({@linkplain #getAreaOfInterest()})</code>.</p>
*
- * @param area the geographic area of interest, or {@code null} if none.
+ * @param area the new geographic area of interest, or {@code null} if none.
*/
public void setAreaOfInterest(final GeographicBoundingBox area) {
areaOfInterest = setGeographicBoundingBox(areaOfInterest, area);
}
+ /**
+ * Adds a geographic element of the area of interest.
+ * The given area is added to the list returned by {@link Extent#getGeographicElements()}.
+ * If the list contains two or more elements, {@link DefaultCoordinateOperationFactory}
+ * will use the union of all elements.
+ *
+ * @param area the geographic area of interest to add, or {@code null} if none.
+ * @since 1.6
+ */
+ public void addAreaOfInterest(final GeographicBoundingBox area) {
+ if (area != null) {
+ if (areaOfInterest == null) {
+ setAreaOfInterest(area);
+ } else {
+ areaOfInterest.getGeographicElements().add(area);
+ }
+ }
+ }
+
+ /**
+ * Adds the geographic, vertical and temporal elements of an envelope as areas of interest.
+ * The geographic element is added to the list returned by {@link Extent#getGeographicElements()},
+ * and likewise for vertical and temporal components. If the envelope cannot be transformed to the
+ * <abbr>CRS</abbr> expected by a component list, then the exception is reported to the
+ * {@linkplain #getLogFilter() log filter} and eventually logged.
+ *
+ * @param area the envelope to add as an area of interest, or {@code null} if none.
+ * @since 1.6
+ */
+ public void addAreaOfInterest(final Envelope area) {
+ if (area != null) try {
+ if (areaOfInterest == null) {
+ areaOfInterest = new DefaultExtent();
+ }
+ areaOfInterest.addElements(area);
+ } catch (TransformException e) {
+ recoverableException("addAreaOfInterest", e);
+ }
+ }
+
/**
* Sets the given geographic bounding box in the given extent.
- * This method may modify the extent given in parameters.
+ * Modifies the extent given in parameters if non-null, or otherwise creates a new extent.
*
* @param areaOfInterest the extent to update, or {@code null}.
* @return the updated extent. May be the given one or a new instance.
*/
- static Extent setGeographicBoundingBox(Extent areaOfInterest, final GeographicBoundingBox bbox) {
+ static DefaultExtent setGeographicBoundingBox(DefaultExtent areaOfInterest, final GeographicBoundingBox bbox) {
if (areaOfInterest != null) {
- final DefaultExtent ex = DefaultExtent.castOrCopy(areaOfInterest);
- ex.setGeographicElements(Containers.singletonOrEmpty(bbox));
- areaOfInterest = ex;
+ areaOfInterest.setGeographicElements(Containers.singletonOrEmpty(bbox));
} else if (bbox != null) {
areaOfInterest = new DefaultExtent(null, bbox, null, null);
}
@@ -226,39 +275,143 @@ public class CoordinateOperationContext implements Serializable {
}
/**
- * Invoked when at least one coordinate in the target <abbr>CRS</abbr> cannot be computed from the coordinates in
- * the source <abbr>CRS</abbr>. For example, if the source <abbr>CRS</abbr> has (<var>x</var>, <var>y</var>) axes
+ * Returns coordinate values that may be considered as constant, or {@code null} if none.
+ * This method is invoked when at least one coordinate in the target <abbr>CRS</abbr>
+ * cannot be computed from the coordinates in the source <abbr>CRS</abbr>.
+ * For example, if the source <abbr>CRS</abbr> has (<var>x</var>, <var>y</var>) axes
* and the target <abbr>CRS</abbr> has (<var>x</var>, <var>y</var>, <var>t</var>) axes,
* then this method is invoked for determining which value to assign to the <var>t</var> coordinate.
- * In some cases, the user can tell that the coordinate should be set to a constant value.
+ * All other coordinates will be ignored and can be NaN.
+ *
+ * <h4>Coordinate reference system</h4>
+ * If {@link DirectPosition#getCoordinateReferenceSystem()} returns null, the default coordinate reference system
+ * is the target <abbr>CRS</abbr> of the requested coordinate operation. Otherwise, the <abbr>CRS</abbr> of the
+ * returned position should be one of the components of the target <abbr>CRS</abbr>. If the <abbr>CRS</abbr> is
+ * not recognized, the returned position may be ignored.
*
- * <p>If this method returns {@code null}, then the {@link CoordinateOperationFinder} caller will throw an
- * {@link OperationNotFoundException}. Otherwise, the returned array should have a length equals to the number
- * of dimensions in the full (usually compound) target <abbr>CRS</abbr>. Only coordinate values in dimensions
- * without source (the <var>t</var> dimension in the above example) will be used.
- * All other coordinate values will be ignored.</p>
+ * @return coordinates to take as constants, or {@code null} if none.
+ * @since 1.6
+ */
+ public DirectPosition getConstantCoordinates() {
+ return constantCoordinates;
+ }
+
+ /**
+ * Sets coordinate values that may be considered as constant.
+ * This position will be used if at least one coordinate in the target <abbr>CRS</abbr>
+ * cannot be computed from the coordinates in the source <abbr>CRS</abbr>.
*
- * @return coordinate values to take as constants for the specified target component, or {@code null} if none.
- * @throws TransformException if the coordinates cannot be computed. This exception may occur when the constant
- * coordinate values are the results of performing a coordinate operation in advance.
+ * <h4>Allowed coordinate reference systems</h4>
+ * The Coordinate Reference System (<abbr>CRS</abbr>) of the given position is optional.
+ * If absent, it is assumed to be the target <abbr>CRS</abbr> of the coordinate operation.
+ * If present, it should be either the target <abbr>CRS</abbr> of the requested operation,
+ * or one of the components of the target <abbr>CRS</abbr> (if the latter is compound).
+ * If this condition does not hold, the position may be ignored (it will not be transformed).
*
- * @todo Non-public API for now, pending more feedback from experience. A public method would be non-static.
+ * @param coordinates coordinate values that may be considered as constant, or {@code null} if none.
+ * @since 1.6
*/
- static double[] getConstantCoordinates() throws TransformException {
- final Supplier<double[]> f = CoordinateOperations.CONSTANT_COORDINATES.get();
- if (f != null) try {
- return f.get();
- } catch (BackingStoreException e) {
- throw e.unwrapOrRethrow(TransformException.class);
- }
- return null;
+ public void setConstantCoordinates(final DirectPosition coordinates) {
+ constantCoordinates = coordinates;
}
/**
* Returns whether it is safe to use cached operation.
* This is {@code false} if the operation result may depend on external configuration.
*/
- static boolean canReadFromCache() {
- return CoordinateOperations.CONSTANT_COORDINATES.get() == null;
+ final boolean canReadFromCache() {
+ return getAreaOfInterest() == null && getDesiredAccuracy() == 0 && getConstantCoordinates() == null;
+ }
+
+ /**
+ * Returns the locale for error messages.
+ * By default, this is the <abbr>JVM</abbr> default locale.
+ * Note that this locale has no incidence on the selection of coordinate operations.
+ * In particular, this locale does not determine a geographic area.
+ *
+ * @return the locale for error messages.
+ * @since 1.6
+ */
+ @Override
+ public Locale getLocale() {
+ return (locale != null) ? locale : Locale.getDefault();
+ }
+
+ /**
+ * Sets the locale for error messages.
+ * Locale specified by this method are used on a best-effort basis only.
+ * Note that this locale has no incidence on the selection of coordinate operations.
+ * In particular, this locale does not determine a geographic area.
+ *
+ * @param locale the locale for error messages, or {@code null} for the <abbr>JVM</abbr> default locale.
+ * @since 1.6
+ */
+ public void setLocale(final Locale locale) {
+ this.locale = locale;
+ }
+
+ /**
+ * Returns a filter which will receive warnings, or {@code null} if none.
+ * If that filter returns {@code false} for a given log record, then that
+ * record will not be logged by {@link CoordinateOperationFinder}.
+ *
+ * @return a filter which will receive warnings, or {@code null} if none.
+ * @since 1.6
+ */
+ public Filter getLogFilter() {
+ return logFilter;
+ }
+
+ /**
+ * Specifies a filter which will receive warnings, or {@code null} if none.
+ * If that filter returns {@code false} for a given log record, then that
+ * record will not be logged by {@link CoordinateOperationFinder}.
+ *
+ * <h4>Usage example</h4>
+ * This filter can be used as a way to redirect the logs to something else,
+ * such as {@linkplain org.apache.sis.storage.event.StoreListener data store listeners}.
+ * In such case, the return value of the filter controls whether the record should be also
+ * logged with the usual loggers.
+ *
+ * @param logFilter a filter which will receive warnings, or {@code null} if none.
+ * @since 1.6
+ */
+ public void setLogFilter(final Filter logFilter) {
+ this.logFilter = logFilter;
+ }
+
+ /**
+ * Logs a record about an exception that we can ignore.
+ *
+ * @param method the method to declare as the source.
+ * @param e the exception to report.
+ */
+ private void recoverableException(final String method, final Exception e) {
+ final var record = new LogRecord(Level.FINE, e.getLocalizedMessage());
+ record.setThrown(e);
+ record.setSourceMethodName(method);
+ log(this, CoordinateOperationContext.class, AbstractCoordinateOperation.LOGGER, record);
+ }
+
+ /**
+ * Logs the given record. The logger name of the record is unconditionally set to the name of the given logger.
+ * Then, if the context is non-null and has a non-null log filter, the record is submitted to the filter.
+ * If the filter didn't returned {@code false}, the record is logged.
+ *
+ * @param context the context, or {@code null} if none.
+ * @param caller the class to report as the source of the log.
+ * @param logger the logger where to log the record.
+ * @param record the record to log.
+ */
+ static void log(CoordinateOperationContext context, Class<?> caller, Logger logger, LogRecord record) {
+ record.setSourceClassName(caller.getCanonicalName());
+ record.setLoggerName(logger.getName());
+ if (context != null) {
+ final Filter logFilter = context.getLogFilter();
+ if (logFilter != null && !logFilter.isLoggable(record)) {
+ return;
+ }
+ }
+ logger.log(record);
}
}
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index 2921d6c4e0..ca2d90fdd4 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
@@ -22,6 +22,7 @@ import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.ListIterator;
+import java.util.Locale;
import java.util.Optional;
import java.time.Duration;
import javax.measure.Unit;
@@ -39,6 +40,7 @@ import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.parameter.ParameterValueGroup;
import org.apache.sis.measure.Units;
import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.metadata.iso.extent.DefaultExtent;
import org.apache.sis.metadata.iso.extent.Extents;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.CommonCRS;
@@ -58,6 +60,7 @@ import org.apache.sis.referencing.datum.DatumOrEnsemble;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.matrix.NoninvertibleMatrixException;
+import org.apache.sis.referencing.operation.matrix.UnderdeterminedMatrixException;
import org.apache.sis.referencing.operation.provider.DatumShiftMethod;
import org.apache.sis.referencing.operation.provider.GeocentricAffine;
import org.apache.sis.util.Utilities;
@@ -136,7 +139,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
* The pair of source and target CRS for which we already searched a coordinate operation.
* This is used as a safety against infinite recursion.
*/
- private final Map<CRSPair,Boolean> previousSearches;
+ private final Map<CRSPair, Boolean> previousSearches;
/**
* Whether this finder instance is allowed to use {@link DefaultCoordinateOperationFactory#cache}.
@@ -162,13 +165,13 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
* @see DefaultCoordinateOperationFactory#createOperationFinder(CoordinateOperationAuthorityFactory, CoordinateOperationContext)
*/
public CoordinateOperationFinder(final CoordinateOperationAuthorityFactory registry,
- final CoordinateOperationFactory factory,
- final CoordinateOperationContext context) throws FactoryException
+ final CoordinateOperationFactory factory,
+ final CoordinateOperationContext context) throws FactoryException
{
super(registry, factory, context);
identifierOfStepCRS = new HashMap<>(8);
previousSearches = new HashMap<>(8);
- canReadFromCache = (context == null) && (factory == factorySIS) && CoordinateOperationContext.canReadFromCache();
+ canReadFromCache = (context == null || context.canReadFromCache()) && (factory == factorySIS);
canStoreInCache = (context == null);
}
@@ -242,9 +245,10 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, null,
CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(),
targetCRS.getCoordinateSystem())));
- } catch (IllegalArgumentException | IncommensurableException e) {
- final CRSPair key = new CRSPair(sourceCRS, targetCRS);
- throw new FactoryException(resources().getString(Resources.Keys.CanNotInstantiateGeodeticObject_1, key), e);
+ } catch (UnderdeterminedMatrixException exception) {
+ throw new MissingSourceDimensionsException(notFoundMessage(sourceCRS, targetCRS), exception);
+ } catch (IllegalArgumentException | IncommensurableException exception) {
+ throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS), exception);
}
/*
* If this method is invoked recursively, verify if the requested operation is already in the cache.
@@ -268,7 +272,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
if (bbox == null) {
bbox = Extents.intersection(CRS.getGeographicBoundingBox(sourceCRS),
CRS.getGeographicBoundingBox(targetCRS));
- areaOfInterest = CoordinateOperationContext.setGeographicBoundingBox(areaOfInterest, bbox);
+ areaOfInterest = CoordinateOperationContext.setGeographicBoundingBox(DefaultExtent.castOrCopy(areaOfInterest), bbox);
}
/*
* Verify in the EPSG dataset if the operation is explicitly defined by an authority.
@@ -752,6 +756,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
final Matrix matrix;
try {
matrix = CoordinateSystems.swapAndScaleAxes(sourceCS, targetCS);
+ } catch (UnderdeterminedMatrixException exception) {
+ throw new MissingSourceDimensionsException(notFoundMessage(sourceCRS, targetCRS), exception);
} catch (IllegalArgumentException | IncommensurableException exception) {
throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS), exception);
}
@@ -820,6 +826,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
final MatrixSIS matrix;
try {
matrix = MatrixSIS.castOrCopy(CoordinateSystems.swapAndScaleAxes(sourceCS, targetCS));
+ } catch (UnderdeterminedMatrixException exception) {
+ throw new MissingSourceDimensionsException(notFoundMessage(sourceCRS, targetCRS), exception);
} catch (IllegalArgumentException | IncommensurableException exception) {
throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS), exception);
}
@@ -873,6 +881,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
final Matrix matrix;
try {
matrix = CoordinateSystems.swapAndScaleAxes(sourceCS, targetCS);
+ } catch (UnderdeterminedMatrixException exception) {
+ throw new MissingSourceDimensionsException(notFoundMessage(sourceCRS, targetCRS), exception);
} catch (IllegalArgumentException | IncommensurableException exception) {
throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS), exception);
}
@@ -909,15 +919,12 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
*/
final SubOperationInfo[] infos;
try {
- infos = SubOperationInfo.createSteps(this, sourceComponents, targetComponents);
+ infos = SubOperationInfo.createSteps(this, sourceCRS, sourceComponents, targetComponents);
} catch (NoninvertibleTransformException e) {
throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS), e);
} catch (TransformException e) {
throw new FactoryException(notFoundMessage(sourceCRS, targetCRS), e);
}
- if (infos == null) {
- throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS));
- }
/*
* At this point, a coordinate operation has been found for all components of the target CRS.
* However, the CoordinateOperation.getSourceCRS() values are not necessarily in the same order
@@ -959,8 +966,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
for (int i=0; i<stepComponents.length; i++) {
final SubOperationInfo info = infos[i];
final CoordinateReferenceSystem source = stepComponents[i];
- final CoordinateReferenceSystem target = targetComponents.get(info.targetComponentIndex);
- canStoreInCache &= info.canStoreInCache;
+ final CoordinateReferenceSystem target = info.targetComponent;
+ canStoreInCache &= info.canStoreInCache();
/*
* In order to compute `stepTargetCRS`, replace in-place a single element in `stepComponents`.
* For each step except the last one, `stepTargetCRS` is a mix of target CRS and source CRS.
@@ -1008,16 +1015,14 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
* source dimensions, add those constants as the last step. It never occur except is some particular
* contexts like when computing a transform between two `GridGeometry` instances.
*/
- if (stepComponents.length < infos.length) try {
- final Matrix m = SubOperationInfo.createConstantOperation(infos,
+ if (stepComponents.length < infos.length) {
+ final Matrix m = SubOperationInfo.createConstantOperation(context, infos,
stepSourceCRS.getCoordinateSystem().getDimension(),
targetCRS.getCoordinateSystem().getDimension());
operation = concatenate(operation, createFromAffineTransform(CONSTANTS, stepSourceCRS, targetCRS, null, m));
for (int i = stepComponents.length; i < infos.length; i++) {
- canStoreInCache &= infos[i].canStoreInCache;
+ canStoreInCache &= infos[i].canStoreInCache();
}
- } catch (TransformException e) {
- throw new FactoryException(notFoundMessage(sourceCRS, targetCRS), e);
}
return asList(operation);
}
@@ -1182,7 +1187,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
if (isIdentity(step3)) return concatenate(step1, step2);
if (canHide(step1.getName())) return concatenate(concatenate(step1, step2), step3);
if (canHide(step3.getName())) return concatenate(step1, concatenate(step2, step3));
- final Map<String,?> properties = defaultName(step1.getSourceCRS(), step3.getTargetCRS());
+ final Map<String, ?> properties = defaultName(step1.getSourceCRS(), step3.getTargetCRS());
return factory.createConcatenatedOperation(properties, null, null, step1, step2, step3);
}
@@ -1223,7 +1228,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
/**
* Returns the given name in a singleton map.
*/
- private static Map<String,?> properties(final String name) {
+ private static Map<String, ?> properties(final String name) {
return Map.of(IdentifiedObject.NAME_KEY, name);
}
@@ -1233,7 +1238,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
* where "(step 1)" may be replaced by "(step 2)", "(step 3)", <i>etc.</i> if this
* method has already been invoked for the same identifier (directly or indirectly).
*/
- private Map<String,?> derivedFrom(final IdentifiedObject object) {
+ private Map<String, ?> derivedFrom(final IdentifiedObject object) {
Identifier oldID = object.getName();
Object p = identifierOfStepCRS.get(oldID);
if (p instanceof Identifier) {
@@ -1245,7 +1250,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
identifierOfStepCRS.put(newID, oldID);
identifierOfStepCRS.put(oldID, count);
- final Map<String,Object> properties = properties(newID);
+ final Map<String, Object> properties = properties(newID);
properties.put(IdentifiedObject.REMARKS_KEY, Vocabulary.formatInternational(Vocabulary.Keys.DerivedFrom_1, label(object)));
return properties;
}
@@ -1253,7 +1258,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
/**
* Returns a name for a transformation between two CRS.
*/
- private static Map<String,?> defaultName(CoordinateReferenceSystem source, CoordinateReferenceSystem target) {
+ private static Map<String, ?> defaultName(CoordinateReferenceSystem source, CoordinateReferenceSystem target) {
return properties(new CRSPair(source, target).toString());
}
@@ -1278,8 +1283,12 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
*
* @see #datumChangeNotFound(IdentifiedObject, IdentifiedObject)
*/
- private String notFoundMessage(final CoordinateReferenceSystem source, final CoordinateReferenceSystem target) {
- return resources().getString(Resources.Keys.CoordinateOperationNotFound_2, label(source), label(target));
+ final String notFoundMessage(final CoordinateReferenceSystem source, final CoordinateReferenceSystem target) {
+ final Locale locale = getLocale();
+ return Resources.forLocale(locale).getString(
+ Resources.Keys.CoordinateOperationNotFound_2,
+ CRSPair.label(source, locale),
+ CRSPair.label(target, locale));
}
/**
@@ -1290,7 +1299,10 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
* @return a default error message.
*/
private String canNotInvert(final DerivedCRS crs) {
- return resources().getString(Resources.Keys.NonInvertibleOperation_1, label(crs.getConversionFromBase()));
+ final Locale locale = getLocale();
+ return Resources.forLocale(locale).getString(
+ Resources.Keys.NonInvertibleOperation_1,
+ CRSPair.label(crs.getConversionFromBase(), locale));
}
/**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index af31347efd..bb4f667fdc 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
@@ -75,7 +75,6 @@ import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.Deprecable;
-import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.resources.Vocabulary;
@@ -212,6 +211,14 @@ class CoordinateOperationRegistry {
*/
final DefaultCoordinateOperationFactory factorySIS;
+ /**
+ * Options such as initial area of interest, desired accuracy and coordinates that may be considered as constants.
+ * This is null if no context has been specified.
+ *
+ * @since 1.6
+ */
+ protected final CoordinateOperationContext context;
+
/**
* The spatiotemporal area of interest, or {@code null} if none.
* When a new {@code CoordinateOperationFinder} instance is created with a non-null
@@ -239,14 +246,6 @@ class CoordinateOperationRegistry {
*/
boolean stopAtFirst;
- /**
- * A filter that can be used for applying additional restrictions on the coordinate operation,
- * or {@code null} if none. If non-null, only operations passing this filter will be considered.
- *
- * @see CoordinateOperationContext#getOperationFilter()
- */
- private Predicate<CoordinateOperation> filter;
-
/**
* Authority codes found for CRS. This is a cache for {@link #findCode(CoordinateReferenceSystem)}.
* This map may be non-empty only if {@link #codeFinder} is non-null.
@@ -262,13 +261,6 @@ class CoordinateOperationRegistry {
*/
private final Map<CoordinateReferenceSystem, List<String>> authorityCodes;
- /**
- * The locale for error messages.
- *
- * @todo Add a setter method, or make configurable in some way.
- */
- private Locale locale;
-
/**
* Creates a new instance for the given factory and context.
*
@@ -302,10 +294,10 @@ class CoordinateOperationRegistry {
}
}
this.authorityCodes = authorityCodes;
+ this.context = context;
if (context != null) {
areaOfInterest = context.getAreaOfInterest();
desiredAccuracy = context.getDesiredAccuracy();
- filter = context.getOperationFilter();
}
}
@@ -701,7 +693,7 @@ class CoordinateOperationRegistry {
continue; // Try again with the next best case.
}
/*
- * It is possible that the CRS given to this method were not quite right. For example, the user
+ * It is possible that the CRSs given to this method were not quite right. For example, the user
* may have created his CRS from a WKT using a different axis order than the order specified by
* the authority and still (wrongly) call those CRS "EPSG:xxxx". So we check if the source and
* target CRS for the operation we just created are equivalent to the CRS specified by the user.
@@ -713,6 +705,7 @@ class CoordinateOperationRegistry {
* FactoryException propagate.
*/
operation = complete(operation, sourceCRS, targetCRS);
+ final Predicate<CoordinateOperation> filter = (context != null) ? context.getOperationFilter() : null;
if (filter == null || filter.test(operation)) {
if (stopAtFirst) {
operations.clear();
@@ -754,7 +747,7 @@ class CoordinateOperationRegistry {
final CoordinateReferenceSystem sourceCRS = op.getSourceCRS();
final CoordinateReferenceSystem targetCRS = op.getTargetCRS();
final MathTransform transform = op.getMathTransform().inverse();
- final OperationMethod method = InverseOperationMethod.create(op.getMethod(), factorySIS);
+ final OperationMethod method = InverseOperationMethod.create(op.getMethod(), this);
final Map<String,Object> properties = properties(INVERSE_OPERATION);
InverseOperationMethod.properties(op, properties);
inverse = createFromMathTransform(properties, targetCRS, sourceCRS, transform, method, null, typeOf(op));
@@ -1388,13 +1381,22 @@ class CoordinateOperationRegistry {
return null;
}
+ /**
+ * Returns the locale to use for error messages.
+ *
+ * @return the locale, or {@code null} for the default locale.
+ */
+ final Locale getLocale() {
+ return (context != null) ? context.getLocale() : null;
+ }
+
/**
* Returns the localized resources for error messages.
*
* @return localized resources for errors.
*/
final Resources resources() {
- return Resources.forLocale(locale);
+ return Resources.forLocale(getLocale());
}
/**
@@ -1404,7 +1406,7 @@ class CoordinateOperationRegistry {
* @return label identifying the given object.
*/
final String label(final IdentifiedObject object) {
- return CRSPair.label(object, locale);
+ return CRSPair.label(object, getLocale());
}
/**
@@ -1416,45 +1418,51 @@ class CoordinateOperationRegistry {
* @return a default error message.
*/
final String datumChangeNotFound(final IdentifiedObject source, final IdentifiedObject target) {
- return resources().getString(Resources.Keys.DatumChangeNotFound_2,
+ final Locale locale = getLocale();
+ return Resources.forLocale(locale).getString(Resources.Keys.DatumChangeNotFound_2,
IdentifiedObjects.getDisplayName(source, locale),
IdentifiedObjects.getDisplayName(target, locale));
}
/**
- * Logs an unexpected but ignorable exception. This method pretends that the logging
- * come from {@link CoordinateOperationFinder} since this is the public API which
- * use this {@code CoordinateOperationRegistry} class.
+ * Logs an unexpected but ignorable exception. This method pretends that the logging come from
+ * some {@link CoordinateOperationFinder} method because this is the public <abbr>API</abbr>
+ * which uses this {@code CoordinateOperationRegistry} class.
*
* @param record the record to log, or {@code null} for creating from the exception.
- * @param exception the exception which occurred, or {@code null} if a {@code record} is specified instead.
+ * @param exception the exception that occurred, or {@code null} if a {@code record} is specified instead.
*/
- private static void log(LogRecord record, final Exception exception) {
+ private void log(LogRecord record, final Exception exception) {
if (record == null) {
record = new LogRecord(Level.WARNING, exception.getLocalizedMessage());
+ /*
+ * We usually do not log the stack trace because this method is invoked mostly for exceptions
+ * such as `NoSuchAuthorityCodeException` or `MissingFactoryResourceException`, for which the
+ * message is descriptive enough. But we make a special case for `NoninvertibleTransformException`
+ * because its cause may have deeper root.
+ */
+ if (exception instanceof NoninvertibleTransformException) {
+ record.setThrown(exception);
+ }
}
- /*
- * We usually do not log the stack trace because this method should be invoked only for exceptions
- * such as NoSuchAuthorityCodeException or MissingFactoryResourceException, for which the message
- * is descriptive enough. But we make a special case for NoninvertibleTransformException because
- * its cause may have deeper root.
- */
- if (exception instanceof NoninvertibleTransformException) {
- record.setThrown(exception);
+ if (record.getSourceMethodName() == null) {
+ record.setSourceMethodName("createOperations");
}
- Logging.completeAndLog(AbstractCoordinateOperation.LOGGER, CoordinateOperationFinder.class, "createOperations", record);
+ CoordinateOperationContext.log(context, CoordinateOperationFinder.class, AbstractCoordinateOperation.LOGGER, record);
}
/**
* Logs an ignorable exception. This method pretends that the logging come from
- * {@link CoordinateOperationFinder} since this is the public API which use this
- * {@code CoordinateOperationRegistry} class.
+ * {@link CoordinateOperationFinder} because this is the public <abbr>API</abbr>
+ * which uses this {@code CoordinateOperationRegistry} class.
*
* @param method the method name where the error occurred.
- * @param exception the exception which occurred, or {@code null} if a {@code record} is specified instead.
+ * @param exception the exception that occurred.
*/
- static void recoverableException(final String method, final Exception exception) {
- Logging.recoverableException(AbstractCoordinateOperation.LOGGER,
- CoordinateOperationFinder.class, method, exception);
+ final void recoverableException(final String method, final Exception exception) {
+ final var record = new LogRecord(Level.FINE, exception.getLocalizedMessage());
+ record.setThrown(exception);
+ record.setSourceMethodName(method);
+ log(record, null);
}
}
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java
index 50a5a0d9c0..e71d98b793 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java
@@ -76,7 +76,7 @@ final class InverseOperationMethod extends DefaultOperationMethod {
* for the inverse operation either with the exact same parameter values or with the sign of some values
* reversed, then the given method is returned as-is. Otherwise a synthetic method is created.
*/
- static OperationMethod create(OperationMethod method, final DefaultCoordinateOperationFactory factorySIS)
+ static OperationMethod create(OperationMethod method, final CoordinateOperationRegistry caller)
throws FactoryException
{
if (method instanceof InverseOperationMethod) {
@@ -84,10 +84,10 @@ final class InverseOperationMethod extends DefaultOperationMethod {
}
if (!(method instanceof AbstractProvider)) try {
method = CoordinateOperations.findMethod(
- factorySIS.getMathTransformFactory(),
+ caller.factorySIS.getMathTransformFactory(),
method.getName().getCode());
} catch (NoSuchIdentifierException e) {
- CoordinateOperationRegistry.recoverableException("inverse", e);
+ caller.recoverableException("inverse", e);
}
if (method instanceof AbstractProvider) {
final AbstractProvider inverse = ((AbstractProvider) method).inverse();
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MismatchedDatumException.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MismatchedDatumException.java
index 6e8666c6e6..a4bb586cb0 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MismatchedDatumException.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MismatchedDatumException.java
@@ -18,19 +18,19 @@ package org.apache.sis.referencing.operation;
/**
- * Thrown when the source and target CRS of a {@linkplain DefaultConversion conversion} use different datum.
- * By definition, conversions do not perform any change of datum
+ * Thrown when the source and target <abbr>CRS</abbr> of a coordinate conversion use different datum.
+ * By definition, {@linkplain DefaultConversion conversions} do not perform any change of datum
* while {@linkplain DefaultTransformation transformations} can do.
*
* <h2>Usage note</h2>
- * SIS is tolerant about different datum at {@code DefaultConversion} construction time,
+ * Apache <abbr>SIS</abbr> is tolerant about different datum at {@code DefaultConversion} construction time,
* for the reasons explained in {@linkplain DefaultConversion#DefaultConversion(java.util.Map,
* org.opengis.referencing.crs.CoordinateReferenceSystem,
* org.opengis.referencing.crs.CoordinateReferenceSystem,
* org.opengis.referencing.crs.CoordinateReferenceSystem,
* org.opengis.referencing.operation.OperationMethod,
* org.opengis.referencing.operation.MathTransform) its constructor}.
- * However, SIS is stricter at {@linkplain org.apache.sis.referencing.crs.DefaultDerivedCRS Derived CRS}
+ * However, <abbr>SIS</abbr> is stricter at {@linkplain org.apache.sis.referencing.crs.DefaultDerivedCRS Derived CRS}
* construction time.
*
* @author Martin Desruisseaux (Geomatys)
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MissingSourceDimensionsException.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MissingSourceDimensionsException.java
new file mode 100644
index 0000000000..690f82ecc0
--- /dev/null
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MissingSourceDimensionsException.java
@@ -0,0 +1,121 @@
+/*
+ * 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;
+
+import java.util.Set;
+import java.util.Collections;
+import org.apache.sis.referencing.internal.shared.AxisDirections;
+import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.operation.OperationNotFoundException;
+import org.apache.sis.referencing.operation.matrix.UnderdeterminedMatrixException;
+import org.apache.sis.util.collection.CodeListSet;
+
+
+/**
+ * Thrown when a coordinate operation cannot be find because some dimensions are missing in the source <abbr>CRS</abbr>.
+ * The missing dimensions are identified by the directions of the missing axes. For example, if the coordinate system of
+ * the source <abbr>CRS</abbr> does not have a temporal axis which was requested by the target <abbr>CRS</abbr>,
+ * then the {@link #getMissingAxes()} should contain {@link AxisDirection#FUTURE} or {@link AxisDirection#PAST}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.6
+ * @since 1.6
+ */
+public class MissingSourceDimensionsException extends OperationNotFoundException {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = 5228802326932593847L;
+
+ /**
+ * The unknown axes identified by their directions, created when first needed.
+ */
+ private CodeListSet<AxisDirection> axes;
+
+ /**
+ * Constructs a new exception with no message.
+ */
+ public MissingSourceDimensionsException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message the detail message, or {@code null} if none.
+ */
+ public MissingSourceDimensionsException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * @param message the detail message, or {@code null} if none.
+ * @param cause the cause, or {@code null} if none.
+ */
+ public MissingSourceDimensionsException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Adds all axis directions of the target coordinate system
+ * that are not found in the source coordinate system.
+ */
+ final void addMissing(final CoordinateSystem source, final CoordinateSystem target) {
+ for (int i = target.getDimension(); --i >= 0;) {
+ AxisDirection direction = target.getAxis(i).getDirection();
+ if (AxisDirections.indexOfColinear(source, direction) < 0) {
+ addMissing(direction);
+ }
+ }
+ }
+
+ /**
+ * Indicates which dimension was missing, specified as an axis direction.
+ *
+ * @param direction direction of the missing axis.
+ */
+ public void addMissing(final AxisDirection direction) {
+ if (axes == null) {
+ axes = new CodeListSet<>(AxisDirection.class);
+ final Throwable cause = getCause();
+ if (cause instanceof UnderdeterminedMatrixException) {
+ axes.addAll(((UnderdeterminedMatrixException) cause).getUnknownAxes());
+ }
+ }
+ axes.add(direction);
+ }
+
+ /**
+ * Indices which axes were missing in the source coordinate system.
+ * The returned set does not need to be exhaustive and may be empty if this information is not available.
+ *
+ * @return directions of axes that are missing in the source coordinate system.
+ */
+ public Set<AxisDirection> getMissingAxes() {
+ if (axes != null) {
+ return Collections.unmodifiableSet(axes);
+ }
+ final Throwable cause = getCause();
+ if (cause instanceof UnderdeterminedMatrixException) {
+ return ((UnderdeterminedMatrixException) cause).getUnknownAxes();
+ }
+ return Set.of();
+ }
+}
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
index 17a604bd58..153c5c23d0 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
@@ -18,12 +18,15 @@ package org.apache.sis.referencing.operation;
import java.util.List;
import org.opengis.referencing.crs.*;
+import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.OperationNotFoundException;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.operation.TransformException;
+import org.opengis.geometry.DirectPosition;
import org.opengis.util.FactoryException;
+import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.internal.Resources;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
@@ -79,31 +82,28 @@ final class SubOperationInfo {
}
/**
- * The coordinate operation between a source CRS component and a target CRS component.
- * Exactly one of {@link #operation} or {@link #constants} shall be non-null.
+ * The coordinate operation between a component of the source CRS and a component of the target CRS.
+ * If {@code null}, then {@link CoordinateOperationContext#getConstantCoordinates()} shall return a
+ * non-null value, otherwise a {@link MissingSourceDimensionsException} will be thrown.
*/
final CoordinateOperation operation;
/**
- * The constant values to store in target coordinates, or {@code null} if none. This array is usually null.
- * It may be non-null if no source CRS component has been found for a target CRS component.
- * Exactly one of {@link #operation} or {@link #constants} shall be non-null.
+ * The constant values to store in target coordinates, or {@code null} if none.
+ * This property is usually null, but may be non-null if no component of the source <abbr>CRS</abbr>
+ * has been found for a component of the target <abbr>CRS</abbr>, and fallbacks were specified with
+ * {@link CoordinateOperationContext#getConstantCoordinates()}.
*
- * <p>The array length is the number of dimensions in the full (usually compound) target CRS, but only the
- * coordinate values for sourceless dimensions are used. Other coordinates are ignored and can be NaN.</p>
- *
- * <h4>Example</h4>
- * If the source <abbr>CRS</abbr> has (<var>x</var>, <var>y</var>) axes and the target <abbr>CRS</abbr>
- * has (<var>x</var>, <var>y</var>, <var>t</var>) axes, then this array may be set to a non-null value
- * for specifying the <var>t</var> value.
+ * <p>The array length is the number of dimensions in the <em>component</em> of the target <abbr>CRS</abbr>
+ * for which this {@code SubOperationInfo} is constructed. These coordinates cannot be <abbr>NaN</abbr>.</p>
*
* @see CoordinateOperationContext#getConstantCoordinates()
*/
- private final double[] constants;
+ private double[] constantCoordinates;
/**
- * The first dimension (inclusive) and the last dimension (exclusive) where the {@link SingleCRS} starts/ends
- * in the full (usually compound) CRS.
+ * The first dimension (inclusive) and the last dimension (exclusive) where the
+ * {@link SingleCRS} starts/ends in the full (usually compound) <abbr>CRS</abbr>.
*
* @see #sourceToSelected(int, SubOperationInfo[])
*/
@@ -117,45 +117,48 @@ final class SubOperationInfo {
final int targetComponentIndex;
/**
- * Whether this operation can be cached. This flag shall be {@code false} if
- * the operation depends on parameters that may vary between two executions.
+ * The component of the target <abbr>CRS</abbr> which is managed by this {@code SubOperationInfo}.
+ * This is the component at the index specified by {@link #targetComponentIndex}.
*/
- final boolean canStoreInCache;
+ final SingleCRS targetComponent;
/**
- * Creates a new instance wrapping the given coordinate operation or coordinate constants.
- * Exactly one of {@code operation} or {@code constants} shall be non-null.
+ * Creates a new instance wrapping the given coordinate operation or constant coordinates.
+ * The constant coordinates are fetched after construction if they appear needed.
+ *
+ * @see #fetchConstantsForMissingSourceDimensions(CoordinateOperationContext)
*/
- private SubOperationInfo(final CoordinateOperation operation, final double[] constants,
+ private SubOperationInfo(final CoordinateOperation operation,
final int sourceLowerDimension, final int sourceUpperDimension,
final int targetLowerDimension, final int targetUpperDimension,
- final int targetComponentIndex, final boolean canStoreInCache)
+ final int targetComponentIndex, final SingleCRS targetComponent)
{
this.operation = operation;
- this.constants = constants;
this.sourceLowerDimension = sourceLowerDimension;
this.sourceUpperDimension = sourceUpperDimension;
this.targetLowerDimension = targetLowerDimension;
this.targetUpperDimension = targetUpperDimension;
this.targetComponentIndex = targetComponentIndex;
- this.canStoreInCache = canStoreInCache;
- assert (operation == null) != (constants == null);
+ this.targetComponent = targetComponent;
}
/**
* Searches in given list of source components for operations capable to transform coordinates to each target CRS.
- * There is one {@code SubOperationInfo} per target CRS because we need to satisfy all target dimensions, while it
- * is okay to ignore some source dimensions. If an operation cannot be found, then this method returns {@code null}.
+ * There is one {@code SubOperationInfo} instance per target <abbr>CRS</abbr> because we need to satisfy all target
+ * dimensions. However, it is okay to ignore some components or dimensions of the source <abbr>CRS</abbr>.
*
- * @param caller the object which is inferring a coordinate operation.
- * @param sources all components of the source CRS.
- * @param targets all components of the target CRS.
- * @return information about each coordinate operation from a source CRS to a target CRS, or {@code null}.
+ * @param caller the object which is inferring a coordinate operation.
+ * @param sourceCRS the source source <abbr>CRS</abbr>, used for error message.
+ * @param sources all components of the source <abbr>CRS</abbr>.
+ * @param targets all components of the target <abbr>CRS</abbr>.
+ * @return information about each coordinate operation from a source <abbr>CRS</abbr> to a target <abbr>CRS</abbr>.
* @throws FactoryException if an error occurred while grabbing a coordinate operation.
+ * @throws MissingSourceDimensionsException if a target axis cannot be mapped to a source axis or a constant.
* @throws TransformException if an error occurred while computing a coordinate value for unmatched dimension.
* @throws NoninvertibleTransformException if the default coordinate value for a unmatched dimension is NaN.
*/
static SubOperationInfo[] createSteps(final CoordinateOperationFinder caller,
+ final CoordinateReferenceSystem sourceCRS,
final List<? extends SingleCRS> sources,
final List<? extends SingleCRS> targets)
throws FactoryException, TransformException
@@ -163,108 +166,137 @@ final class SubOperationInfo {
final var infos = new SubOperationInfo[targets.size()];
final var sourceComponentIsUsed = new boolean[sources.size()];
/*
- * Iterate over target CRS because all of them must have an operation.
- * One the other hand, source CRS can be used zero or one (not two) time.
+ * Each target CRS must be associated to exactly one source CRS
+ * (or to zero source CRS if the missing source CRS can be replaced by constant coordinate values).
+ * Each source CRS must be associated to zero or one target CRS, not necessarily in the same order.
+ * Therefore, the iteration over source components will be repeated for each target CRS, combined
+ * with `sourceComponentIsUsed` flags for using each source CRS at most once.
*/
int targetLowerDimension;
int targetUpperDimension = 0;
-next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length; targetComponentIndex++) {
- final SingleCRS target = targets.get(targetComponentIndex);
+ int targetComponentIndex = 0;
+ while (targetComponentIndex < infos.length) {
+ final SingleCRS targetComponent = targets.get(targetComponentIndex);
targetLowerDimension = targetUpperDimension;
- targetUpperDimension += target.getCoordinateSystem().getDimension();
+ targetUpperDimension += targetComponent.getCoordinateSystem().getDimension();
- final Class<?> targetType = type(target);
+ final Class<?> targetType = type(targetComponent);
OperationNotFoundException failure = null;
+ CoordinateOperation operation = null;
/*
* For each target CRS, search for a source CRS which has not yet been used.
* Some sources may be left unused after this method completion. Check only
* sources that may be compatible according the `COMPATIBLE_TYPES` array.
*/
- int sourceLowerDimension;
+ int sourceLowerDimension = 0;
int sourceUpperDimension = 0;
- for (int sourceComponentIndex = 0; sourceComponentIndex < sourceComponentIsUsed.length; sourceComponentIndex++) {
- final SingleCRS source = sources.get(sourceComponentIndex);
- sourceLowerDimension = sourceUpperDimension;
- sourceUpperDimension += source.getCoordinateSystem().getDimension();
+ int sourceComponentIndex = 0;
+searchSrc: while (sourceComponentIndex < sourceComponentIsUsed.length) {
+ final SingleCRS sourceComponent = sources.get(sourceComponentIndex);
+ sourceUpperDimension += sourceComponent.getCoordinateSystem().getDimension();
if (!sourceComponentIsUsed[sourceComponentIndex]) {
- final Class<?> sourceType = type(source);
+ final Class<?> sourceType = type(sourceComponent);
for (final Class<?>[] compatibleTypes : COMPATIBLE_TYPES) {
if (compatibleTypes[0].isAssignableFrom(targetType)) {
for (final Class<?> compatibleType : compatibleTypes) {
- if (compatibleType.isAssignableFrom(sourceType)) {
- final CoordinateOperation operation;
- try {
- operation = caller.createOperation(source, target);
- } catch (OperationNotFoundException exception) {
- if (failure == null) {
- failure = exception;
- } else {
- failure.addSuppressed(exception);
- }
- continue;
- }
- /*
- * Found an operation. Exclude the source component from the list because each source
- * should be used at most once by `SubOperationInfo`. Note that the same source may
- * be used again in another context if that source is also an interpolation CRS.
- *
- * EXAMPLE: consider a coordinate operation from (GeodeticCRS₁, VerticalCRS₁) source
- * to (GeodeticCRS₂, VerticalCRS₂) target. The source GeodeticCRS₁ should be mapped
- * to exactly one target component (which is GeodeticCRS₂) and VerticalCRS₁ mapped
- * to VerticalCRS₂. But the operation on vertical coordinates may need GeodeticCRS₁
- * for doing its work, so GeodeticCRS₁ is needed twice. However, when needed for the
- * vertical coordinate operation, the GeodeticCRS₁ is used as an "interpolation CRS".
- * Interpolation CRS are handled in other code paths, it is not the business of this
- * `SubOperationInfo` class to care about them. From the point of view of this class,
- * GeodeticCRS₁ is used only once.
- */
+ if (compatibleType.isAssignableFrom(sourceType)) try {
+ operation = caller.createOperation(sourceComponent, targetComponent);
sourceComponentIsUsed[sourceComponentIndex] = true;
- infos[targetComponentIndex] = new SubOperationInfo(
- operation, null,
- sourceLowerDimension, sourceUpperDimension,
- targetLowerDimension, targetUpperDimension,
- targetComponentIndex, true);
-
- if (failure != null) {
- CoordinateOperationRegistry.recoverableException("decompose", failure);
+ break searchSrc;
+ } catch (OperationNotFoundException exception) {
+ if (failure == null) {
+ failure = exception;
+ } else {
+ failure.addSuppressed(exception);
}
- continue next;
}
}
}
}
}
+ // Following variable must be updated before to stop the loop. Therefore, it cannot be at loop beginning.
+ sourceLowerDimension = sourceUpperDimension;
+ sourceComponentIndex++;
}
if (failure != null) {
- throw failure;
+ if (operation == null) {
+ throw failure;
+ }
+ caller.recoverableException("createOperationStep", failure);
}
+ final var info = new SubOperationInfo(operation,
+ sourceLowerDimension, sourceUpperDimension,
+ targetLowerDimension, targetUpperDimension,
+ targetComponentIndex, targetComponent);
/*
- * If we reach this point, we have not been able to find a source CRS that we can map to the target CRS.
- * Usually this is fatal, returning null will instruct the caller to throw `OperationNotFoundException`.
+ * If we enter in the following block, we have not been able to find a source for the target component.
+ * Usually, we don't know how to get coordinate values, so `MissingSourceDimensionsException` is thrown.
* However, in some contexts (e.g. when searching for an operation between two `GridGeometry` instances)
* it is possible to assign a constant value to the target coordinates. Those values cannot be guessed
- * by `org.apache.sis.referencing`; they must be provided by caller. If such constants are specified,
- * then we will try to apply them.
+ * by `org.apache.sis.referencing`, they must be provided by the user.
*/
- final double[] constants = CoordinateOperationContext.getConstantCoordinates();
- if (constants == null || constants.length < targetUpperDimension) {
- return null;
- }
- for (int i = targetLowerDimension; i < targetUpperDimension; i++) {
- if (Double.isNaN(constants[i])) {
- throw new NoninvertibleTransformException(caller.resources()
- .getString(Resources.Keys.ConstantCoordinateValueRequired_1, i));
+ if (operation == null) {
+ final CoordinateSystemAxis missing = info.fetchConstantsForMissingSourceDimensions(caller.context);
+ if (info.constantCoordinates == null) {
+ final MissingSourceDimensionsException e;
+ if (missing != null) {
+ e = new MissingSourceDimensionsException(caller.resources().getString(
+ Resources.Keys.ConstantCoordinateValueRequired_1, caller.label(missing)));
+ e.addMissing(missing.getDirection());
+ } else {
+ e = new MissingSourceDimensionsException(caller.notFoundMessage(sourceCRS, targetComponent));
+ e.addMissing(sourceCRS.getCoordinateSystem(), targetComponent.getCoordinateSystem());
+ }
+ throw e;
}
}
- infos[targetComponentIndex] = new SubOperationInfo(
- null, constants,
- sourceUpperDimension, sourceUpperDimension,
- targetLowerDimension, targetUpperDimension,
- targetComponentIndex, false);
+ infos[targetComponentIndex++] = info;
}
return infos;
}
+ /**
+ * Initializes the {@link #constantCoordinates} field to the constant coordinate values for the operation step
+ * managed by this {@code SubOperationInfo}. If no constants have been specified, {@link #constantCoordinates}
+ * field stay null and the reason for the failure is returned. That reason is an axis missing in the source
+ * <abbr>CRS</abbr>, or {@code null} if that axis is unknown.
+ *
+ * @param context options supplied by the user, or {@code null}.
+ * @return the coordinate axis that could not be resolved, or {@code null} if none or unknown.
+ */
+ private CoordinateSystemAxis fetchConstantsForMissingSourceDimensions(final CoordinateOperationContext context) {
+ if (context != null) {
+ final DirectPosition coordinates = context.getConstantCoordinates();
+ if (coordinates != null) {
+ /*
+ * Finds the index of the first coordinate to use among the constant coordinates.
+ * The default CRS of `coordinates` is the full target CRS (with all dimensions).
+ * If a different CRS is specified, search the index of this target CRS component.
+ * No coordinate transformation is perfomed in this method, only selection.
+ */
+ int indexOfConstant = targetLowerDimension; // Value for the default CRS.
+ final CoordinateReferenceSystem crs = coordinates.getCoordinateReferenceSystem();
+locate: if (crs != null) {
+ indexOfConstant = 0;
+ for (SingleCRS component : CRS.getSingleComponents(crs)) {
+ if (CRS.equivalent(targetComponent, component)) break locate;
+ indexOfConstant += component.getCoordinateSystem().getDimension();
+ }
+ return null;
+ }
+ final int d = coordinates.getDimension();
+ final var c = new double[targetUpperDimension - targetLowerDimension];
+ for (int i=0; i<c.length; i++) {
+ if (indexOfConstant >= d || Double.isNaN(c[i] = coordinates.getCoordinate(indexOfConstant++))) {
+ return targetComponent.getCoordinateSystem().getAxis(i);
+ }
+ }
+ constantCoordinates = c;
+ }
+ }
+ return null;
+ }
+
/**
* Returns the source CRS of given operations. This method modifies the given array in-place by moving all
* sourceless operations last. Then an array is returned with the source CRS of only ordinary operations.
@@ -375,59 +407,84 @@ next: for (int targetComponentIndex = 0; targetComponentIndex < infos.length;
/**
* Returns the matrix of an operation setting some coordinates to constant values.
+ * This is possible only if the user as provided a non-null value for
+ * {@link CoordinateOperationContext#getConstantCoordinates()}.
*
+ * @param context options supplied by the user, or {@code null}.
* @param selected all operations from source to target {@link CompoundCRS}.
* @param srcDim number of dimensions in the target CRS of previous operation step.
* @param tgtDim number of dimensions in the full (usually compound) target CRS.
*/
- static MatrixSIS createConstantOperation(final SubOperationInfo[] selected, final int srcDim, final int tgtDim)
- throws TransformException
+ static MatrixSIS createConstantOperation(final CoordinateOperationContext context,
+ final SubOperationInfo[] selected, final int srcDim, final int tgtDim)
+ throws MissingSourceDimensionsException
{
- final boolean[] targetDimensionIsUsed = new boolean[tgtDim];
- final MatrixSIS m = Matrices.createZero(tgtDim + 1, srcDim + 1);
- m.setElement(tgtDim, srcDim, 1);
+ final MatrixSIS matrix = Matrices.createZero(tgtDim + 1, srcDim + 1);
+ matrix.setElement(tgtDim, srcDim, 1);
for (final SubOperationInfo component : selected) {
- if (component.constants != null) {
- /*
- * Component for which no operation has been found.
- * Set all coordinate values of that component to the specified constants
- */
- for (int j = component.targetLowerDimension; j < component.targetUpperDimension; j++) {
- m.setElement(j, srcDim, component.constants[j]);
- targetDimensionIsUsed[j] = true;
- }
- } else {
- /*
- * Component for which an operation has been found, but maybe this is the inverse of the
- * "Geographic 3D to 2D conversion" (EPSG:9659) which sets the height to `DEFAULT_HEIGHT`.
- * If all values in the row are zero, then the coordinate value is unconditionally zero.
- * Replace that value (usually the default ellipsoidal height) by the specified constant.
- */
- final Matrix last = MathTransforms.getMatrix(MathTransforms.getLastStep(component.operation.getMathTransform()));
- if (last != null) {
+ component.setConstantTerms(context, matrix, srcDim);
+ }
+ return matrix;
+ }
+
+ /**
+ * Sets the coefficients of the given matrix to the constant coordinate values
+ * in the dimensions managed by this {@code SubOperationInfo}.
+ *
+ * @param context options supplied by the user, or {@code null}.
+ * @param matrix the matrix for which to set the coefficients.
+ * @param translationColumn index of the last column of the given matrix.
+ */
+ private void setConstantTerms(final CoordinateOperationContext context, final MatrixSIS matrix, final int translationColumn) {
+ if (constantCoordinates != null) {
+ /*
+ * Component for which no operation has been found.
+ * Set all coordinate values of that component to the specified constant values.
+ */
+ for (int j = targetUpperDimension - targetLowerDimension; --j >= 0;) {
+ matrix.setElement(targetLowerDimension + j, translationColumn, constantCoordinates[j]);
+ }
+ } else {
+ /*
+ * Component for which an operation has been found, but maybe this is the inverse of the
+ * "Geographic 3D to 2D conversion" (EPSG:9659) which sets the height to `DEFAULT_HEIGHT`.
+ * If all values in the row are zero, then the coordinate value is unconditionally zero.
+ * Replace that value (usually the default ellipsoidal height) by the specified constant.
+ */
+ final var targetDimensionIsUsed = new boolean[targetUpperDimension - targetLowerDimension];
+ final Matrix last = MathTransforms.getMatrix(MathTransforms.getLastStep(operation.getMathTransform()));
+ if (last != null) {
+ fetchConstantsForMissingSourceDimensions(context);
+ if (constantCoordinates != null) {
otherRow: for (int j = last.getNumRow() - 1; --j >= 0;) { // Ignore the last row.
for (int i = last.getNumCol(); --i >= 0;) {
if (last.getElement(j, i) != 0) {
continue otherRow;
}
}
- final double[] constants = CoordinateOperationContext.getConstantCoordinates();
- if (constants == null) break;
- final int k = component.targetLowerDimension + j;
- m.setElement(j, srcDim, constants[k]);
- targetDimensionIsUsed[k] = true;
+ matrix.setElement(targetLowerDimension + j, translationColumn, constantCoordinates[j]);
+ targetDimensionIsUsed[j] = true;
}
}
}
- }
- /*
- * All coordinates that have not been set to a constant shall be propagated unchanged (scale factor of 1).
- */
- for (int i=0,j=0; j<tgtDim; j++) {
- if (!targetDimensionIsUsed[j]) {
- m.setElement(j, i++, 1);
+ /*
+ * All coordinates that have not been set to a constant
+ * shall be propagated unchanged (scale factor of 1).
+ */
+ int i = sourceLowerDimension;
+ for (int j = 0; j < targetDimensionIsUsed.length; j++) {
+ if (!targetDimensionIsUsed[j]) {
+ matrix.setElement(targetLowerDimension + j, i++, 1);
+ }
}
}
- return m;
+ }
+
+ /**
+ * Whether this operation can be cached. This flag shall be {@code false} if
+ * the operation depends on parameters that may vary between two executions.
+ */
+ final boolean canStoreInCache() {
+ return constantCoordinates == null;
}
}
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/TransformedCoordinateSet.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/TransformedCoordinateSet.java
index e51b2228f8..eca20504d4 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/TransformedCoordinateSet.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/TransformedCoordinateSet.java
@@ -23,7 +23,6 @@ import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import org.opengis.util.FactoryException;
import org.opengis.geometry.DirectPosition;
-import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.CRS;
@@ -83,9 +82,9 @@ final class TransformedCoordinateSet extends AbstractCoordinateSet implements Un
}
final CoordinateMetadata metadata = data.getCoordinateMetadata();
if (metadata != null) try {
- GeographicBoundingBox aoi = CRS.getGeographicBoundingBox(op);
+ final var context = CoordinateOperationContext.fromBoundingBox(CRS.getGeographicBoundingBox(op));
final var step = new DefaultCoordinateMetadata(op.getSourceCRS(), op.getSourceEpoch().orElse(null));
- transform = MathTransforms.concatenate(CRS.findOperation(metadata, step, aoi).getMathTransform(), transform);
+ transform = MathTransforms.concatenate(CRS.findOperation(metadata, step, context).getMathTransform(), transform);
} catch (FactoryException | MismatchedDimensionException e) {
throw new TransformException(e.getMessage(), e);
}
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
index 4665b95ef7..66bed3e256 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
@@ -272,6 +272,7 @@ public final class Matrices {
* (NORTH,EAST) to (EAST,NORTH)), then coordinates at index {@code srcIndex} will have
* to be moved at index {@code dstIndex}.
*/
+ UnderdeterminedMatrixException exception = null;
for (int dstIndex = 0; dstIndex < dstAxes.length; dstIndex++) {
boolean hasFound = false;
final AxisDirection dstDir = dstAxes[dstIndex];
@@ -312,10 +313,16 @@ public final class Matrices {
}
}
if (!hasFound) {
- throw new IllegalArgumentException(Resources.format(
- Resources.Keys.CanNotMapAxisToDirection_1, dstAxes[dstIndex]));
+ if (exception == null) {
+ exception = new UnderdeterminedMatrixException(Resources.format(
+ Resources.Keys.CanNotMapAxisToDirection_1, dstDir));
+ }
+ exception.addUnknown(dstDir);
}
}
+ if (exception != null) {
+ throw exception;
+ }
matrix.setElement(dstAxes.length, srcAxes.length, 1);
return matrix;
}
@@ -439,8 +446,8 @@ public final class Matrices {
* @param srcAxes the ordered sequence of axis directions for source coordinate system.
* @param dstAxes the ordered sequence of axis directions for destination coordinate system.
* @return the transform from the given source axis directions to the given target axis directions.
- * @throws IllegalArgumentException if {@code dstAxes} contains at least one axis not found in {@code srcAxes},
- * or if some colinear axes were found.
+ * @throws UnderdeterminedMatrixException if {@code dstAxes} contains at least one axis not found in {@code srcAxes}.
+ * @throws IllegalArgumentException if some colinear axes were found.
*
* @see #createTransform(Envelope, Envelope)
* @see #createTransform(Envelope, AxisDirection[], Envelope, AxisDirection[])
@@ -511,8 +518,8 @@ public final class Matrices {
* to the given envelope and target axis directions.
* @throws MismatchedDimensionException if an envelope {@linkplain Envelope#getDimension() dimension} does not
* match the length of the axis directions sequence.
- * @throws IllegalArgumentException if {@code dstAxes} contains at least one axis not found in {@code srcAxes},
- * or if some colinear axes were found.
+ * @throws UnderdeterminedMatrixException if {@code dstAxes} contains at least one axis not found in {@code srcAxes}.
+ * @throws IllegalArgumentException if some colinear axes were found.
*
* @see #createTransform(Envelope, Envelope)
* @see #createTransform(AxisDirection[], AxisDirection[])
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/NoninvertibleMatrixException.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/NoninvertibleMatrixException.java
index 2552c5508f..f5a99b6f29 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/NoninvertibleMatrixException.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/NoninvertibleMatrixException.java
@@ -23,10 +23,10 @@ import org.opengis.referencing.operation.NoninvertibleTransformException;
* Thrown when a matrix cannot be inverted.
* This may be because the matrix is singular, or because the matrix is not square.
*
- * <h2>Usage note</h2>
- * This exception extends {@code NoninvertibleTransformException} because the matrices in this package
- * are used in <i>Coordinate Operation Steps</i>, in which case a singular matrix means that the
- * operation is not invertible.
+ * <h2>Design note</h2>
+ * This exception extends {@code NoninvertibleTransformException} because in the context
+ * of Apache <abbr>SIS</abbr>, matrix inversions occur mostly during the construction of
+ * a coordinate operation between a pair of <abbr>CRS</abbr>.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.4
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/UnderdeterminedMatrixException.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/UnderdeterminedMatrixException.java
new file mode 100644
index 0000000000..a0c7f12e70
--- /dev/null
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/UnderdeterminedMatrixException.java
@@ -0,0 +1,81 @@
+/*
+ * 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.matrix;
+
+import java.util.Set;
+import java.util.Collections;
+import org.apache.sis.util.collection.CodeListSet;
+import org.opengis.referencing.cs.AxisDirection;
+
+
+/**
+ * Thrown when a matrix cannot be determined because of unknown terms.
+ * It may happen, for example, during the construction of an affine transform between two
+ * coordinate systems when the source coordinate system does not have enough dimensions.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.6
+ *
+ * @see Matrices#createTransform(AxisDirection[], AxisDirection[])
+ *
+ * @since 1.6
+ */
+public class UnderdeterminedMatrixException extends IllegalArgumentException {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = 5248380660948565901L;
+
+ /**
+ * The unknown axes, identified by their directions.
+ */
+ private final CodeListSet<AxisDirection> axes = new CodeListSet<>(AxisDirection.class);
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message the detail message, or {@code null} if none.
+ */
+ public UnderdeterminedMatrixException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Indicates which term was unknown, specified as an axis direction for convenience.
+ * Unknown axes correspond to specific row indexes in the matrix, but are specified
+ * as axis directions for making easier for the caller to analyze. There is usually
+ * only one missing axis (typically the vertical or temporal one), but more can be added.
+ *
+ * <p>In the current Apache <abbr>SIS</abbr> version, the only reason for undetermined
+ * matrix is a missing axis, but more reasons may be added in future versions.</p>
+ *
+ * @param direction direction of the unknown axis.
+ */
+ public void addUnknown(final AxisDirection direction) {
+ axes.add(direction);
+ }
+
+ /**
+ * Indicates which axes were unknown during the attempt to create an affine transform.
+ * Unknown axes are identified by the directions that they were expected to have.
+ *
+ * @return directions of axes that are missing in the source coordinate system.
+ */
+ public Set<AxisDirection> getUnknownAxes() {
+ return Collections.unmodifiableSet(axes);
+ }
+}
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
index 89d0028a49..b67b620626 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.referencing.operation.matrix;
+import java.util.Set;
import static java.lang.Double.NaN;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
@@ -33,6 +34,7 @@ import org.apache.sis.util.iso.Types;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.sis.test.TestCase;
+import static org.apache.sis.test.Assertions.assertSetEquals;
import static org.apache.sis.test.Assertions.assertMultilinesEquals;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -181,11 +183,12 @@ public final class MatricesTest extends TestCase {
*/
@Test
public void testCreateTransformWithAxisNotInSource() {
- var e = assertThrows(IllegalArgumentException.class,
+ var e = assertThrows(UnderdeterminedMatrixException.class,
() -> Matrices.createTransform(
new AxisDirection[] {NORTH, EAST, UP},
new AxisDirection[] {DOWN, GEOCENTRIC_X}));
assertMessageContainsDirection(e, GEOCENTRIC_X);
+ assertSetEquals(Set.of(GEOCENTRIC_X), e.getUnknownAxes());
}
/**
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
index 75792a41a8..dd8495506a 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
@@ -73,7 +73,7 @@ final class MultiResolutionImage extends GridResourceWrapper implements StoreRes
/**
* The last coordinate operation returned by {@link #getTransformFrom(CoordinateReferenceSystem)}.
- * Used as an optimization in the common case where the same CRS is used for many requests.
+ * Used as an optimization in the common case where the same <abbr>CRS</abbr> is used for many requests.
*/
private volatile CoordinateOperation lastOperation;
@@ -205,12 +205,12 @@ final class MultiResolutionImage extends GridResourceWrapper implements StoreRes
}
/**
- * Converts a resolution from units in the given CRS to units of this coverage CRS.
+ * Returns the resolution of the given grid geometry, but in units of this coverage <abbr>CRS</abbr>.
*
* @param domain the geometry from which to get the resolution.
* @return resolution from the given grid geometry in units of this coverage CRS, or {@code null}.
*/
- private double[] getResolution(final GridGeometry domain) throws DataStoreException {
+ private double[] convertResolutionOf(final GridGeometry domain) throws DataStoreException {
if (domain == null || !domain.isDefined(GridGeometry.RESOLUTION)) {
return null;
}
@@ -253,7 +253,7 @@ final class MultiResolutionImage extends GridResourceWrapper implements StoreRes
*/
@Override
public GridCoverage read(final GridGeometry domain, final int... ranges) throws DataStoreException {
- final double[] request = getResolution(domain);
+ final double[] request = convertResolutionOf(domain);
int level = (request != null) ? resolutions.length : 1;
synchronized (getSynchronizationLock()) {
finer: while (--level > 0) {
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Localized.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Localized.java
index 3c20d02fe2..c5be805ed9 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Localized.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Localized.java
@@ -41,7 +41,7 @@ public interface Localized {
* </ul>
*
* Implementations are encouraged to return a non-null value in every cases.
- * Nevertheless client codes should be prepared to receive null values.
+ * Nevertheless, client codes should be prepared to receive null values.
*
* @return the locale, or {@code null} if not explicitly defined.
*/
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java
index d25c1352ee..f9e9d5f6f1 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java
@@ -336,7 +336,7 @@ public final class Logging {
public static boolean recoverableException(final Logger logger, final Class<?> classe,
final String method, final Throwable error)
{
- final String classname = (classe != null) ? classe.getName() : null;
+ final String classname = (classe != null) ? classe.getCanonicalName() : null;
return unexpectedException(logger, classname, method, error, Level.FINE);
}