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 f416e5744b Refactor the
`CoordinateOperationFinder.createOperationStep(GeodeticCRS source, GeodeticCRS
target)` method for adding a "spherical to ellipsoidal" step (or its inverse)
when needed, including addition of radius coordinate ("spherical 2D to 3D").
f416e5744b is described below
commit f416e5744b135c2c00b3f53d16743da7d49227e4
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Jun 8 19:28:43 2025 +0200
Refactor the `CoordinateOperationFinder.createOperationStep(GeodeticCRS
source, GeodeticCRS target)` method for adding a
"spherical to ellipsoidal" step (or its inverse) when needed, including
addition of radius coordinate ("spherical 2D to 3D").
https://issues.apache.org/jira/browse/SIS-302
---
.../sis/referencing/datum/BursaWolfParameters.java | 2 +-
.../referencing/datum/DefaultGeodeticDatum.java | 2 +-
.../internal/ParameterizedTransformBuilder.java | 239 ++++++++++------
.../operation/CoordinateOperationFinder.java | 208 +++-----------
.../operation/MathTransformContext.java | 88 +++++-
.../provider/GeocentricTranslation3D.java | 1 +
.../operation/provider/Geographic2Dto3D.java | 26 +-
.../operation/provider/Geographic3Dto2D.java | 19 +-
.../operation/provider/Spherical2Dto3D.java | 7 +-
.../operation/provider/Spherical3Dto2D.java | 7 +-
.../CoordinateSystemTransformBuilder.java | 318 +++++++++++++++++----
.../transform/DefaultMathTransformFactory.java | 4 +-
.../ParameterizedTransformBuilderTest.java | 2 +-
.../operation/CoordinateOperationFinderTest.java | 22 +-
14 files changed, 613 insertions(+), 332 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/BursaWolfParameters.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/BursaWolfParameters.java
index a38bd2c7d8..5af07dcc5a 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/BursaWolfParameters.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/BursaWolfParameters.java
@@ -645,7 +645,7 @@ public class BursaWolfParameters extends FormattableObject
implements Cloneable,
@Override
public boolean equals(final Object object) {
if (object != null && object.getClass() == getClass()) {
- final BursaWolfParameters that = (BursaWolfParameters) object;
+ final var that = (BursaWolfParameters) object;
return Arrays.equals(this.getValues(), that.getValues()) &&
Objects.equals(this.targetDatum, that.targetDatum) &&
Objects.equals(this.domainOfValidity, that.domainOfValidity);
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
index d90ed49af8..75cc8178e2 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
@@ -413,7 +413,7 @@ public class DefaultGeodeticDatum extends AbstractDatum
implements GeodeticDatum
*/
public Matrix getPositionVectorTransformation(final GeodeticDatum
targetDatum, final Extent areaOfInterest) {
ensureNonNull("targetDatum", targetDatum);
- final ExtentSelector<BursaWolfParameters> selector = new
ExtentSelector<>(areaOfInterest);
+ final var selector = new
ExtentSelector<BursaWolfParameters>(areaOfInterest);
BursaWolfParameters candidate = select(targetDatum, selector);
if (candidate != null) {
return createTransformation(candidate, areaOfInterest);
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
index d439168dd4..4b9d9ef158 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
@@ -37,11 +37,11 @@ import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.OperationMethod;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.util.NoSuchIdentifierException;
import org.opengis.util.FactoryException;
import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Classes;
import org.apache.sis.util.privy.Strings;
import org.apache.sis.util.privy.Constants;
@@ -52,12 +52,14 @@ import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.cs.CoordinateSystems;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.provider.AbstractProvider;
+import org.apache.sis.referencing.operation.provider.Geographic2Dto3D;
import org.apache.sis.referencing.operation.provider.VerticalOffset;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.MathTransformBuilder;
import org.apache.sis.referencing.operation.transform.ContextualParameters;
import org.apache.sis.referencing.operation.transform.MathTransformProvider;
import
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+import
org.apache.sis.referencing.operation.transform.EllipsoidToRadiusTransform;
import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
import org.apache.sis.referencing.privy.CoordinateOperations;
import org.apache.sis.referencing.privy.ReferencingUtilities;
@@ -99,12 +101,12 @@ public class ParameterizedTransformBuilder extends
MathTransformBuilder implemen
/**
* Coordinate system of the source or target points.
*/
- private CoordinateSystem sourceCS, targetCS;
+ protected CoordinateSystem sourceCS, targetCS;
/**
* The ellipsoid of the source or target ellipsoidal coordinate system, or
{@code null} if it does not apply.
*/
- private Ellipsoid sourceEllipsoid, targetEllipsoid;
+ protected Ellipsoid sourceEllipsoid, targetEllipsoid;
/**
* The parameters of the transform to create. This is initialized to
default values.
@@ -117,7 +119,7 @@ public class ParameterizedTransformBuilder extends
MathTransformBuilder implemen
* or {@link #getCompletedParameters()} will throw {@link
IllegalStateException},
* unless {@link #setParameters(ParameterValueGroup, boolean)} is
invoked.</p>
*/
- private ParameterValueGroup parameters;
+ protected ParameterValueGroup parameters;
/**
* Names of parameters which have been inferred from the context.
@@ -353,7 +355,7 @@ public class ParameterizedTransformBuilder extends
MathTransformBuilder implemen
/**
* Returns the parameter values to modify for defining the transform to
create.
- * Those parameters are initialized to default values, which are
{@linkplain #getMethod() method} depend.
+ * Those parameters are initialized to default values, which are
{@linkplain #getMethod() method} dependent.
* User-supplied values should be set directly in the returned instance
with codes like
*
<code>parameter(</code><var>name</var><code>).setValue(</code><var>value</var><code>)</code>.
*
@@ -665,14 +667,19 @@ public class ParameterizedTransformBuilder extends
MathTransformBuilder implemen
/**
* Given a transform between normalized spaces,
- * creates a transform taking in account axis directions, units of
measurement and longitude rotation.
- * This method {@linkplain #createConcatenatedTransform concatenates} the
given parameterized transform
+ * creates a transform taking in account axis directions and units of
measurement.
+ * This method {@linkplain #createConcatenatedTransform concatenates} the
given normalized transform
* with any other transform required for performing units changes and
coordinates swapping.
*
- * <p>The given {@code parameterized} transform shall expect
+ * <h4>Design note</h4>
+ * The {@code normalized} transform is a black box receiving inputs in any
<abbr>CS</abbr> and producing
+ * outputs in any <abbr>CS</abbr>, not necessarily of the same kind. For
that reason, this method cannot
+ * use {@link CoordinateSystems#swapAndScaleAxes(CoordinateSystem,
CoordinateSystem)} between the normalized CS.
+ * This method have to trust that the callers know that the coordinate
systems that they provided are correct
+ * for the work done by the transform. The given {@code normalized}
transform shall expect
* {@linkplain org.apache.sis.referencing.cs.AxesConvention#NORMALIZED
normalized} input coordinates
* and produce normalized output coordinates. See {@link
org.apache.sis.referencing.cs.AxesConvention}
- * for more information about what Apache SIS means by "normalized".</p>
+ * for more information about what Apache SIS means by "normalized".
*
* <h4>Example</h4>
* The most typical examples of transforms with normalized inputs/outputs
are normalized
@@ -694,16 +701,20 @@ public class ParameterizedTransformBuilder extends
MathTransformBuilder implemen
* @see
org.apache.sis.referencing.operation.DefaultConversion#DefaultConversion(Map,
OperationMethod, MathTransform, ParameterValueGroup)
*/
public MathTransform swapAndScaleAxes(final MathTransform normalized)
throws FactoryException {
- ArgumentChecks.ensureNonNull("parameterized", normalized);
+ ArgumentChecks.ensureNonNull("normalized", normalized);
/*
- * Compute matrices for swapping axis and performing units conversion.
+ * Compute matrices for swapping axes and performing units conversion.
* There is one matrix to apply before projection from (λ,φ)
coordinates,
* and one matrix to apply after projection on (easting,northing)
coordinates.
*/
final Matrix swap1 =
getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
final Matrix swap3 =
getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
/*
- * Prepare the concatenation of the matrices computed above and the
projection.
+ * Prepare the concatenation of above `swap` matrices with the
normalized transform.
+ * The chain of transforms built by this method will be:
+ *
+ * step1 ⟶ step2 (normalized) ⟶ step3
+ *
* Note that at this stage, the dimensions between each step may not
be compatible.
* For example, the projection (step2) is usually two-dimensional
while the source
* coordinate system (step1) may be three-dimensional if it has a
height.
@@ -712,77 +723,79 @@ public class ParameterizedTransformBuilder extends
MathTransformBuilder implemen
MathTransform step3 = swap3 != null ?
factory.createAffineTransform(swap3) :
MathTransforms.identity(normalized.getTargetDimensions());
MathTransform step2 = normalized;
/*
- * Special case for the way EPSG handles reversal of axis direction.
For now the "Vertical Offset" (EPSG:9616)
- * method is the only one for which we found a need for special case.
But if more special cases are added in a
- * future SIS version, then we should replace the static method by a
non-static one defined in AbstractProvider.
+ * Special case for the way that EPSG handles reversal of axis
direction.
+ * For now the "Vertical Offset" (EPSG:9616) method is the only known
case.
+ * But if more special cases are added in a future SIS version, then
we should
+ * replace the static method by a non-static one defined in
`AbstractProvider`.
*/
if (provider instanceof VerticalOffset) {
step2 = VerticalOffset.postCreate(step2, swap3);
}
/*
- * If the target coordinate system has a height, instruct the
projection to pass the height unchanged from
- * the base CRS to the target CRS. After this block, the dimensions of
`step2` and `step3` should match.
- *
- * The height is always the last dimension in a normalized
EllipdoidalCS. We accept only a hard-coded list
- * of dimensions because it is not `MathTransformFactory` job to build
a transform chain in a generic way.
- * We handle only the cases that are necessary because of the way some
operation methods are provided.
- * In particular Apache SIS provides only 2D map projections, so 3D
projections have to be "generated"
- * on the fly. That use case is:
+ * Add or remove the vertical coordinate of an ellipsoidal or
spherical coordinate system.
+ * For an ellipsoidal CS, the vertical coordinate is the height and
its default value is 0.
+ * For a spherical CS, the vertical coordinate is the radius and its
value depends on the latitude.
+ * If there is more than one dimension to add or remove, the extra
dimensions will be handled later.
*
- * - Source CRS: a GeographicCRS (regardless its number of
dimension – it will be addressed in next block)
- * - Target CRS: a 3D ProjectedCRS
- * - Parameterized transform: a 2D map projection. We need the
ellipsoidal height to passthrough.
- *
- * The reverse order (projected source CRS and geographic target CRS)
is also accepted but should be uncommon.
+ * Note that the vertical coordinate is added only if needed by
`step2`. If `step2` does not expect
+ * a vertical coordinate, then that coordinate is not added here. It
will be added later with a NaN
+ * value for avoiding a false sense of information availability.
+ */
+ int sourceDim = step1.getTargetDimensions(); // Number of
available dimensions.
+ int neededDim = step2.getSourceDimensions(); // Number of
required dimensions.
+ int kernelDim = step2.getTargetDimensions(); // Result of the
core part of transform.
+ int resultDim = step3.getSourceDimensions(); // Expected result
after the kernel part.
+ if (sourceDim == 2 && neededDim > 2) {
+ MathTransform change = addOrRemoveVertical(sourceCS,
sourceEllipsoid, targetEllipsoid, false);
+ if (change != null) {
+ step1 = factory.createConcatenatedTransform(step1, change);
+ sourceDim = step1.getTargetDimensions();
+ }
+ }
+ if (kernelDim == 3 && resultDim < 3) {
+ MathTransform change = addOrRemoveVertical(targetCS,
targetEllipsoid, sourceEllipsoid, true);
+ if (change != null) {
+ step3 = factory.createConcatenatedTransform(change, step3);
+ resultDim = step3.getSourceDimensions();
+ }
+ }
+ /*
+ * Make the number of target dimensions of `step2` compatible with the
number of source dimensions of `step3`.
+ * For example, SIS provides only 2D map projections, therefore 3D
projections must be generated on the fly
+ * by adding a "pass-through" transform.
*/
- final int resultDim = step3.getSourceDimensions(); //
Final result (minus trivial changes).
- final int kernelDim = step2.getTargetDimensions(); //
Result of the core part of transform.
final int numTrailingCoordinates = resultDim - kernelDim;
if (numTrailingCoordinates != 0) {
- ensureDimensionChangeAllowed(normalized, numTrailingCoordinates,
resultDim);
+ ensureDimensionChangeAllowed(numTrailingCoordinates, resultDim,
normalized);
if (numTrailingCoordinates > 0) {
step2 = factory.createPassThroughTransform(0, step2,
numTrailingCoordinates);
} else {
- var select = Matrices.createDimensionSelect(kernelDim,
ArraysExt.range(0, resultDim));
- step2 = factory.createConcatenatedTransform(step2,
factory.createAffineTransform(select));
+ step2 = factory.createConcatenatedTransform(step2,
addOrRemoveDimensions(kernelDim, resultDim));
}
+ neededDim = step2.getSourceDimensions();
+ kernelDim = step2.getTargetDimensions();
}
/*
- * If the source CS has a height but the target CS doesn't, drops the
extra coordinates.
- * Conversely if the source CS is missing a height, add a height with
NaN values.
- * After this block, the dimensions of `step1` and `step2` should
match.
- *
- * When adding an ellipsoidal height, there are two scenarios: the
ellipsoidal height may be used by the
- * parameterized operation, or it may be passed through (in which case
the operation ignores the height).
- * If the height is expected as operation input, set the height to 0.
Otherwise (the pass through case),
- * set the height to NaN. We do that way because the given
`parameterized` transform may be a Molodensky
- * transform or anything else that could use the height in its
calculation. If we have to add a height as
- * a pass through dimension, maybe the parameterized transform is a 2D
Molodensky instead of a 3D Molodensky.
- * The result of passing through the height is not the same as if a 3D
Molodensky was used in the first place.
- * A NaN value avoid to give a false sense of accuracy.
+ * Make the number of target dimensions of `step1` compatible with the
number of source dimensions of `step2`.
+ * If dimensions must be added, their values will be NaN. Note that
the vertical dimension (height or radius)
+ * has already been added with a non-NaN value before to reach this
point if that value was required by `step2`.
*/
- final int sourceDim = step1.getTargetDimensions();
- final int targetDim = step2.getSourceDimensions();
- int insertCount = targetDim - sourceDim;
- if (insertCount != 0) {
- ensureDimensionChangeAllowed(normalized, insertCount, targetDim);
- final Matrix resize = Matrices.createZero(targetDim+1,
sourceDim+1);
- for (int j=0; j<targetDim; j++) {
- resize.setElement(j, Math.min(j, sourceDim), (j < sourceDim) ?
1 :
- ((--insertCount >= numTrailingCoordinates) ? 0 :
Double.NaN)); // See above note.
- }
- resize.setElement(targetDim, sourceDim, 1); // Element in the
lower-right corner.
- step1 = factory.createConcatenatedTransform(step1,
factory.createAffineTransform(resize));
+ if (sourceDim != neededDim) {
+ ensureDimensionChangeAllowed(neededDim - sourceDim, neededDim,
normalized);
+ step1 = factory.createConcatenatedTransform(step1,
addOrRemoveDimensions(sourceDim, neededDim));
+ }
+ if (kernelDim != resultDim) {
+ ensureDimensionChangeAllowed(resultDim - kernelDim, resultDim,
normalized);
+ step3 =
factory.createConcatenatedTransform(addOrRemoveDimensions(kernelDim,
resultDim), step3);
}
- MathTransform mt =
factory.createConcatenatedTransform(factory.createConcatenatedTransform(step1,
step2), step3);
/*
- * At this point we finished to create the transform. But before to
return it, verify if the
- * parameterized transform given in argument had some custom
parameters. This happen with the
- * Equirectangular projection, which can be simplified as an
AffineTransform while we want to
- * continue to describe it with the "semi_major", "semi_minor", etc.
parameters instead of
- * "elt_0_0", "elt_0_1", etc. The following code just forwards those
parameters to the newly
- * created transform; it does not change the operation.
+ * Create the transform.
+ *
+ * Special case: if the parameterized transform was a map projection
but the result, after simplification,
+ * is an affine transform (it can happen with the Equirectangular
projection), wraps the affine transform
+ * for continuing to show "semi_major", "semi_minor", etc. parameters
instead of "elt_0_0", "elt_0_1", etc.
*/
+ MathTransform mt =
factory.createConcatenatedTransform(factory.createConcatenatedTransform(step1,
step2), step3);
if (normalized instanceof ParameterizedAffine && !(mt instanceof
ParameterizedAffine)) {
if (mt != (mt = ((ParameterizedAffine)
normalized).newTransform(mt))) {
mt = unique(mt);
@@ -792,26 +805,80 @@ public class ParameterizedTransformBuilder extends
MathTransformBuilder implemen
}
/**
- * Checks whether {@link #swapAndScaleAxes(MathTransform)} should accept
to adjust the number of
- * transform dimensions. The current implementation accepts only addition
or removal of ellipsoidal height,
- * but future version may expand the list of accepted cases. The intent
for this method is to catch errors
- * caused by wrong coordinate systems associated to a parameterized
transform, keeping in mind that it is
- * not {@link DefaultMathTransformFactory} job to handle changes between
arbitrary CRS (those changes are
- * handled by {@link
org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory}
instead).
+ * Adds or removes the ellipsoidal height or spherical radius dimension.
+ *
+ * @param cs the coordinate system for which to add a dimension,
or {@code null} if unknown.
+ * @param ellipsoid the ellipsoid to use, or {@code null} if unknown.
+ * @param fallback another ellipsoid that may be used if {@code
ellipsoid} is null.
+ * @param remove {@code false} for adding the dimension, or {@code
true} for removing the dimension.
+ * @return the transform to concatenate, or {@code null} if the coordinate
system is not recognized.
+ * @throws FactoryException if an error occurred while creating the
transform.
*
- * <h4>Implementation note</h4>
- * The {@code parameterized} transform is a black box receiving inputs in
any <abbr>CS</abbr> and
- * producing outputs in any <abbr>CS</abbr>, not necessarily of the same
kind. For that reason, we cannot use
- * {@link CoordinateSystems#swapAndScaleAxes(CoordinateSystem,
CoordinateSystem)} between the normalized CS.
- * We have to trust that the caller knows that the coordinate systems
(s)he provided are correct for the work
- * done by the transform.
+ * @see
org.apache.sis.referencing.operation.transform.CoordinateSystemTransformBuilder#addOrRemoveVertical
+ */
+ private MathTransform addOrRemoveVertical(final CoordinateSystem cs,
Ellipsoid ellipsoid,
+ final Ellipsoid fallback, final boolean remove) throws
FactoryException
+ {
+ if (ellipsoid == null) {
+ ellipsoid = fallback;
+ }
+ MathTransform tr;
+ if (cs instanceof EllipsoidalCS) {
+ Matrix resize = Matrices.createDiagonal(4, 3); // Set the height
to zero (not NaN).
+ resize.setElement(3, 2, 1); // Element in the
lower-right corner.
+ resize.setElement(2, 2, Geographic2Dto3D.DEFAULT_HEIGHT);
+ tr = factory.createAffineTransform(resize);
+ } else if (ellipsoid != null && cs instanceof SphericalCS) {
+ tr = EllipsoidToRadiusTransform.createGeodeticConversion(factory,
ellipsoid);
+ } else {
+ return null;
+ }
+ if (remove) try {
+ tr = tr.inverse();
+ } catch (NoninvertibleTransformException cause) {
+ throw new FactoryException(cause.getMessage(), cause);
+ }
+ return tr;
+ }
+
+ /**
+ * Adds or removes an arbitrary number of dimensions.
+ * Coordinate values of added dimensions will be NaN.
*
- * @param parameterized the parameterized transform, for producing an
error message if needed.
- * @param change number of dimensions to add (if positive) or
remove (if negative).
- * @param resultDim number of dimensions after the change.
+ * @param sourceDim the current number of dimensions.
+ * @param targetDim the desired number of dimensions.
+ * @return the transform to concatenate.
+ * @throws FactoryException if the transform cannot be created.
+ */
+ private MathTransform addOrRemoveDimensions(final int sourceDim, final int
targetDim) throws FactoryException {
+ final Matrix resize = Matrices.createDiagonal(targetDim+1,
sourceDim+1);
+ if (sourceDim < targetDim) {
+ for (int j=sourceDim; j<targetDim; j++) {
+ resize.setElement(j, sourceDim, Double.NaN);
+ }
+ } else {
+ resize.setElement(targetDim, targetDim, 0);
+ }
+ resize.setElement(targetDim, sourceDim, 1); // Element in the
lower-right corner.
+ return factory.createAffineTransform(resize);
+ }
+
+ /**
+ * Checks whether {@link #swapAndScaleAxes(MathTransform)} should accept
to adjust the number of dimensions.
+ * This method is for catching errors caused by wrong coordinate systems
associated to a parameterized transform,
+ * keeping in mind that it is not {@link DefaultMathTransformFactory} job
to handle changes between arbitrary CRS
+ * (those changes are handled by {@link
org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory}).
+ *
+ * <h4>Current rules</h4>
+ * The current implementation accepts only addition or removal of
ellipsoidal height.
+ * Future Apache SIS versions may expand the list of accepted cases.
+ *
+ * @param change number of dimensions to add (if positive) or remove
(if negative).
+ * @param resultDim number of dimensions after the change.
+ * @param normalized the parameterized transform, for producing an error
message if needed.
*/
- private void ensureDimensionChangeAllowed(final MathTransform
parameterized,
- final int change, final int resultDim) throws FactoryException
+ private void ensureDimensionChangeAllowed(final int change, final int
resultDim, final MathTransform normalized)
+ throws FactoryException
{
if (Math.abs(change) == 1 && resultDim >= 2 && resultDim <= 3) {
if (sourceCS instanceof EllipsoidalCS || targetCS instanceof
EllipsoidalCS) {
@@ -822,16 +889,16 @@ public class ParameterizedTransformBuilder extends
MathTransformBuilder implemen
* Creates the error message for a transform that cannot be associated
with given coordinate systems.
*/
String name = null;
- if (parameterized instanceof Parameterized) {
- name = IdentifiedObjects.getDisplayName(((Parameterized)
parameterized).getParameterDescriptors(), null);
+ if (normalized instanceof Parameterized) {
+ name = IdentifiedObjects.getDisplayName(((Parameterized)
normalized).getParameterDescriptors(), null);
}
if (name == null) {
- name = Classes.getShortClassName(parameterized);
+ name = Classes.getShortClassName(normalized);
}
final var b = new StringBuilder();
getSourceDimensions().ifPresent((dim) -> b.append(dim).append("D → "));
- b.append("tr(").append(parameterized.getSourceDimensions()).append("D
→ ")
-
.append(parameterized.getTargetDimensions()).append("D)");
+ b.append("tr(").append(normalized.getSourceDimensions()).append("D → ")
+ .append(normalized.getTargetDimensions()).append("D)");
getTargetDimensions().ifPresent((dim) -> b.append(" →
").append(dim).append('D'));
throw new
InvalidGeodeticParameterException(Resources.format(Resources.Keys.CanNotAssociateToCS_2,
name, b));
}
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 241ec340b4..2eb7c5a3fa 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
@@ -37,8 +37,6 @@ import org.opengis.metadata.Identifier;
import org.opengis.metadata.quality.PositionalAccuracy;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.parameter.ParameterDescriptorGroup;
-import org.apache.sis.parameter.TensorParameters;
import org.apache.sis.measure.Units;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.metadata.iso.extent.Extents;
@@ -58,12 +56,7 @@ import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
import org.apache.sis.referencing.datum.PseudoDatum;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
-import org.apache.sis.referencing.operation.provider.Affine;
import org.apache.sis.referencing.operation.provider.DatumShiftMethod;
-import org.apache.sis.referencing.operation.provider.Geographic2Dto3D;
-import org.apache.sis.referencing.operation.provider.Geographic3Dto2D;
-import org.apache.sis.referencing.operation.provider.GeographicToGeocentric;
-import org.apache.sis.referencing.operation.provider.GeocentricToGeographic;
import org.apache.sis.referencing.operation.provider.GeocentricAffine;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.ArgumentChecks;
@@ -490,8 +483,8 @@ public class CoordinateOperationFinder extends
CoordinateOperationRegistry {
* </ul>
*
* This method returns only <em>one</em> step for a chain of concatenated
operations (to be built by the caller).
- * But a list is returned because the same step may be implemented by
different operation methods. Only one element
- * in the returned list should be selected (usually the first one).
+ * But a list is returned because the same step may be implemented by
different operation methods.
+ * Only one element in the returned list should be selected (usually the
first one).
*
* @param sourceCRS input coordinate reference system.
* @param targetCRS output coordinate reference system.
@@ -506,176 +499,69 @@ public class CoordinateOperationFinder extends
CoordinateOperationRegistry {
final CoordinateSystem targetCS = targetCRS.getCoordinateSystem();
final GeodeticDatum sourceDatum = PseudoDatum.of(sourceCRS);
final GeodeticDatum targetDatum = PseudoDatum.of(targetCRS);
- Matrix datumShift = null;
- /*
- * If the prime meridian is not the same, we will concatenate a
longitude rotation before or after datum shift
- * (that concatenation will be performed by the `MathTransformContext`
builder created below).
- * Actually we do not know if the longitude rotation should be before
or after datum shift. But this ambiguity
- * can usually be ignored because Bursa-Wolf parameters are always
used with source and target prime meridians
- * set to Greenwich in EPSG dataset 8.9. For safety, the SIS's
DefaultGeodeticDatum class ensures that if the
- * prime meridians are not the same, then the target meridian must be
Greenwich.
- */
- final MathTransformFactory mtFactory =
factorySIS.getMathTransformFactory();
- final var context = new MathTransformContext(mtFactory, sourceDatum,
targetDatum);
- context.setSourceAxes(sourceCS, sourceDatum.getEllipsoid());
- context.setTargetAxes(targetCS, targetDatum.getEllipsoid());
/*
- * If both CRS use the same datum and the same prime meridian, then
the coordinate operation is only axis
- * swapping, unit conversion or change of coordinate system type
(Ellipsoidal ↔ Cartesian ↔ Spherical).
- * Otherwise (if the datum are not the same), we will need to perform
a scale, translation and rotation
- * in Cartesian space using the Bursa-Wolf parameters. If the user
does not require the best accuracy,
- * then the Molodensky approximation may be used for avoiding the
conversion step to geocentric CRS.
+ * Find the type of operation depending on whether there is a change
of geodetic reference frame (datum).
+ * The `DATUM_SHIFT` and `ELLIPSOID_CHANGE` identifiers mean that
there is a datum change, and all other
+ * identifiers mean that the coordinate operation is only a change of
coordinate system type (Ellipsoidal
+ * ↔ Cartesian ↔ Spherical), axis swapping and unit conversions.
*/
- Identifier identifier;
- boolean isGeographicToGeocentric = false;
+ final Matrix datumShift;
+ final Identifier identifier;
+ final MathTransform transform;
+ ParameterValueGroup parameters;
+ final Optional<OperationMethod> method;
final Optional<GeodeticDatum> commonDatum =
PseudoDatum.ofOperation(sourceCRS, targetCRS);
if (commonDatum.isPresent()) {
- final boolean isGeocentricToGeographic;
- isGeographicToGeocentric = (sourceCS instanceof EllipsoidalCS &&
targetCS instanceof CartesianCS);
- isGeocentricToGeographic = (sourceCS instanceof CartesianCS &&
targetCS instanceof EllipsoidalCS);
/*
- * Above booleans should never be true at the same time. If it
nevertheless happen (we are paranoiac;
- * maybe a lazy user implemented all interfaces in a single
class), do not apply any geographic ↔
- * geocentric conversion. Instead, do as if the coordinate system
types were the same.
+ * Coordinate system change (including change in the number of
dimensions) without datum shift.
+ * May contain the addition of ellipsoidal height or spherical
radius, which need an ellipsoid.
*/
- if (isGeocentricToGeographic ^ isGeographicToGeocentric) {
- identifier = GEOCENTRIC_CONVERSION;
- } else {
- identifier = AXIS_CHANGES;
- }
+ final boolean isGeographic = (sourceCS instanceof EllipsoidalCS);
+ identifier = isGeographic != (targetCS instanceof EllipsoidalCS) ?
GEOCENTRIC_CONVERSION : AXIS_CHANGES;
+ final var builder =
factorySIS.getMathTransformFactory().builder(Constants.COORDINATE_SYSTEM_CONVERSION);
+ final var ellipsoid = (isGeographic ? sourceDatum :
targetDatum).getEllipsoid();
+ builder.setSourceAxes(sourceCS, ellipsoid);
+ builder.setTargetAxes(targetCS, ellipsoid);
+ transform = builder.create();
+ method = builder.getMethod();
+ parameters = builder.parameters();
+ datumShift = null;
} else {
- identifier = ELLIPSOID_CHANGE;
- if (sourceDatum instanceof DefaultGeodeticDatum) {
- datumShift = ((DefaultGeodeticDatum)
sourceDatum).getPositionVectorTransformation(targetDatum, areaOfInterest);
- if (datumShift != null) {
- identifier = DATUM_SHIFT;
- }
- }
- }
- /*
- * Conceptually, all transformations below could be done by first
converting from source coordinate
- * system to geocentric Cartesian coordinates (X,Y,Z), apply an affine
transform represented by the
- * datum shift matrix, then convert from the (X′,Y′,Z′) coordinates to
the target coordinate system.
- * However, there are two exceptions to this path:
- *
- * 1) In the particular where both the source and target CS are
ellipsoidal, we may use the
- * Molodensky approximation as a shortcut (if the desired
accuracy allows).
- *
- * 2) Even if we really go through the XYZ coordinates without
Molodensky approximation, there is
- * at least 9 different ways to name this operation depending on
whether the source and target
- * CRS are geocentric or geographic, 2- or 3-dimensional, whether
there is a translation or not,
- * the rotation sign, etc. We try to use the most specific name
if we can find one, and fallback
- * on an arbitrary name only in last resort.
- */
- MathTransform before = null, after = null;
- ParameterValueGroup parameters;
- OperationMethod method = null;
- if (identifier == DATUM_SHIFT || identifier == ELLIPSOID_CHANGE) {
/*
- * If the transform can be represented by a single coordinate
operation, returns that operation.
+ * Conceptually, all transformations below could be done by first
converting from source coordinate
+ * system to geocentric Cartesian coordinates (X,Y,Z), apply an
affine transform represented by the
+ * datum shift matrix, then convert from the (X′,Y′,Z′)
coordinates to the target coordinate system.
+ * However, there are exceptions to this path:
+ *
+ * 1) Conversion from ellipsoidal to spherical CS can skip the
Cartesian step for performance.
+ * 2) Transformation between ellipsoidal CS may use the
Molodensky approximation as a shortcut.
+ * 3) Even when really going through the XYZ coordinates, the
name of that operation depends on
+ * whether the source and target CRS are geocentric or
geographic, 2- or 3-dimensional,
+ * whether there is a translation, the rotation sign, etc.
+ *
* Possible operations are:
*
* - Position Vector transformation (in geocentric,
geographic-2D or geographic-3D domains)
* - Geocentric translation (in geocentric,
geographic-2D or geographic-3D domains)
* - [Abridged] Molodensky (as an approximation of
geocentric translation)
* - Identity (if the desired accuracy is
so large than we can skip datum shift)
- *
- * If both CS are ellipsoidal but with different number of
dimensions, then a three-dimensional
- * operation is used and `DefaultMathTransformFactory` will add an
ellipsoidal height on-the-fly.
- * We let the transform factory do this work instead of adding an
intermediate "geographic 2D
- * to 3D" operation here because the transform factory works with
normalized CS, which avoid the
- * need the search in which dimension to add the ellipsoidal
height (it should always be last,
- * but SIS is tolerant to unusual axis order).
*/
+ datumShift = (sourceDatum instanceof DefaultGeodeticDatum) ?
+ ((DefaultGeodeticDatum)
sourceDatum).getPositionVectorTransformation(targetDatum, areaOfInterest) :
null;
+ identifier = (datumShift != null) ? DATUM_SHIFT : ELLIPSOID_CHANGE;
+ var builder = new
MathTransformContext(factorySIS.getMathTransformFactory(), sourceDatum,
targetDatum);
+ builder.setSourceAxes(sourceCS, sourceDatum.getEllipsoid());
+ builder.setTargetAxes(targetCS, targetDatum.getEllipsoid());
parameters = GeocentricAffine.createParameters(sourceCS, targetCS,
datumShift,
DatumShiftMethod.forAccuracy(desiredAccuracy));
- if (parameters == null) {
- /*
- * Failed to select a coordinate operation. Maybe because the
coordinate system types are not the same.
- * Convert unconditionally to XYZ geocentric coordinates and
apply the datum shift in that CS space.
- */
- if (datumShift != null) {
- parameters =
TensorParameters.WKT1.createValueGroup(properties(Constants.AFFINE),
datumShift);
- } else {
- parameters = Affine.identity(3); //
Dimension of geocentric CRS.
- }
- final CoordinateSystem normalized =
CommonCRS.WGS84.geocentric().getCoordinateSystem();
- before = context.createCoordinateSystemChange(sourceCS,
normalized, sourceDatum.getEllipsoid());
- after = context.createCoordinateSystemChange(normalized,
targetCS, targetDatum.getEllipsoid());
- context.setSourceAxes(normalized, null);
- context.setTargetAxes(normalized, null);
- /*
- * The name of the `parameters` group determines the
`OperationMethod` later in this method.
- * We cannot leave that name to "Affine" if `before` or
`after` transforms are not identity.
- * Note: we check for identity transforms instead of relaxing
to general `LinearTransform`
- * because otherwise, we would have to update values declared
in `parameters`. It is doable
- * but not done yet.
- */
- if (!(before.isIdentity() && after.isIdentity())) {
- method = LooselyDefinedMethod.AFFINE_GEOCENTRIC;
- }
- }
- } else if (identifier == GEOCENTRIC_CONVERSION) {
- /*
- * Geographic ↔︎ Geocentric conversion.
- */
- final ParameterDescriptorGroup descriptor;
- if (isGeographicToGeocentric) {
- descriptor = GeographicToGeocentric.PARAMETERS;
+ if (parameters != null) {
+ builder.setParameters(parameters, false);
+ transform = builder.create();
} else {
- descriptor = GeocentricToGeographic.PARAMETERS;
- }
- parameters = descriptor.createValue();
- } else {
- /*
- * Coordinate system change (including change in the number of
dimensions) without datum shift.
- */
- final int sourceDim = sourceCS.getDimension();
- final int targetDim = targetCS.getDimension();
- if (sourceDim == 2 && targetDim == 3 && sourceCS instanceof
EllipsoidalCS) {
- parameters = Geographic2Dto3D.PARAMETERS.createValue();
- } else if (sourceDim == 3 && targetDim == 2 && targetCS instanceof
EllipsoidalCS) {
- parameters = Geographic3Dto2D.PARAMETERS.createValue();
- } else {
- parameters = Affine.identity(targetDim);
- /*
- * "Coordinate system conversion" needs the ellipsoid
associated to the ellipsoidal coordinate system,
- * if any. If none or both coordinate systems are ellipsoidal,
then the ellipsoid will be ignored.
- */
- var ellipsoid = (sourceCS instanceof EllipsoidalCS ?
sourceDatum : targetDatum).getEllipsoid();
- var builder =
mtFactory.builder(Constants.COORDINATE_SYSTEM_CONVERSION);
- builder.setSourceAxes(sourceCS, ellipsoid);
- builder.setTargetAxes(targetCS, ellipsoid);
- before = builder.create();
- method = builder.getMethod().orElse(null);
- context.setSourceAxes(targetCS, null);
- }
- }
- /*
- * Transform between differents datums using Bursa Wolf parameters.
The Bursa Wolf parameters are used
- * with "standard" geocentric CS, i.e. with X axis towards the prime
meridian, Y axis towards East and
- * Z axis toward North, unless the Molodensky approximation is used.
The following steps are applied:
- *
- * source CRS →
- * normalized CRS with source datum →
- * normalized CRS with target datum →
- * target CRS
- *
- * Those steps may be either explicit with the `before` and `after`
transforms, or implicit with the
- * Context parameter. The operation name is inferred from the
parameters, unless a method has been
- * specified in advance.
- */
- context.setParameters(parameters, false);
- MathTransform transform = context.create();
- if (method == null) {
- method = context.getMethod().orElse(null);
- }
- if (before != null) {
- parameters = null; // Providing parameters would be
misleading because they apply to only a step of the operation.
- transform = mtFactory.createConcatenatedTransform(before,
transform);
- if (after != null) {
- transform = mtFactory.createConcatenatedTransform(transform,
after);
+ transform = builder.createAffineGeocentric(datumShift);
}
+ method = builder.getMethod();
+ parameters = builder.parametersForMetadata();
}
/*
* Adjust the accuracy information if the datum shift has been
computed by an indirect path.
@@ -690,7 +576,7 @@ public class CoordinateOperationFinder extends
CoordinateOperationRegistry {
if (accuracy != null) {
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, accuracy);
}
- return asList(createFromMathTransform(properties, sourceCRS,
targetCRS, transform, method, parameters, null));
+ return asList(createFromMathTransform(properties, sourceCRS,
targetCRS, transform, method.orElse(null), parameters, null));
}
/**
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MathTransformContext.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MathTransformContext.java
index bac42900d7..1e8f15335d 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MathTransformContext.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MathTransformContext.java
@@ -16,7 +16,9 @@
*/
package org.apache.sis.referencing.operation;
+import java.util.Map;
import org.opengis.util.FactoryException;
+import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.cs.CartesianCS;
import org.opengis.referencing.cs.SphericalCS;
import org.opengis.referencing.cs.EllipsoidalCS;
@@ -26,12 +28,16 @@ import org.opengis.referencing.datum.Ellipsoid;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.parameter.ParameterValueGroup;
+import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.privy.ReferencingUtilities;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.Matrix4;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.provider.Affine;
import
org.apache.sis.referencing.operation.transform.ContextualParameters.MatrixRole;
import org.apache.sis.referencing.internal.ParameterizedTransformBuilder;
+import org.apache.sis.parameter.TensorParameters;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.privy.Constants;
import org.apache.sis.measure.Units;
@@ -42,10 +48,22 @@ import org.opengis.util.UnimplementedServiceException;
/**
* Information about the context in which a {@code MathTransform} is created.
- * This class performs the same normalization as the super-class (namely axis
swapping and unit conversions),
- * with the addition of longitude rotation for supporting change of prime
meridian.
- * This latter change is not applied by the super-class because prime meridian
is part of geodetic reference frame,
- * and the public math transform factory knows nothing about datum (on design,
for separation of concerns).
+ * This class inherits the normalization performed by the super-class (axis
swapping and unit conversions),
+ * then adds the work described in the sub-sections below. This additional
work is separated in this class
+ * because that work handles changes of a property of reference frames (e.g.,
a change of prime meridian),
+ * and the public math transform factory does not handle datum change by
design (for separation of concerns).
+ *
+ * <h2>Longitude rotation</h2>
+ * If the prime meridian is not the same, this class concatenates a longitude
rotation with the normalization.
+ * Actually, we do not know if the longitude rotation should be before or
after datum shift. But this ambiguity
+ * can usually be ignored because Bursa-Wolf parameters are always used with
source and target prime meridians
+ * set to Greenwich in EPSG dataset 8.9. For safety, {@link
org.apache.sis.referencing.datum.DefaultGeodeticDatum}
+ * constructor ensures that if the prime meridians are not the same, then the
target meridian must be Greenwich.
+ *
+ * <h2>Bursa-Wolf parameters</h2>
+ * If the source and target ellipsoids are not the same, this class can apply
an affine operation in geocentric
+ * Cartesian coordinates using Bursa-Wolf parameters. This is used only as a
fallback when no explicit operation
+ * was found in the <abbr>EPSG</abbr> database.
*
* @author Martin Desruisseaux (Geomatys)
*/
@@ -92,19 +110,71 @@ final class MathTransformContext extends
ParameterizedTransformBuilder {
* @return a conversion from the given source to the given target
coordinate system.
* @throws FactoryException if the conversion cannot be created.
*/
- final MathTransform createCoordinateSystemChange(final CoordinateSystem
source,
- final CoordinateSystem
target,
- final Ellipsoid ellipsoid)
+ private MathTransform createCoordinateSystemChange(final CoordinateSystem
source,
+ final CoordinateSystem
target,
+ final Ellipsoid
ellipsoid)
throws FactoryException
{
- final var builder =
getFactory().builder(Constants.COORDINATE_SYSTEM_CONVERSION);
+ final var builder =
factory.builder(Constants.COORDINATE_SYSTEM_CONVERSION);
builder.setSourceAxes(source, ellipsoid);
builder.setTargetAxes(target, ellipsoid);
return builder.create();
}
/**
- * Returns the normalization or denormalization matrix.
+ * Creates an affine operation (translation and rotation) in geocentric
Cartesian space.
+ * This method is invoked as a fallback when {@link
CoordinateOperationFinder} failed to
+ * select a coordinate operation, maybe because the coordinate system
types are not the same.
+ * This method unconditionally converts to geocentric Cartesian
coordinates and applies
+ * the datum shift in that space.
+ *
+ * <p>This method sets {@link #canDeclareParameters} to {@code true} if it
is okay to use
+ * {@link #parameters()} as metadata in the coordinate operation created
with this transform.</p>
+ *
+ * @param datumShift the operation to apply in geocentric Cartesian
coordinates, or {@code null} for identity.
+ * @return the transform applying the specified affine operation in
geocentric Cartesian space.
+ * @throws FactoryException if the transformation cannot be created.
+ */
+ final MathTransform createAffineGeocentric(final Matrix datumShift) throws
FactoryException {
+ if (datumShift != null) {
+ final var properties = Map.of(IdentifiedObject.NAME_KEY,
Constants.AFFINE);
+ parameters = TensorParameters.WKT1.createValueGroup(properties,
datumShift);
+ } else {
+ parameters = Affine.identity(3); // Dimension of geocentric
CRS.
+ }
+ provider = Affine.provider();
+ final CoordinateSystem normalized =
CommonCRS.WGS84.geocentric().getCoordinateSystem();
+ final MathTransform before = createCoordinateSystemChange(sourceCS,
normalized, sourceEllipsoid);
+ final MathTransform after = createCoordinateSystemChange(normalized,
targetCS, targetEllipsoid);
+ setSourceAxes(normalized, null);
+ setTargetAxes(normalized, null);
+ final MathTransform tr = factory.createConcatenatedTransform(before,
+ factory.createConcatenatedTransform(create(),
after));
+ if (!(before.isIdentity() && after.isIdentity())) {
+ // Providing parameters would be misleading because they apply to
only a step of the operation.
+ provider = LooselyDefinedMethod.AFFINE_GEOCENTRIC;
+ parameters = null;
+ }
+ return tr;
+ }
+
+ /**
+ * Returns the parameters if they can be declared as metadata in the
coordinate operation.
+ * The parameters may need to be ignored when providing them would be
misleading, for example,
+ * because additional steps have been added for normalization before or
after the main transform.
+ */
+ final ParameterValueGroup parametersForMetadata() {
+ return parameters;
+ }
+
+ /**
+ * Returns the normalization or denormalization matrix, including
longitude rotation if any.
+ * This method is invoked indirectly by {@link #create()} and {@link
#createAffineGeocentric(Matrix)}.
+ * Therefore, longitude rotations are injected in a transform chain
indirectly through this method.
+ *
+ * @param role whether the normalization or denormalization matrix is
desired.
+ * @return the requested matrix, or {@code null} if this builder has no
information about the coordinate system.
+ * @throws UnimplementedServiceException if a longitude rotation is needed
but the coordinate system type is not recognized.
*/
@Override
@SuppressWarnings("fallthrough")
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/GeocentricTranslation3D.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/GeocentricTranslation3D.java
index 6ffa20b55f..359454aa13 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/GeocentricTranslation3D.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/GeocentricTranslation3D.java
@@ -45,6 +45,7 @@ public final class GeocentricTranslation3D extends
GeocentricAffineBetweenGeogra
.addName("Geocentric translations (geog3D domain)")
.createGroupWithSameParameters(GeocentricTranslation2D.PARAMETERS);
}
+
/**
* The canonical instance of this operation method.
*
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Geographic2Dto3D.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Geographic2Dto3D.java
index 09c385a610..db79328b9b 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Geographic2Dto3D.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Geographic2Dto3D.java
@@ -26,6 +26,8 @@ import org.opengis.referencing.operation.MathTransform;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.parameter.ParameterBuilder;
import org.apache.sis.util.privy.Constants;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.measure.Units;
/**
@@ -39,6 +41,7 @@ import org.apache.sis.util.privy.Constants;
* @author Martin Desruisseaux (Geomatys)
*
* @see Geographic3Dto2D
+ * @see Spherical2Dto3D
*/
@XmlTransient
public final class Geographic2Dto3D extends AbstractProvider {
@@ -47,6 +50,11 @@ public final class Geographic2Dto3D extends AbstractProvider
{
*/
private static final long serialVersionUID = -1198461394243672064L;
+ /**
+ * The name used by Apache <abbr>SIS</abbr> for this operation method.
+ */
+ public static final String NAME = "Geographic2D to 3D conversion";
+
/**
* The ellipsoidal height to set.
*
@@ -56,7 +64,12 @@ public final class Geographic2Dto3D extends AbstractProvider
{
* <tr><td> SIS: </td><td> height </td></tr>
* </table>
*/
- public static final ParameterDescriptor<Double> HEIGHT;
+ private static final ParameterDescriptor<Double> HEIGHT;
+
+ /**
+ * The default height value.
+ */
+ public static final double DEFAULT_HEIGHT = 0;
/**
* The group of all parameters expected by this coordinate operation.
@@ -64,8 +77,8 @@ public final class Geographic2Dto3D extends AbstractProvider {
public static final ParameterDescriptorGroup PARAMETERS;
static {
final ParameterBuilder builder = builder().setCodeSpace(Citations.SIS,
Constants.SIS);
- HEIGHT = createShift(builder.addName("height"));
- PARAMETERS = builder.addName("Geographic2D to 3D
conversion").createGroup(HEIGHT);
+ HEIGHT = builder.addName("height").create(DEFAULT_HEIGHT, Units.METRE);
+ PARAMETERS = builder.addName(NAME).createGroup(HEIGHT);
}
/**
@@ -118,7 +131,8 @@ public final class Geographic2Dto3D extends
AbstractProvider {
}
/**
- * Returns the transform.
+ * Creates the transform adding a constant ellipsoidal height.
+ * The parameter value is unconditionally converted to metres.
*
* @param context the parameter values together with its context.
* @return the math transform for the given parameter values.
@@ -126,8 +140,10 @@ public final class Geographic2Dto3D extends
AbstractProvider {
*/
@Override
public MathTransform createMathTransform(final Context context) throws
FactoryException {
+ final Parameters pg =
Parameters.castOrWrap(context.getCompletedParameters());
return Geographic3Dto2D.createMathTransform(context,
context.getSourceDimensions().orElse(2),
- context.getTargetDimensions().orElse(3));
+ context.getTargetDimensions().orElse(3),
+ pg.doubleValue(HEIGHT));
}
}
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Geographic3Dto2D.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Geographic3Dto2D.java
index 9fa4933f14..9286ff48bc 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Geographic3Dto2D.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Geographic3Dto2D.java
@@ -41,6 +41,7 @@ import org.apache.sis.io.wkt.Formatter;
* @author Martin Desruisseaux (Geomatys)
*
* @see Geographic2Dto3D
+ * @see Spherical3Dto2D
*/
@XmlTransient
public final class Geographic3Dto2D extends AbstractProvider {
@@ -49,11 +50,16 @@ public final class Geographic3Dto2D extends
AbstractProvider {
*/
private static final long serialVersionUID = -9103595336196565505L;
+ /**
+ * The <abbr>EPSG</abbr> name used for this operation method.
+ */
+ public static final String NAME = "Geographic3D to 2D conversion";
+
/**
* The group of all parameters expected by this coordinate operation (in
this case, none).
*/
public static final ParameterDescriptorGroup PARAMETERS = builder()
- .addIdentifier("9659").addName("Geographic3D to 2D
conversion").createGroup();
+ .addIdentifier("9659").addName(NAME).createGroup();
/**
* The canonical instance of this operation method.
@@ -122,13 +128,14 @@ public final class Geographic3Dto2D extends
AbstractProvider {
public MathTransform createMathTransform(final Context context) throws
FactoryException {
return createMathTransform(context,
context.getSourceDimensions().orElse(3),
- context.getTargetDimensions().orElse(2));
+ context.getTargetDimensions().orElse(2),
+ Geographic2Dto3D.DEFAULT_HEIGHT);
}
/**
* Implementation of {@link #createMathTransform(Context)} shared by
{@link Geographic2Dto3D}.
*/
- static MathTransform createMathTransform(final Context context, int
sourceDimensions, int targetDimensions)
+ static MathTransform createMathTransform(final Context context, int
sourceDimensions, int targetDimensions, final double height)
throws FactoryException
{
final boolean inverse = (sourceDimensions > targetDimensions);
@@ -138,13 +145,13 @@ public final class Geographic3Dto2D extends
AbstractProvider {
targetDimensions = swap;
}
final MatrixSIS m = Matrices.createDiagonal(targetDimensions + 1,
sourceDimensions + 1);
- m.setElement(sourceDimensions, sourceDimensions, 0); // Here is the
height value that we want.
- m.setElement(targetDimensions, sourceDimensions, 1); // Most be
last in case the matrix is square.
+ m.setElement(sourceDimensions, sourceDimensions, height);
+ m.setElement(targetDimensions, sourceDimensions, 1); // Must be
last in case the matrix is square.
MathTransform tr = context.getFactory().createAffineTransform(m);
if (inverse) try {
tr = tr.inverse();
} catch (NoninvertibleTransformException e) {
- throw new FactoryException(e); // Should
never happen.
+ throw new FactoryException(e); // Should
never happen.
}
return tr;
}
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Spherical2Dto3D.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Spherical2Dto3D.java
index 4893b482ab..10c117bff4 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Spherical2Dto3D.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Spherical2Dto3D.java
@@ -44,13 +44,18 @@ public final class Spherical2Dto3D extends AbstractProvider
{
*/
private static final long serialVersionUID = -2320384527305401074L;
+ /**
+ * The name used by Apache <abbr>SIS</abbr> for this operation method.
+ */
+ public static final String NAME = "Spherical2D to 3D conversion";
+
/**
* The group of all parameters expected by this coordinate operation.
*/
public static final ParameterDescriptorGroup PARAMETERS;
static {
final ParameterBuilder builder = builder().setCodeSpace(Citations.SIS,
Constants.SIS);
- PARAMETERS = builder.addName("Spherical2D to 3D
conversion").createGroupForMapProjection();
+ PARAMETERS = builder.addName(NAME).createGroupForMapProjection();
}
/**
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Spherical3Dto2D.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Spherical3Dto2D.java
index a831f5e455..408b8788d1 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Spherical3Dto2D.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Spherical3Dto2D.java
@@ -45,13 +45,18 @@ public final class Spherical3Dto2D extends AbstractProvider
{
*/
private static final long serialVersionUID = -6165087357029633662L;
+ /**
+ * The name used by Apache <abbr>SIS</abbr> for this operation method.
+ */
+ public static final String NAME = "Spherical3D to 2D conversion";
+
/**
* The group of all parameters expected by this coordinate operation.
*/
public static final ParameterDescriptorGroup PARAMETERS;
static {
final ParameterBuilder builder = builder().setCodeSpace(Citations.SIS,
Constants.SIS);
- PARAMETERS = builder.addName("Spherical3D to 2D
conversion").createGroupForMapProjection();
+ PARAMETERS = builder.addName(NAME).createGroupForMapProjection();
}
/**
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransformBuilder.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransformBuilder.java
index 1476165cc0..82a6d02100 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransformBuilder.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransformBuilder.java
@@ -29,10 +29,17 @@ import org.opengis.referencing.cs.SphericalCS;
import org.opengis.referencing.cs.PolarCS;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.OperationNotFoundException;
+import org.apache.sis.parameter.Parameterized;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.cs.CoordinateSystems;
import org.apache.sis.referencing.internal.Resources;
+import org.apache.sis.referencing.operation.provider.Affine;
+import org.apache.sis.referencing.operation.provider.Spherical2Dto3D;
+import org.apache.sis.referencing.operation.provider.Spherical3Dto2D;
+import org.apache.sis.referencing.operation.provider.Geographic2Dto3D;
+import org.apache.sis.referencing.operation.provider.Geographic3Dto2D;
import org.apache.sis.referencing.operation.provider.GeocentricToGeographic;
import org.apache.sis.referencing.operation.provider.GeographicToGeocentric;
import org.apache.sis.referencing.privy.WKTUtilities;
@@ -41,6 +48,9 @@ import org.apache.sis.util.resources.Errors;
/**
* Builder of transforms between coordinate systems.
+ * This class performs only axis swapping, unit conversions and change of
coordinate system type.
+ * This class does not handle datum shifts. All <abbr>CRS</abbr> associated to
the <abbr>CS</abbr>
+ * must use the same datum.
*
* @author Martin Desruisseaux (Geomatys)
*/
@@ -51,12 +61,53 @@ final class CoordinateSystemTransformBuilder extends
MathTransformBuilder {
private CoordinateSystem source, target;
/**
- * The ellipsoid of the source or the target.
- * Only one of the source or target should have an ellipsoid,
- * because this builder is not for datum change.
+ * The ellipsoid of the source and/or the target. Usually, only one of the
source or target
+ * is associated to an ellipsoid. If an ellipsoid is specified for both
source and target,
+ * then it must be the same ellipsoid because this builder is not for
datum change.
*/
private Ellipsoid ellipsoid;
+ /**
+ * The parameters used for creating the transform, or {@code null} if
unspecified.
+ *
+ * @see #parameters()
+ * @see #setParameters(MathTransform, OperationMethod, ParameterValueGroup)
+ */
+ private ParameterValueGroup parameters;
+
+ /**
+ * The {@link MathTransform} to use as the source of parameters, or {@code
null} if none.
+ * This is used as a fallback if {@link #parameters} is null.
+ *
+ * @see #parameters()
+ * @see #setParameters(MathTransform, OperationMethod, ParameterValueGroup)
+ */
+ private Parameterized parameterized;
+
+ /**
+ * A code identifying the type of parameters. Values can be:
+ *
+ * <ul>
+ * <li>0: parameters are not set.</li>
+ * <li>1: identity transform.</li>
+ * <li>2: linear transform.</li>
+ * <li>3: non-linear transform.</li>
+ * </ul>
+ *
+ * THe {@link #provider} and {@link #parameters} fields should be updated
together
+ * and only with strictly increasing {@code parameterType} values. If
parameters
+ * are specified twice for the same type, the first occurrence prevails.
+ *
+ * @see #parameters()
+ * @see #setParameters(MathTransform, OperationMethod, ParameterValueGroup)
+ */
+ private byte parametersType;
+
+ /**
+ * Values for the {@link #parametersType} field.
+ */
+ private static final byte IDENTITY = 1, LINEAR = 2, CONVERSION = 3;
+
/**
* Creates a new builder.
*
@@ -68,6 +119,9 @@ final class CoordinateSystemTransformBuilder extends
MathTransformBuilder {
/**
* Sets the source coordinate system.
+ * The ellipsoid shall be either null or the same as the target.
+ *
+ * @throws IllegalStateException if more than one ellipsoid is specified.
*/
@Override
public void setSourceAxes(CoordinateSystem cs, Ellipsoid ellipsoid) {
@@ -77,6 +131,9 @@ final class CoordinateSystemTransformBuilder extends
MathTransformBuilder {
/**
* Sets the target coordinate system.
+ * The ellipsoid shall be either null or the same as the source.
+ *
+ * @throws IllegalStateException if more than one ellipsoid is specified.
*/
@Override
public void setTargetAxes(CoordinateSystem cs, Ellipsoid ellipsoid) {
@@ -85,7 +142,10 @@ final class CoordinateSystemTransformBuilder extends
MathTransformBuilder {
}
/**
- * Sets the ellipsoid if it was not already set.
+ * Sets the unique ellipsoid.
+ *
+ * @param value the ellipsoid, or {@code null} if unspecified.
+ * @throws IllegalStateException if more than one ellipsoid is specified.
*/
private void setEllipsoid(final Ellipsoid value) {
if (value != null) {
@@ -97,11 +157,31 @@ final class CoordinateSystemTransformBuilder extends
MathTransformBuilder {
}
/**
- * Unsupported operation because this builder has no parameters.
+ * Returns the parameters for creating the transform. If possible, the
parameter values will be set
+ * to the values actually used, but this is not guaranteed (this is not
required by method contract).
+ * This information is valid after the {@link #create()} method has been
invoked.
*/
@Override
public ParameterValueGroup parameters() {
- throw new
IllegalStateException(Errors.format(Errors.Keys.MissingValueForProperty_1,
"method"));
+ if (parameters == null) {
+ if (parameterized != null) {
+ parameters = parameterized.getParameterValues();
+ }
+ if (parameters == null) {
+ OperationMethod method = provider;
+ if (method == null) {
+ method = Affine.provider();
+ }
+ /*
+ * This is either a parameterless conversion (for example,
from spherical to Cartesian coordinate system),
+ * in which case there is no parameters to set, or an affine
transform. In the latter case, the default is
+ * the identity transform, which is often correct. But even if
not exact, an identity affine is still okay
+ * because this method contract is to provide an initial set
of parameters to be filled by the user.
+ */
+ parameters = method.getParameters().createValue();
+ }
+ }
+ return parameters;
}
/**
@@ -122,16 +202,6 @@ final class CoordinateSystemTransformBuilder extends
MathTransformBuilder {
final List<CoordinateSystem> sources =
CoordinateSystems.getSingleComponents(source);
final List<CoordinateSystem> targets =
CoordinateSystems.getSingleComponents(target);
final int count = sources.size();
- if (ellipsoid != null && (count | targets.size()) == 1) {
- final boolean isEllipsoidalSource = (source instanceof
EllipsoidalCS);
- if (isEllipsoidalSource != (target instanceof EllipsoidalCS)) {
- final var context = factory.builder(isEllipsoidalSource ?
GeographicToGeocentric.NAME
- :
GeocentricToGeographic.NAME);
- context.setSourceAxes(source, isEllipsoidalSource? ellipsoid :
null);
- context.setTargetAxes(target, isEllipsoidalSource ? null :
ellipsoid);
- return context.create();
- }
- }
/*
* Current implementation expects the same number of components, in
the same order
* and with the same number of dimensions in each component. A future
version will
@@ -142,40 +212,90 @@ final class CoordinateSystemTransformBuilder extends
MathTransformBuilder {
final int dimension = source.getDimension();
int firstAffectedCoordinate = 0;
for (int i=0; i<count; i++) {
- final CoordinateSystem s = sources.get(i);
- final CoordinateSystem t = targets.get(i);
- final int sd = s.getDimension();
- if (t.getDimension() != sd) {
+ final CoordinateSystem stepSource = sources.get(i);
+ final CoordinateSystem stepTarget = targets.get(i);
+ final int sourceDim = stepSource.getDimension();
+ if (stepTarget.getDimension() != sourceDim) {
result = null;
break;
}
- final MathTransform subTransform =
factory.createPassThroughTransform(
- firstAffectedCoordinate,
- single(s, t),
- dimension - (firstAffectedCoordinate + sd));
+ MathTransform step;
+ try {
+ step = single(stepSource, stepTarget);
+ } catch (IllegalArgumentException | IncommensurableException
e) {
+ throw new
OperationNotFoundException(operationNotFound(stepSource, stepTarget), e);
+ }
+ final int numTrailingCoordinates = dimension -
(firstAffectedCoordinate + sourceDim);
+ step =
factory.createPassThroughTransform(firstAffectedCoordinate, step,
numTrailingCoordinates);
if (result == null) {
- result = subTransform;
+ result = step;
} else {
- result = factory.createConcatenatedTransform(result,
subTransform);
+ result = factory.createConcatenatedTransform(result, step);
}
- firstAffectedCoordinate += sd;
+ firstAffectedCoordinate += sourceDim;
}
}
- // If we couldn't process components separately, try with the compound
CS as a whole.
- if (result == null) {
+ /*
+ * If we couldn't process components separately, try with the CS as a
whole.
+ * It may be a `CompoundCS` (in which case we can still apply axis
swapping)
+ * or it may be standard CS with different number of dimensions.
+ */
+ if (result == null) try {
result = single(source, target);
+ } catch (IllegalArgumentException | IncommensurableException e) {
+ throw new OperationNotFoundException(operationNotFound(source,
target), e);
}
return unique(result);
}
/**
* Implementation of {@code create(…)} for a single component.
- * This implementation can handle changes of coordinate system type between
+ * This implementation can handle changes of coordinate system type
between {@link EllipsoidalCS},
* {@link CartesianCS}, {@link SphericalCS}, {@link CylindricalCS} and
{@link PolarCS}.
+ *
+ * @param stepSource source coordinate system of the step to build.
+ * @param stepTarget target coordinate system of the step to build.
+ * @return transform between the given coordinate systems (never null in
current implementation).
+ * @throws IllegalArgumentException if the <abbr>CS</abbr> are not
compatible, or axes do not match.
+ * @throws IncommensurableException if the units are not compatible, or
the conversion is non-linear.
+ * @throws FactoryException if a factory method failed.
*/
private MathTransform single(final CoordinateSystem stepSource,
- final CoordinateSystem stepTarget) throws
FactoryException
+ final CoordinateSystem stepTarget)
+ throws FactoryException, IncommensurableException
{
+ /*
+ * Cases that require an ellipsoid. All those cases are delegated to
another operation method
+ * in the transform factory. The check for axis order and unit of
measurement will be done by
+ * public methods of the factory, which may invoke this
`CoordinateSystemTransformBuilder`
+ * recursively but with a different pair of coordinate systems.
+ */
+ if (ellipsoid != null) {
+ if (stepSource instanceof EllipsoidalCS) {
+ if (stepTarget instanceof EllipsoidalCS) {
+ return addOrRemoveVertical(stepSource, stepTarget,
Geographic2Dto3D.NAME, Geographic3Dto2D.NAME);
+ }
+ if ((stepTarget instanceof CartesianCS || stepTarget
instanceof SphericalCS)) {
+ final var context =
factory.builder(GeographicToGeocentric.NAME);
+ context.setSourceAxes(stepSource, ellipsoid);
+ context.setTargetAxes(stepTarget, null);
+ return delegate(context);
+ }
+ } else if (stepTarget instanceof EllipsoidalCS) {
+ if ((stepSource instanceof CartesianCS || stepSource
instanceof SphericalCS)) {
+ final var context =
factory.builder(GeocentricToGeographic.NAME);
+ context.setSourceAxes(stepSource, null);
+ context.setTargetAxes(stepTarget, ellipsoid);
+ return delegate(context);
+ }
+ } else if (stepSource instanceof SphericalCS && stepTarget
instanceof SphericalCS) {
+ return addOrRemoveVertical(stepSource, stepTarget,
Spherical2Dto3D.NAME, Spherical3Dto2D.NAME);
+ }
+ }
+ /*
+ * Cases that can be done without ellipsoid. Change of axis order and
unit of measurement
+ * needs to be done here. There is no
`CoordinateSystemTransformBuilder` recursive calls.
+ */
int passthrough = 0;
CoordinateSystemTransform kernel = null;
if (stepSource instanceof CartesianCS) {
@@ -197,32 +317,122 @@ final class CoordinateSystemTransformBuilder extends
MathTransformBuilder {
passthrough = 1;
}
}
- Exception cause = null;
- try {
- if (kernel == null) {
- return
factory.createAffineTransform(CoordinateSystems.swapAndScaleAxes(stepSource,
stepTarget));
- } else if (stepSource.getDimension() ==
kernel.getSourceDimensions() + passthrough &&
- stepTarget.getDimension() ==
kernel.getTargetDimensions() + passthrough)
+ final MathTransform normalized, result;
+ final OperationMethod method;
+ if (kernel == null) {
+ method = Affine.provider();
+ result =
factory.createAffineTransform(CoordinateSystems.swapAndScaleAxes(stepSource,
stepTarget));
+ normalized = result;
+ } else {
+ if (stepSource.getDimension() != kernel.getSourceDimensions() +
passthrough ||
+ stepTarget.getDimension() != kernel.getTargetDimensions() +
passthrough)
{
- final MathTransform tr = (passthrough == 0)
- ? kernel.completeTransform(factory)
- : kernel.passthrough(factory);
- final MathTransform before = factory.createAffineTransform(
- CoordinateSystems.swapAndScaleAxes(stepSource,
- CoordinateSystems.replaceAxes(stepSource,
AxesConvention.NORMALIZED)));
- final MathTransform after = factory.createAffineTransform(
- CoordinateSystems.swapAndScaleAxes(
- CoordinateSystems.replaceAxes(stepTarget,
AxesConvention.NORMALIZED), stepTarget));
- final MathTransform result =
factory.createConcatenatedTransform(before,
-
factory.createConcatenatedTransform(tr, after));
- provider = (passthrough == 0 ? kernel.method :
kernel.method3D);
- return result;
+ throw new
OperationNotFoundException(operationNotFound(stepSource, stepTarget));
+ }
+ final MathTransform before, after;
+ if (passthrough == 0) {
+ method = kernel.method;
+ normalized = kernel.completeTransform(factory);
+ } else {
+ method = kernel.method3D;
+ normalized = kernel.passthrough(factory);
+ }
+ /*
+ * Adjust for axis order an units of measurement.
+ */
+ before = factory.createAffineTransform(
+ CoordinateSystems.swapAndScaleAxes(stepSource,
+ CoordinateSystems.replaceAxes(stepSource,
AxesConvention.NORMALIZED)));
+ after = factory.createAffineTransform(
+ CoordinateSystems.swapAndScaleAxes(
+ CoordinateSystems.replaceAxes(stepTarget,
AxesConvention.NORMALIZED), stepTarget));
+ result = factory.createConcatenatedTransform(before,
+ factory.createConcatenatedTransform(normalized, after));
+ }
+ setParameters(normalized, method, null);
+ return result;
+ }
+
+ /**
+ * Adds or removes the ellipsoidal height or spherical radius dimension.
+ *
+ * @param stepSource source coordinate system of the step to build.
+ * @param stepTarget target coordinate system of the step to build.
+ * @param add the operation method for adding the vertical
dimension.
+ * @param remove the operation method for removing the vertical
dimension.
+ * @return transform adding or removing a vertical coordinate.
+ * @throws IllegalArgumentException if the <abbr>CS</abbr> are not
compatible, or axes do not match.
+ * @throws IncommensurableException if the units are not compatible, or
the conversion is non-linear.
+ * @throws FactoryException if a factory method failed.
+ *
+ * @see
org.apache.sis.referencing.internal.ParameterizedTransformBuilder#addOrRemoveVertical
+ */
+ private MathTransform addOrRemoveVertical(final CoordinateSystem
stepSource,
+ final CoordinateSystem
stepTarget,
+ final String add, final String
remove)
+ throws FactoryException, IncommensurableException
+ {
+ final int change = stepTarget.getDimension() -
stepSource.getDimension();
+ if (change != 0) {
+ final String method = change < 0 ? remove : add;
+ final var context = factory.builder(method);
+ context.setSourceAxes(stepSource, ellipsoid);
+ context.setTargetAxes(stepTarget, ellipsoid);
+ return delegate(context);
+ }
+ // No change in the number of dimensions. Maybe there is axis swapping
and unit conversions.
+ MathTransform step =
factory.createAffineTransform(CoordinateSystems.swapAndScaleAxes(stepSource,
stepTarget));
+ setParameters(step, Affine.provider(), null);
+ return step;
+ }
+
+ /**
+ * Delegates the transform creation to another builder, then remember the
operation method which was used.
+ *
+ * @param context an initialized context on which to invoke the {@code
create()} method.
+ * @return result of {@code context.create()}.
+ * @throws FactoryException if the given context cannot create the
transform.
+ */
+ private MathTransform delegate(final MathTransform.Builder context) throws
FactoryException {
+ final MathTransform step = context.create();
+ setParameters(step, context.getMethod().orElse(null),
context.parameters());
+ return step;
+ }
+
+ /**
+ * Remembers the operation method and parameters for the given transform.
+ *
+ * @param result the transform that has been created.
+ * @param method the method, or {@code null} if unspecified.
+ * @param values the parameter values, or {@code null} for inferring
from the method.
+ */
+ private void setParameters(final MathTransform result, final
OperationMethod method, final ParameterValueGroup values) {
+ final byte type;
+ if (result.isIdentity()) {
+ type = IDENTITY;
+ } else if (MathTransforms.isLinear(result)) {
+ type = LINEAR;
+ } else {
+ type = CONVERSION;
+ }
+ if (parametersType < type) {
+ parametersType = type;
+ provider = method;
+ parameters= values;
+ if (result instanceof Parameterized) {
+ parameterized = (Parameterized) result;
}
- } catch (IllegalArgumentException | IncommensurableException e) {
- cause = e;
}
- throw new
OperationNotFoundException(Resources.format(Resources.Keys.CoordinateOperationNotFound_2,
+ }
+
+ /**
+ * Returns the error message for an operation not found between the
coordinate systems.
+ */
+ private static String operationNotFound(final CoordinateSystem stepSource,
+ final CoordinateSystem stepTarget)
+ {
+ return Resources.format(Resources.Keys.CoordinateOperationNotFound_2,
WKTUtilities.toType(CoordinateSystem.class,
stepSource.getClass()),
- WKTUtilities.toType(CoordinateSystem.class,
stepTarget.getClass())), cause);
+ WKTUtilities.toType(CoordinateSystem.class,
stepTarget.getClass()));
}
}
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
index cf13231218..75f6020366 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
@@ -460,8 +460,8 @@ public class DefaultMathTransformFactory extends
AbstractFactory implements Math
* <ol>
* <li>Inferring the {@code "semi_major"}, {@code "semi_minor"}, {@code
"src_semi_major"},
* {@code "src_semi_minor"}, {@code "tgt_semi_major"} or {@code
"tgt_semi_minor"} parameter values
- * from the {@link Ellipsoid} associated to the source or target
CRS, if these parameters are
- * not explicitly given and if they are relevant for the coordinate
operation method.</li>
+ * from the {@link Ellipsoid} associated to the source or target
<abbr>CRS</abbr>, if these parameters
+ * are not explicitly given and if they are relevant for the
coordinate operation method.</li>
* <li>{@linkplain #createConcatenatedTransform Concatenating} the
parameterized transform
* with any other transforms required for performing units changes
and coordinates swapping.</li>
* </ol>
diff --git
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ParameterizedTransformBuilderTest.java
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ParameterizedTransformBuilderTest.java
index 45f5c4546e..daa49c56bb 100644
---
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ParameterizedTransformBuilderTest.java
+++
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ParameterizedTransformBuilderTest.java
@@ -47,7 +47,7 @@ public final class ParameterizedTransformBuilderTest extends
TestCase {
}
/**
- * Tests {@link
DefaultMathTransformFactory#swapAndScaleAxes(MathTransform,
MathTransformProvider.Context)}
+ * Tests {@link
ParameterizedTransformBuilder#swapAndScaleAxes(MathTransform)}
* with different number of dimensions.
*
* @throws FactoryException if the transform construction failed.
diff --git
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
index be6446c47e..85d7fed458 100644
---
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
+++
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/CoordinateOperationFinderTest.java
@@ -29,6 +29,7 @@ import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.crs.CompoundCRS;
import org.opengis.referencing.crs.DerivedCRS;
+import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.SingleOperation;
@@ -47,6 +48,7 @@ import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.cs.DefaultCartesianCS;
import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis;
import org.apache.sis.referencing.datum.DefaultEngineeringDatum;
+import org.apache.sis.referencing.crs.DefaultGeocentricCRS;
import org.apache.sis.referencing.crs.DefaultEngineeringCRS;
import org.apache.sis.referencing.crs.DefaultCompoundCRS;
import org.apache.sis.referencing.crs.DefaultDerivedCRS;
@@ -663,7 +665,7 @@ public final class CoordinateOperationFinderTest extends
MathTransformTestCase {
*/
@Test
public void testSphericalToProjection() throws ParseException,
FactoryException, TransformException {
- final CoordinateReferenceSystem sourceCRS = parse(
+ final DefaultGeocentricCRS sourceCRS =
assertInstanceOf(DefaultGeocentricCRS.class, parse(
"GEODCRS[\"Mars (2015) / Ocentric\",\n" +
" DATUM[\"Mars (2015)\",\n" +
" ELLIPSOID[\"Mars (2015)\", 3396190, 169.8944472236118,\n"
+
@@ -677,9 +679,9 @@ public final class CoordinateOperationFinderTest extends
MathTransformTestCase {
" AXIS[\"planetocentric longitude (V)\", east,\n" +
" ANGLEUNIT[\"degree\", 0.0174532925199433]],\n" +
" ID[\"IAU\", 49902, 2015],\n" +
- " REMARK[\"Source of IAU Coordinate systems:
doi:10.1007/s10569-017-9805-5\"]]");
+ " REMARK[\"Source of IAU Coordinate systems:
doi:10.1007/s10569-017-9805-5\"]]"));
- final CoordinateReferenceSystem targetCRS = parse(
+ final ProjectedCRS targetCRS = assertInstanceOf(ProjectedCRS.class,
parse(
"PROJCRS[\"Mars (2015) / Ocentric / Equirectangular, clon =
0\",\n" +
" BASEGEODCRS[\"Mars (2015) / Ocentric\",\n" +
" DATUM[\"Mars (2015)\",\n" +
@@ -696,11 +698,23 @@ public final class CoordinateOperationFinderTest extends
MathTransformTestCase {
" LENGTHUNIT[\"metre\", 1]],\n" +
" AXIS[\"Northing (N)\", north,\n" +
" LENGTHUNIT[\"metre\", 1]],\n" +
- " ID[\"IAU\", 49912, 2015]]");
+ " ID[\"IAU\", 49912, 2015]]"));
final CoordinateOperation operation =
finder().createOperation(sourceCRS, targetCRS);
assertSame(sourceCRS, operation.getSourceCRS());
assertSame(targetCRS, operation.getTargetCRS());
+ /*
+ * The result of coordinate operations below have have not been
verified by an external source.
+ * We test "geographic to projected" before to test "spherical to
projected" for verifying that
+ * the "spherical to geographic" conversion has been inserted.
+ */
+ tolerance = 1E-2;
+ transform = targetCRS.getConversionFromBase().getMathTransform();
+ verifyTransform(new double[] {40, 120}, new double[] {7112963.70,
2349260.02});
+
+ transform = operation.getMathTransform();
+ verifyTransform(new double[] {40, 120}, new double[] {7112963.70,
2368936.27});
+ validate();
}