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 2020/08/31 17:35:46 UTC
[sis] 02/02: Add a WraparoundTransform,
to be used in a next commit for resampling images crossing the
anti-meridian.
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
commit 5227d015b8f07ac700a3a098c8ccf0d84664ed7f
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Aug 30 13:35:57 2020 +0200
Add a WraparoundTransform, to be used in a next commit for resampling images crossing the anti-meridian.
---
.../coverage/grid/ResampledGridCoverageTest.java | 2 +-
.../internal/referencing/WraparoundTransform.java | 417 +++++++++++++++++++++
.../referencing/operation/matrix/MatrixSIS.java | 2 +-
.../operation/transform/ConcatenatedTransform.java | 4 +-
.../operation/transform/MathTransforms.java | 5 +-
.../referencing/WraparoundTransformTest.java | 135 +++++++
.../apache/sis/referencing/crs/HardCodedCRS.java | 47 ++-
.../sis/referencing/crs/HardCodedCRSTest.java | 4 +-
.../apache/sis/referencing/cs/HardCodedAxes.java | 9 +-
.../org/apache/sis/referencing/cs/HardCodedCS.java | 9 +-
.../sis/referencing/datum/HardCodedDatum.java | 8 +-
.../sis/test/suite/ReferencingTestSuite.java | 1 +
12 files changed, 624 insertions(+), 19 deletions(-)
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
index 841e08d..64a9a2e 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
@@ -358,7 +358,7 @@ public final strictfp class ResampledGridCoverageTest extends TestCase {
@Test
public void testTemporalAxisMoved() throws TransformException {
final GridCoverage source = createCoverageND(true);
- final GridGeometry target = createGridGeometryND(HardCodedCRS.TIME_WGS84, 1, 2, 3, 0, false);
+ final GridGeometry target = createGridGeometryND(HardCodedCRS.WGS84_4D_TIME_FIRST, 1, 2, 3, 0, false);
final GridCoverage result = resample(source, target);
assertAxisDirectionsEqual("Expected (t,λ,φ,H) axes.",
result.getGridGeometry().getCoordinateReferenceSystem().getCoordinateSystem(),
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundTransform.java
new file mode 100644
index 0000000..7366cf6
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundTransform.java
@@ -0,0 +1,417 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.referencing;
+
+import java.util.List;
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.cs.CoordinateSystemAxis;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.transform.AbstractMathTransform;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.io.wkt.Formatter;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.logging.Logging;
+
+
+/**
+ * Enforces coordinate values in the range of a wraparound axis (typically longitude).
+ * This transform is usually not needed for the [-180 … +180]° range since it is the
+ * range of trigonometric functions. However this transform is useful for shifting
+ * transformation results in the [0 … 360]° range.
+ *
+ * <p>{@code WraparoundTransform}s are not created automatically by {@link org.apache.sis.referencing.CRS#findOperation
+ * CRS.findOperation(…)} because they introduce a discontinuity in coordinate transformations. Such discontinuities are
+ * hurtless when transforming only a cloud of points, but produce undesirable artifacts when transforming geometries.
+ * Callers need to invoke {@link #create create} explicitly if discontinuities are acceptable.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public final class WraparoundTransform extends AbstractMathTransform {
+ /**
+ * The dimension of source and target coordinates.
+ */
+ private final int dimension;
+
+ /**
+ * The dimension where to apply wraparound.
+ */
+ final int wraparoundDimension;
+
+ /**
+ * Creates a new transform with a wraparound behavior in the given dimension.
+ * Input and output values in the wraparound dimension shall be normalized in
+ * the [0 … 1] range.
+ */
+ private WraparoundTransform(final int dimension, final int wraparoundDimension) {
+ this.dimension = dimension;
+ this.wraparoundDimension = wraparoundDimension;
+ }
+
+ /**
+ * Returns an instance with the given number of dimensions while keeping {@link #wraparoundDimension} unchanged.
+ * If no instance can be created for the given number of dimensions, then this method returns {@code null}.
+ */
+ private WraparoundTransform redim(final int n) {
+ if (n == dimension) return this;
+ if (n >= wraparoundDimension) return null;
+ return new WraparoundTransform(n, wraparoundDimension);
+ }
+
+ /**
+ * Returns the transform of the given coordinate operation augmented with a "wrap around" behavior if applicable.
+ * The wraparound is applied on target coordinates and aims to clamp coordinate values inside the range of target
+ * coordinate system axes.
+ *
+ * @param factory the factory to use for creating math transforms.
+ * @param op the coordinate operation for which to get the math transform.
+ * @return the math transform for the given coordinate operation.
+ * @throws FactoryException if an error occurred while creating the math transform.
+ */
+ public static MathTransform create(final MathTransformFactory factory, final CoordinateOperation op)
+ throws FactoryException
+ {
+ MathTransform tr = op.getMathTransform();
+ final CoordinateSystem cs = op.getTargetCRS().getCoordinateSystem();
+ final int dimension = cs.getDimension();
+ for (final int wraparoundDimension : CoordinateOperations.wrapAroundChanges(op)) {
+ final CoordinateSystemAxis axis = cs.getAxis(wraparoundDimension);
+ final MathTransform wraparound = create(factory, dimension, wraparoundDimension,
+ axis.getMinimumValue(), axis.getMaximumValue());
+ tr = factory.createConcatenatedTransform(tr, wraparound);
+ }
+ return tr;
+ }
+
+ /**
+ * Creates a transform with a "wrap around" behavior in the given dimension.
+ *
+ * @param factory the factory to use for creating math transforms.
+ * @param dimension the number of source and target dimensions.
+ * @param wraparoundDimension the dimension where "wrap around" behavior apply.
+ * @param minimum minimal value in the "wrap around" dimension.
+ * @param maximum maximal value in the "wrap around" dimension.
+ * @return the math transform with "wrap around" behavior in the specified dimension.
+ * @throws FactoryException in an error occurred while creating the math transform.
+ */
+ private static MathTransform create(final MathTransformFactory factory, final int dimension,
+ final int wraparoundDimension, final double minimum, final double maximum) throws FactoryException
+ {
+ ArgumentChecks.ensureStrictlyPositive("dimension", dimension);
+ ArgumentChecks.ensureBetween("wraparoundDimension", 0, dimension - 1, wraparoundDimension);
+ NoninvertibleTransformException cause = null;
+ final double span = maximum - minimum;
+ if (span > 0 && span != Double.POSITIVE_INFINITY) {
+ final MatrixSIS m = Matrices.createIdentity(dimension + 1);
+ m.setElement(wraparoundDimension, wraparoundDimension, span);
+ m.setElement(wraparoundDimension, dimension, minimum);
+ final MathTransform denormalize = factory.createAffineTransform(m);
+ final WraparoundTransform wraparound = new WraparoundTransform(dimension, wraparoundDimension);
+ try {
+ return factory.createConcatenatedTransform(denormalize.inverse(),
+ factory.createConcatenatedTransform(wraparound, denormalize));
+ } catch (NoninvertibleTransformException e) {
+ // Matrix is non-invertible only if the range given in argument is illegal.
+ cause = e;
+ }
+ }
+ throw new InvalidGeodeticParameterException(Errors.format(Errors.Keys.IllegalRange_2, minimum, maximum), cause);
+ }
+
+ /**
+ * Gets the dimension of input points.
+ *
+ * @return the dimension of input points.
+ */
+ @Override
+ public int getSourceDimensions() {
+ return dimension;
+ }
+
+ /**
+ * Gets the dimension of output points.
+ *
+ * @return the dimension of output points.
+ */
+ @Override
+ public int getTargetDimensions() {
+ return dimension;
+ }
+
+ /**
+ * Gets the derivative of this transform at a point.
+ */
+ @Override
+ public Matrix derivative(final DirectPosition point) {
+ final MatrixSIS derivative = Matrices.createIdentity(dimension);
+ final double v = point.getOrdinate(wraparoundDimension);
+ if (v == Math.floor(v)) {
+ derivative.setElement(wraparoundDimension, wraparoundDimension, Double.NEGATIVE_INFINITY);
+ }
+ return derivative;
+ }
+
+ /**
+ * Wraparounds a single coordinate point in an array,
+ * and optionally computes the transform derivative at that location.
+ */
+ @Override
+ public Matrix transform(final double[] srcPts, final int srcOff,
+ final double[] dstPts, final int dstOff, final boolean derivate)
+ {
+ double v = srcPts[srcOff];
+ v -= Math.floor(v);
+ if (dstPts != null) {
+ System.arraycopy(srcPts, srcOff, dstPts, dstOff, dimension);
+ dstPts[dstOff + wraparoundDimension] = v;
+ }
+ if (!derivate) {
+ return null;
+ }
+ final MatrixSIS derivative = Matrices.createIdentity(dimension);
+ if (v == 0) {
+ derivative.setElement(wraparoundDimension, wraparoundDimension, Double.NEGATIVE_INFINITY);
+ }
+ return derivative;
+ }
+
+ /**
+ * Transforms many coordinates in a list of ordinal values.
+ */
+ @Override
+ public void transform(final double[] srcPts, int srcOff,
+ final double[] dstPts, int dstOff, int numPts)
+ {
+ System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * dimension);
+ dstOff += wraparoundDimension;
+ while (--numPts >= 0) {
+ dstPts[dstOff] -= Math.floor(dstPts[dstOff]);
+ dstOff += dimension;
+ }
+ }
+
+ /**
+ * Transforms many coordinates in a list of ordinal values.
+ */
+ @Override
+ public void transform(final float[] srcPts, int srcOff,
+ final float[] dstPts, int dstOff, int numPts)
+ {
+ System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * dimension);
+ dstOff += wraparoundDimension;
+ while (--numPts >= 0) {
+ dstPts[dstOff] -= Math.floor(dstPts[dstOff]);
+ dstOff += dimension;
+ }
+ }
+
+ /**
+ * Throws a {@code NoninvertibleTransformException}.
+ * We do not return another {@code WraparoundTransform} for three reasons:
+ *
+ * <ul>
+ * <li>The inverse wraparound would work on a different range of values, but we do not know that range.</li>
+ * <li>Even if we knew the original range of values, creating the inverse transform would require the affine
+ * transforms before and after {@code WraparoundTransform} to be different; it would not be their inverse.
+ * This is impractical, especially since the transform matrices may have been multiplied with other affine
+ * transforms.</li>
+ * <li>Even if we were able to build the inverse {@code WraparoundTransform}, it would not necessarily be
+ * appropriate. For example in "ProjectedCRS → BaseCRS → GeographicCRS" operation chain, wraparound
+ * may happen after the geographic CRS. But in the "GeographicCRS → BaseCRS → ProjectedCRS" inverse
+ * operation, the wraparound would be between BaseCRS and ProjectedCRS, which is often not needed.</li>
+ * </ul>
+ *
+ * We do not return an identity transform because it causes incorrect resampling operation steps when concatenated,
+ * especially when testing if transforms are mutually the inverse of each other.
+ *
+ * @return never return.
+ * @throws NoninvertibleTransformException always thrown.
+ */
+ @Override
+ public MathTransform inverse() throws NoninvertibleTransformException {
+ return super.inverse();
+ }
+
+ /**
+ * Concatenates in an optimized way this math transform with the given one, if possible.
+ */
+ @Override
+ protected MathTransform tryConcatenate(final boolean applyOtherFirst, final MathTransform other,
+ final MathTransformFactory factory) throws FactoryException
+ {
+ /*
+ * If the other transform is also a `WraparoundTransform` for the same dimension,
+ * then there is no need to concatenate those two consecutive redudant transforms.
+ */
+ if (equals(other, null)) {
+ return this;
+ }
+ /*
+ * If two `WraparoundTransform` instances are separated only by a `LinearTransform`,
+ * then that linear transform is moved before or after this `WraparoundTransform`
+ * for increasing the chances to concatenate it using a matrix multiplication.
+ */
+ if (applyOtherFirst) {
+ final List<MathTransform> steps = MathTransforms.getSteps(other);
+ if (steps.size() == 2) {
+ final MathTransform middle = steps.get(1);
+ Matrix matrix = MathTransforms.getMatrix(middle);
+ if (matrix != null) try {
+ MathTransform step2 = this;
+ final MathTransform after = movable(matrix, factory);
+ if (after != null) {
+ /*
+ * Update the middle matrix with everything that we could not put in `after`.
+ * Usually the matrix is square before the multiplication. But if it was not the case,
+ * the new matrix will have the same number of columns (source coordinates) but a new
+ * number of rows (target coordinates). The result should be a square matrix.
+ */
+ final Matrix remaining = Matrices.multiply(MathTransforms.getMatrix(after.inverse()), matrix);
+ final WraparoundTransform redim = redim(remaining.getNumRow() - 1);
+ if (redim != null) {
+ step2 = factory.createConcatenatedTransform(redim, after);
+ matrix = remaining;
+ }
+ }
+ /*
+ * Now look at the non-linear transform. If it is another instance of `WraparoundTransform`,
+ * then we may move the calculation of some coordinates before it.
+ */
+ MathTransform step1 = steps.get(0);
+ if (step1 instanceof WraparoundTransform) {
+ WraparoundTransform wb = (WraparoundTransform) step1;
+ final MathTransform before = wb.movable(matrix, factory);
+ if (before != null) {
+ final Matrix remaining = Matrices.multiply(matrix, MathTransforms.getMatrix(before.inverse()));
+ wb = wb.redim(remaining.getNumCol() - 1);
+ if (wb != null) {
+ step1 = factory.createConcatenatedTransform(before, wb);
+ matrix = remaining;
+ }
+ }
+ }
+ /*
+ * Done moving the linear operations that we can move.
+ * Put everything together.
+ */
+ return factory.createConcatenatedTransform(
+ factory.createConcatenatedTransform(step1,
+ factory.createAffineTransform(matrix)), step2);
+ } catch (NoninvertibleTransformException e) {
+ // Should not happen. But if it is the case, just abandon the optimization effort.
+ Logging.recoverableException(Logging.getLogger(Modules.REFERENCING), getClass(), "tryConcatenate", e);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a transform based on the given matrix but converting only coordinates in dimensions
+ * that can be processed indifferently before or after this {@code WraparoundTransform}.
+ *
+ * @param matrix the matrix to analyze.
+ * @param factory the factory given to {@link #tryConcatenate tryConcatenate(…)}.
+ * @return a transform processing only the movable parts, or {@code null} if identity.
+ */
+ private MathTransform movable(Matrix matrix, final MathTransformFactory factory) throws FactoryException {
+ long canMoveAfter = Numerics.bitmask(dimension) - 1;
+ canMoveAfter &= ~Numerics.bitmask(wraparoundDimension);
+ /*
+ * If any matrix row (output coordinate) uses the wraparound dimension as input,
+ * then we can not move that row because the coordinate value may not be the same
+ * after execution of `WraparoundTransform`.
+ */
+ if (matrix.getNumCol() - 1 > wraparoundDimension) {
+ for (int j = matrix.getNumRow(); --j >= 0;) {
+ if (matrix.getElement(j, wraparoundDimension) != 0) {
+ canMoveAfter &= ~Numerics.bitmask(j);
+ }
+ }
+ }
+ if (canMoveAfter != 0) {
+ /*
+ * Create a matrix which will convert coordinates in all dimensions
+ * that we can process before or after this `WraparoundTransform`.
+ * We start with a copy and set to identity the rows that we can not move.
+ */
+ matrix = Matrices.copy(matrix);
+ for (int j = matrix.getNumRow() - 1; --j >=0;) {
+ if ((canMoveAfter & Numerics.bitmask(j)) == 0) {
+ for (int i=matrix.getNumCol(); --i >= 0;) {
+ matrix.setElement(j, i, (i == j) ? 1 : 0);
+ }
+ }
+ }
+ }
+ if (!matrix.isIdentity()) {
+ return factory.createAffineTransform(matrix);
+ }
+ return null;
+ }
+
+ /**
+ * Formats this transform as a pseudo-WKT element.
+ *
+ * @param formatter the formatter to use.
+ * @return the WKT element name, which is {@code "Wraparound_MT"}.
+ */
+ @Override
+ protected String formatTo(final Formatter formatter) {
+ formatter.append(wraparoundDimension);
+ formatter.setInvalidWKT(WraparoundTransform.class, null);
+ return "Wraparound_MT";
+ }
+
+ /**
+ * Compares this transform with the given object for equality.
+ *
+ * @param object the object to compare with this transform.
+ * @param mode ignored, can be {@code null}.
+ */
+ @Override
+ public boolean equals(final Object object, final ComparisonMode mode) {
+ if (object instanceof WraparoundTransform) {
+ final WraparoundTransform other = (WraparoundTransform) object;
+ return other.dimension == dimension && other.wraparoundDimension == wraparoundDimension;
+ }
+ return false;
+ }
+
+ /**
+ * Computes a hash code value for this transform.
+ */
+ @Override
+ protected int computeHashCode() {
+ return dimension * 31 + wraparoundDimension;
+ }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/MatrixSIS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
index 42542bd..5500dda 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
@@ -548,7 +548,7 @@ public abstract class MatrixSIS implements Matrix, LenientComparable, Cloneable,
* <h4>Relationship with coordinate operations</h4>
* In the context of coordinate operations, {@code Matrix.multiply(other)} is equivalent to
* <code>{@linkplain AffineTransform#concatenate AffineTransform.concatenate}(other)</code>:
- * first transforms by the supplied transform and then transform the result by the original transform.
+ * first transforms by the {@code other} transform and then transform the result by {@code this} transform.
*
* @param matrix the matrix to multiply to this matrix.
* @return the result of {@code this} × {@code matrix}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
index 801919d..bc6c9d2 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
@@ -903,8 +903,8 @@ class ConcatenatedTransform extends AbstractMathTransform implements Serializabl
}
}
/*
- * Do not invoke super.tryConcatenate(applyOtherFirst, other, factory); the test of whether 'this'
- * is the inverse of 'other' has been done indirectly by the calls to 'createOptimized'.
+ * Do not invoke super.tryConcatenate(applyOtherFirst, other, factory); the test of whether `this`
+ * is the inverse of `other` has been done indirectly by the calls to `createOptimized(…)`.
*/
return null;
}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
index 356de2f..425051d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@ -571,8 +571,9 @@ public final class MathTransforms extends Static {
*
* <ul>
* <li>If {@code transform} is {@code null}, returns an empty list.</li>
- * <li>Otherwise if {@code transform} is the result of a call to a {@code concatenate(…)} method,
- * returns all components. All nested concatenated transforms (if any) will be flattened.</li>
+ * <li>Otherwise if {@code transform} is the result of calls to {@code concatenate(…)} methods, returns
+ * all steps making the transformation chain. Nested concatenated transforms (if any) are flattened.
+ * Note that some steps may have have been merged together, resulting in a shorter list.</li>
* <li>Otherwise returns the given transform in a list of size 1.</li>
* </ul>
*
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundTransformTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundTransformTest.java
new file mode 100644
index 0000000..492d2a5
--- /dev/null
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundTransformTest.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.referencing;
+
+import java.util.List;
+import java.util.Collections;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.referencing.crs.HardCodedCRS;
+import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.matrix.Matrix3;
+import org.apache.sis.referencing.operation.matrix.Matrix4;
+import org.apache.sis.referencing.operation.matrix.NoninvertibleMatrixException;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.opengis.test.Assert.*;
+
+
+/**
+ * Tests {@link WraparoundTransform}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public final strictfp class WraparoundTransformTest extends TestCase {
+ /**
+ * Tests wraparound on one axis.
+ *
+ * @throws FactoryException if the transform can not be created.
+ * @throws NoninvertibleMatrixException if the expected matrix can not be inverted.
+ */
+ @Test
+ public void testOneAxis() throws FactoryException, NoninvertibleMatrixException {
+ final AbstractCoordinateOperation op = new AbstractCoordinateOperation(
+ Collections.singletonMap(AbstractCoordinateOperation.NAME_KEY, "Wrapper"),
+ HardCodedCRS.WGS84_φλ.forConvention(AxesConvention.POSITIVE_RANGE),
+ HardCodedCRS.WGS84_φλ, null, MathTransforms.scale(3, 5));
+ /*
+ * Transform should be [scale & normalization] → [wraparound] → [denormalization].
+ * The wraparound is applied on target coordinates, which is why it appears after [scale].
+ * Wrararound is often (but not always) unnecessary on source coordinates if the operation
+ * uses trigonometric functions.
+ */
+ final MathTransform wt = WraparoundTransform.create(DefaultFactories.forClass(MathTransformFactory.class), op);
+ final List<MathTransform> steps = MathTransforms.getSteps(wt);
+ assertEquals(3, steps.size());
+ assertEquals(1, ((WraparoundTransform) steps.get(1)).wraparoundDimension);
+ /*
+ * Wraparound outputs are in [0 … 1) range (0 inclusive and 1 exclusive), so we expect a
+ * multiplication by the span of each axis for getting the final range.
+ */
+ assertMatrixEquals("denormalize", new Matrix3(
+ 1, 0, 0, // Latitude (no wrap around)
+ 0, 360, -180, // Longitude in [-180 … 180) range.
+ 0, 0, 1),
+ MathTransforms.getMatrix(steps.get(2)), STRICT);
+ /*
+ * The normalization is the inverse of above matrix (when source and target axes have the same span).
+ * But we expect the normalization matrix to be concatenated with the (3, 2, 5) scale operation.
+ */
+ assertMatrixEquals("normalize", new Matrix3(
+ 3, 0, 0, // 3 is a factor in MathTransforms.scale(…).
+ 0, 5./360, -(-180./360), // 5 is (idem).
+ 0, 0, 1),
+ MathTransforms.getMatrix(steps.get(0)), 1E-15);
+ }
+
+ /**
+ * Tests wraparound on two axes. We expects two instances of {@link WraparoundTransform} without linear
+ * transform between them. The absence of separation between the two {@link WraparoundTransform}s is an
+ * indirect test of {@link WraparoundTransform#tryConcatenate(boolean, MathTransform, MathTransformFactory)}.
+ *
+ * @throws FactoryException if the transform can not be created.
+ * @throws NoninvertibleMatrixException if the expected matrix can not be inverted.
+ */
+ @Test
+ public void testTwoAxes() throws FactoryException, NoninvertibleMatrixException {
+ final AbstractCoordinateOperation op = new AbstractCoordinateOperation(
+ Collections.singletonMap(AbstractCoordinateOperation.NAME_KEY, "Wrapper"),
+ HardCodedCRS.WGS84_3D_TIME.forConvention(AxesConvention.POSITIVE_RANGE),
+ HardCodedCRS.WGS84_3D_TIME_CYCLIC, null, MathTransforms.scale(3, 2, 5));
+ /*
+ * Transform should be [scale & normalization] → [wraparound 1] → [wraparound 2] → [denormalization].
+ * At first an affine transform existed between the two [wraparound] operations, but that affine transform
+ * should have been moved by `WraparoundTransform.tryConcatenate(…)` in order to combine them with initial
+ * [normalization} and final {denormalization].
+ */
+ final MathTransform wt = WraparoundTransform.create(DefaultFactories.forClass(MathTransformFactory.class), op);
+ final List<MathTransform> steps = MathTransforms.getSteps(wt);
+ assertEquals(4, steps.size());
+ assertEquals(0, ((WraparoundTransform) steps.get(1)).wraparoundDimension);
+ assertEquals(2, ((WraparoundTransform) steps.get(2)).wraparoundDimension);
+ /*
+ * Wraparound outputs are in [0 … 1) range (0 inclusive and 1 exclusive), so we expect a
+ * multiplication by the span of each axis for getting the final range.
+ */
+ assertMatrixEquals("denormalize", new Matrix4(
+ 360, 0, 0, -180, // Longitude in [-180 … 180) range.
+ 0, 1, 0, 0, // Latitude (no wrap around)
+ 0, 0, 365, 1, // Day of year in [1 … 366) range.
+ 0, 0, 0, 1),
+ MathTransforms.getMatrix(steps.get(3)), STRICT);
+ /*
+ * The normalization is the inverse of above matrix (when source and target axes have the same span).
+ * But we expect the normalization matrix to be concatenated with the (3, 2, 5) scale operation.
+ */
+ assertMatrixEquals("normalize", new Matrix4(
+ 3./360, 0, 0, -(-180./360), // 3 is a factor in MathTransforms.scale(…).
+ 0, 2, 0, 0, // 2 is (idem).
+ 0, 0, 5./365, -(1./365), // 5 is (idem).
+ 0, 0, 0, 1),
+ MathTransforms.getMatrix(steps.get(0)), 1E-15);
+ }
+}
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRS.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRS.java
index 65bc107..f37bf16 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRS.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRS.java
@@ -83,11 +83,36 @@ public final strictfp class HardCodedCRS {
* with the 3 first dimensions specified by {@link #WGS84_3D} and the fourth dimension specified
* by {@link #TIME}.
*
- * @see #TIME_WGS84
+ * @see #WGS84_4D_TIME_FIRST
*/
public static final DefaultCompoundCRS WGS84_4D;
/**
+ * A (λ,φ,t) CRS where <var>t</var> is the {@link #TIME}.
+ *
+ * @since 1.1
+ */
+ public static final DefaultCompoundCRS WGS84_3D_TIME;
+
+ /**
+ * A (λ,φ,t) CRS where <var>t</var> is the {@link #DAY_OF_YEAR}.
+ * This CRS has two wraparound axes: <var>λ</var> and <var>t</var>.
+ *
+ * @since 1.1
+ */
+ public static final DefaultCompoundCRS WGS84_3D_TIME_CYCLIC;
+
+ /**
+ * A four-dimensional geographic coordinate reference system with time as the first axis.
+ * This CRS uses (<var>time</var>, <var>longitude</var>, <var>latitude</var>, <var>height</var>)
+ * with the first dimension specified by {@link #TIME} and the 3 last dimensions specified by {@link #WGS84_3D}.
+ * Such axis order is unusual but we use it as a way to verify that SIS is robust to arbitrary axis order.
+ *
+ * @since 1.1
+ */
+ public static final DefaultCompoundCRS WGS84_4D_TIME_FIRST;
+
+ /**
* A two-dimensional geographic coordinate reference system using the Paris prime meridian.
* This CRS uses (<var>longitude</var>, <var>latitude</var>) coordinates with longitude values
* increasing towards the East and latitude values increasing towards the North.
@@ -243,17 +268,23 @@ public final strictfp class HardCodedCRS {
public static final DefaultTemporalCRS TIME = new DefaultTemporalCRS(
getProperties(HardCodedCS.DAYS), HardCodedDatum.MODIFIED_JULIAN, HardCodedCS.DAYS);
+ static {
+ WGS84_4D = new DefaultCompoundCRS(properties("WGS 84 (3D) + time", null), WGS84_3D, TIME);
+ WGS84_4D_TIME_FIRST = new DefaultCompoundCRS(properties("time + WGS 84 (3D)", null), TIME, WGS84_3D);
+ }
+
/**
- * A four-dimensional geographic coordinate reference system with time as the first axis.
- * This CRS uses (<var>time</var>, <var>longitude</var>, <var>latitude</var>, <var>height</var>)
- * with the first dimension specified by {@link #TIME} and the 3 last dimensions specified by {@link #WGS84_3D}.
- * Such axis order is unusual but we use it as a way to verify that SIS is robust to arbitrary axis order.
+ * A parametric CRS for day of year, without any particular year.
+ * The axis is cyclic: after day 365 we restart at day 1.
+ *
+ * @since 1.1
*/
- public static final DefaultCompoundCRS TIME_WGS84 = new DefaultCompoundCRS(
- properties("time + WGS 84 (3D)", null), TIME, WGS84_3D);;
+ public static final DefaultParametricCRS DAY_OF_YEAR = new DefaultParametricCRS(
+ getProperties(HardCodedCS.DAY_OF_YEAR), HardCodedDatum.DAY_OF_YEAR, HardCodedCS.DAY_OF_YEAR);
static {
- WGS84_4D = new DefaultCompoundCRS(properties("WGS 84 (3D) + time", null), WGS84_3D, TIME);
+ WGS84_3D_TIME = new DefaultCompoundCRS(properties("WGS 84 + time", null), WGS84, TIME);
+ WGS84_3D_TIME_CYCLIC = new DefaultCompoundCRS(properties("WGS 84 + day of year", null), WGS84, DAY_OF_YEAR);
}
/**
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRSTest.java
index 609ac38..48440de 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRSTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/HardCodedCRSTest.java
@@ -66,13 +66,13 @@ public final strictfp class HardCodedCRSTest extends TestCase {
assertEquals("TIME", 1, TIME .getCoordinateSystem().getDimension());
assertEquals("DEPTH", 1, DEPTH .getCoordinateSystem().getDimension());
assertEquals("WGS84", 2, WGS84 .getCoordinateSystem().getDimension());
- assertEquals("WGS84_φλ", 2, WGS84_φλ .getCoordinateSystem().getDimension());
+ assertEquals("WGS84_φλ", 2, WGS84_φλ .getCoordinateSystem().getDimension());
assertEquals("WGS84_3D", 3, WGS84_3D .getCoordinateSystem().getDimension());
assertEquals("CARTESIAN_2D", 2, CARTESIAN_2D.getCoordinateSystem().getDimension());
assertEquals("CARTESIAN_3D", 3, CARTESIAN_3D.getCoordinateSystem().getDimension());
assertEquals("GEOCENTRIC", 3, GEOCENTRIC .getCoordinateSystem().getDimension());
assertEquals("SPHERICAL", 3, SPHERICAL .getCoordinateSystem().getDimension());
- assertEquals("GEOID_4D", 4, GEOID_4D .getCoordinateSystem().getDimension());
+ assertEquals("GEOID_4D", 4, GEOID_4D .getCoordinateSystem().getDimension());
}
/**
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedAxes.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedAxes.java
index ac65e53..a6f2754 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedAxes.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedAxes.java
@@ -32,7 +32,7 @@ import org.apache.sis.measure.Units;
* Consequently EPSG codes are not provided.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
* @since 0.4
* @module
*/
@@ -432,6 +432,13 @@ public final strictfp class HardCodedAxes {
AxisDirection.FUTURE, Units.DAY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, null);
/**
+ * Axis for time values in a {@linkplain org.apache.sis.referencing.cs.DefaultParametricCS parametric CS}.
+ * The axis is cyclic: after day 365 we restart at day 1.
+ */
+ public static final DefaultCoordinateSystemAxis DAY_OF_YEAR = create("Day of year", "t",
+ AxisDirection.FUTURE, Units.DAY, 1, 366, RangeMeaning.WRAPAROUND);
+
+ /**
* Axis for column indices in a {@linkplain org.apache.sis.coverage.grid.GridCoverage grid coverage}.
* Increasing values go toward {@linkplain AxisDirection#COLUMN_POSITIVE positive column number}.
* The abbreviation is lower case <cite>"i"</cite>.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedCS.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedCS.java
index efb6957..c5ecf4c 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedCS.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedCS.java
@@ -29,7 +29,7 @@ import static org.apache.sis.referencing.IdentifiedObjects.getProperties;
* Collection of coordinate systems for testing purpose.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.1
* @since 0.4
* @module
*/
@@ -256,6 +256,13 @@ public final strictfp class HardCodedCS {
getProperties(HardCodedAxes.DEPTH), HardCodedAxes.DEPTH);
/**
+ * A parametric CRS for day of year, without any particular year.
+ * The axis is cyclic: after day 365 we restart at day 1.
+ */
+ public static final DefaultParametricCS DAY_OF_YEAR = new DefaultParametricCS(
+ getProperties(HardCodedAxes.DAY_OF_YEAR), HardCodedAxes.DAY_OF_YEAR);
+
+ /**
* A one-dimensional temporal CS with
* <var>{@linkplain HardCodedAxes#TIME time}</var>,
* axis in {@linkplain Units#DAY day} units.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/datum/HardCodedDatum.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/datum/HardCodedDatum.java
index 0dee2dd..5e19b08 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/datum/HardCodedDatum.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/datum/HardCodedDatum.java
@@ -34,7 +34,7 @@ import static org.apache.sis.internal.util.StandardDateFormat.MILLISECONDS_PER_D
* Collection of datum for testing purpose.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.1
* @since 0.4
* @module
*/
@@ -153,6 +153,12 @@ public final strictfp class HardCodedDatum {
new Date(-40587L * MILLISECONDS_PER_DAY));
/**
+ * A parametric datum for day of year, without any particular year.
+ */
+ public static final DefaultParametricDatum DAY_OF_YEAR = new DefaultParametricDatum(
+ properties("Day of year", null, null));
+
+ /**
* Image with {@link PixelInCell#CELL_CENTER}.
*/
public static final DefaultImageDatum IMAGE = new DefaultImageDatum(
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
index 438c672..981f88e 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
@@ -148,6 +148,7 @@ import org.junit.BeforeClass;
org.apache.sis.referencing.operation.DefaultFormulaTest.class,
org.apache.sis.referencing.operation.DefaultOperationMethodTest.class,
org.apache.sis.referencing.operation.transform.OperationMethodSetTest.class,
+ org.apache.sis.internal.referencing.WraparoundTransformTest.class,
// Registration of map projections and other math transforms.
org.apache.sis.internal.referencing.provider.AffineTest.class,