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 <[email protected]> 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,
