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 242b57a25f7b5215f93ca911f411c8487fa183d0
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Nov 25 16:02:18 2023 +0100

    Add convenience method for adding a vertical and temporal dimensions to a 
grid coverage.
---
 .../sis/coverage/grid/DimensionAppender.java       |  60 +++++++---
 .../sis/coverage/grid/DimensionalityReduction.java |  15 ++-
 .../sis/coverage/grid/GridCoverageProcessor.java   |  58 +++++++++
 .../org/apache/sis/coverage/grid/GridExtent.java   |  25 ++++
 .../sis/coverage/grid/DimensionAppenderTest.java   | 131 +++++++++++++++++++++
 .../coverage/grid/DimensionalityReductionTest.java |   4 +-
 .../main/org/apache/sis/referencing/CommonCRS.java |  11 ++
 .../main/org/apache/sis/util/ArraysExt.java        |  19 +++
 .../org/apache/sis/util/internal/Numerics.java     |  14 +++
 9 files changed, 317 insertions(+), 20 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionAppender.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionAppender.java
index 4deb4f4a1c..d3fac44fe9 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionAppender.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionAppender.java
@@ -21,7 +21,6 @@ import org.opengis.util.FactoryException;
 import org.apache.sis.image.DataType;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.Workaround;
 import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
@@ -50,12 +49,12 @@ final class DimensionAppender extends GridCoverage {
 
     /**
      * Creates a new dimension appender for the given grid coverage.
-     * The grid extent of {@code dimToAdd} shall have a grid size of one cell 
in all dimensions.
+     * This constructor does not verify the grid geometry validity.
+     * It is caller's responsibility to verify that the size is 1 cell.
      *
      * @param  source    the source grid coverage for which to append extra 
dimensions.
      * @param  dimToAdd  the dimensions to add to the source grid coverage.
      * @throws FactoryException if the compound CRS cannot be created.
-     * @throws IllegalGridGeometryException if a dimension has more than one 
grid cell.
      * @throws IllegalArgumentException if the concatenation results in 
duplicated
      *         {@linkplain GridExtent#getAxisType(int) grid axis types}.
      */
@@ -63,22 +62,32 @@ final class DimensionAppender extends GridCoverage {
         super(source, new GridGeometry(source.getGridGeometry(), dimToAdd));
         this.source   = source;
         this.dimToAdd = dimToAdd;
+    }
+
+    /**
+     * Creates a grid coverage augmented with the given dimensions.
+     * The grid extent of {@code dimToAdd} shall have a grid size of one cell 
in all dimensions.
+     *
+     * @param  source    the source grid coverage for which to append extra 
dimensions.
+     * @param  dimToAdd  the dimensions to add to the source grid coverage.
+     * @throws FactoryException if the compound CRS cannot be created.
+     * @throws IllegalGridGeometryException if a dimension has more than one 
grid cell.
+     * @throws IllegalArgumentException if the concatenation results in 
duplicated
+     *         {@linkplain GridExtent#getAxisType(int) grid axis types}.
+     */
+    static GridCoverage create(GridCoverage source, GridGeometry dimToAdd) 
throws FactoryException {
         final GridExtent extent = dimToAdd.getExtent();
-        for (int i = extent.getDimension(); --i >= 0;) {
-            final long size = extent.getSize(i);
+        int i = extent.getDimension();
+        if (i == 0) {
+            return source;
+        }
+        do {
+            final long size = extent.getSize(--i);
             if (size != 1) {
                 throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.NotASlice_2,
                         extent.getAxisIdentification(i,i), size));
             }
-        }
-    }
-
-    /**
-     * Work around for RFE #4093999 in Sun's bug database
-     * ("Relax constraint on placement of this()/super() call in 
constructors").
-     */
-    @Workaround(library="JDK", version="1.7")
-    static DimensionAppender create(GridCoverage source, GridGeometry 
dimToAdd) throws FactoryException {
+        } while (i != 0);
         if (source instanceof DimensionAppender) {
             final var a = (DimensionAppender) source;
             dimToAdd = new GridGeometry(a.dimToAdd, dimToAdd);
@@ -87,6 +96,29 @@ final class DimensionAppender extends GridCoverage {
         return new DimensionAppender(source, dimToAdd);
     }
 
+    /**
+     * Returns a grid coverage with a subset of the grid dimensions, or {@code 
null} if not possible by this method.
+     *
+     * @param  gridAxesToPass  the grid dimensions to keep. Indices must be in 
strictly increasing order.
+     * @return a grid coverage with the specified dimensions, or {@code null}.
+     * @throws FactoryException if the compound CRS cannot be created.
+     */
+    final GridCoverage selectDimensions(final int[] gridAxesToPass) throws 
FactoryException {
+        final int sourceDim = source.getGridGeometry().getDimension();
+        final int dimension = gridAxesToPass.length;
+        if (dimension < sourceDim || gridAxesToPass[0] != 0 || 
gridAxesToPass[sourceDim - 1] != sourceDim - 1) {
+            return null;
+        }
+        if (dimension == sourceDim) {
+            return source;
+        }
+        final int[] selected = new int[dimension - sourceDim];
+        for (int i=sourceDim; i<dimension; i++) {
+            selected[i - sourceDim] = gridAxesToPass[i] - sourceDim;
+        }
+        return create(source, dimToAdd.selectDimensions(selected));
+    }
+
     /**
      * Returns the data type identifying the primitive type used for storing 
sample values in each band.
      */
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionalityReduction.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionalityReduction.java
index 03bca8b31f..71a07187a7 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionalityReduction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionalityReduction.java
@@ -68,7 +68,7 @@ import org.opengis.coverage.PointOutsideCoverageException;
  *
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   1.4
  */
 public class DimensionalityReduction implements UnaryOperator<GridCoverage>, 
Serializable {
@@ -171,7 +171,7 @@ public class DimensionalityReduction implements 
UnaryOperator<GridCoverage>, Ser
      * @param  source    the grid geometry on which to select a subset of its 
grid dimensions.
      * @param  gridAxes  bitmask of indices of source grid dimensions to keep 
in the reduced grid.
      *                   Will be modified by this constructor for internal 
purpose.
-     * @param  factory   the factory to use for creating new math transforms, 
or {@code null} if none.
+     * @param  factory   the factory to use for creating new math transforms, 
or {@code null} for the default.
      * @throws FactoryException if the dimensions to keep cannot be separated 
from the dimensions to omit.
      */
     protected DimensionalityReduction(final GridGeometry source, final BitSet 
gridAxes, final MathTransformFactory factory)
@@ -262,7 +262,7 @@ public class DimensionalityReduction implements 
UnaryOperator<GridCoverage>, Ser
      * @param  gridAxesToRemove  the dimensions on which to operate.
      * @param  bitset   same as {@link gridAxesToRemove} but as a bit set (for 
efficiency).
      * @param  anchor   whether to compute the transform for pixel corner or 
pixel center.
-     * @param  factory  the factory to use for creating new math transforms, 
or {@code null} if none.
+     * @param  factory  the factory to use for creating new math transforms, 
or {@code null} for the default.
      */
     private MathTransform filterGridToCRS(final int[] gridAxesToRemove, final 
BitSet bitset, final PixelInCell anchor,
             final MathTransformFactory factory) throws FactoryException
@@ -734,7 +734,14 @@ public class DimensionalityReduction implements 
UnaryOperator<GridCoverage>, Ser
     public GridCoverage apply(final GridCoverage source) {
         ArgumentChecks.ensureNonNull("source", source);
         ensureSameAxes(sourceGeometry.extent, source.getGridGeometry().extent);
-        return isIdentity() ? source : new ReducedGridCoverage(source, this);
+        if (isIdentity()) return source;
+        if (source instanceof DimensionAppender) try {
+            GridCoverage c = ((DimensionAppender) 
source).selectDimensions(gridAxesToPass);
+            if (c != null) return c;
+        } catch (FactoryException e) {
+            throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.NonSeparableReducedDimensions,
 e));
+        }
+        return new ReducedGridCoverage(source, this);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index d61f6a4b18..d7194127b1 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -21,6 +21,8 @@ import java.util.Set;
 import java.util.EnumSet;
 import java.util.Objects;
 import java.util.function.Function;
+import java.time.Instant;
+import java.time.Duration;
 import java.lang.reflect.Field;
 import java.lang.reflect.InaccessibleObjectException;
 import java.awt.Shape;
@@ -28,6 +30,7 @@ import java.awt.Rectangle;
 import java.awt.image.RenderedImage;
 import javax.measure.Quantity;
 import org.opengis.util.FactoryException;
+import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.MathTransform;
@@ -44,9 +47,13 @@ import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.image.Interpolation;
 import org.apache.sis.coverage.internal.SampleDimensions;
 import org.apache.sis.coverage.internal.MultiSourceArgument;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.crs.DefaultTemporalCRS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.collection.WeakHashSet;
+import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.util.internal.UnmodifiableArrayList;
 import org.apache.sis.measure.NumberRange;
 
@@ -645,6 +652,57 @@ public class GridCoverageProcessor implements Cloneable {
         }
     }
 
+    /**
+     * Appends a single grid dimension after the dimensions of the given 
source coverage.
+     * This method is typically invoked for adding a vertical axis to a 
two-dimensional coverage.
+     * The default implementation delegates to {@link 
#appendDimensions(GridCoverage, GridGeometry)}.
+     *
+     * @param  source  the source on which to append a dimension.
+     * @param  lower   lower coordinate value of the slice, in units of the 
CRS.
+     * @param  span    size of the slice, in units of the CRS.
+     * @param  crs     coordinate reference system of the slice, or {@code 
null} if unknown.
+     * @return a coverage with the specified dimension added.
+     * @throws IllegalGridGeometryException if the compound CRS or compound 
extent cannot be created.
+     *
+     * @since 1.5
+     */
+    public GridCoverage appendDimension(final GridCoverage source, double 
lower, final double span, final CoordinateReferenceSystem crs) {
+        /*
+         * Choose a cell index such as the translation term in the matrix will 
be as close as possible to zero.
+         * Reducing the magnitude of additions with IEEE 754 arithmetic can 
help to reduce rounding errors.
+         * It also has the desirable side-effect to increase the chances that 
slices share the same
+         * "grid to CRS" transform.
+         */
+        final long index = Numerics.roundAndClamp(lower / span);
+        final long[] indices = new long[] {index};
+        final GridExtent extent = new GridExtent(GridExtent.typeFromAxes(crs, 
1), indices, indices, true);
+        final MathTransform gridToCRS = MathTransforms.linear(span, 
Math.fma(index, -span, lower));
+        return appendDimensions(source, new GridGeometry(extent, 
PixelInCell.CELL_CORNER, gridToCRS, crs));
+    }
+
+    /**
+     * Appends a temporal grid dimension after the dimensions of the given 
source coverage.
+     * The default implementation delegates to {@link 
#appendDimensions(GridCoverage, GridGeometry)}.
+     *
+     * @param  source  the source on which to append a temporal dimension.
+     * @param  lower   start time of the slice.
+     * @param  span    duration of the slice.
+     * @return a coverage with the specified temporal dimension added.
+     * @throws IllegalGridGeometryException if the compound CRS or compound 
extent cannot be created.
+     *
+     * @since 1.5
+     */
+    public GridCoverage appendDimension(final GridCoverage source, final 
Instant lower, final Duration span) {
+        final DefaultTemporalCRS crs = 
DefaultTemporalCRS.castOrCopy(CommonCRS.Temporal.TRUNCATED_JULIAN.crs());
+        double scale  = crs.toValue(span);
+        double offset = crs.toValue(lower);
+        long   index  = Numerics.roundAndClamp(offset / scale);             // 
See comment in above method.
+        offset = crs.toValue(lower.minus(span.multipliedBy(index)));
+        final GridExtent extent = new GridExtent(DimensionNameType.TIME, 
index, index, true);
+        final MathTransform gridToCRS = MathTransforms.linear(scale, offset);
+        return appendDimensions(source, new GridGeometry(extent, 
PixelInCell.CELL_CORNER, gridToCRS, crs));
+    }
+
     /**
      * Automatically reduces a grid coverage dimensionality by removing all 
grid axes with an extent size of 1.
      * Axes in the reduced grid coverage will be in the same order than in the 
source coverage.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
index 62d68bb38b..d996db4595 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
@@ -300,6 +300,31 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
         }
     }
 
+    /**
+     * Constructs a one-dimensional grid extent set to the specified 
coordinates.
+     * This convenience constructor does the same work than the constructor 
for the
+     * {@linkplain 
#GridExtent(org.opengis.metadata.spatial.DimensionNameType[], long[], long[], 
boolean) general case}.
+     * It is provided as a convenience for {@linkplain #GridExtent(GridExtent, 
GridExtent) appending a single dimension}
+     * to an existing grid.
+     *
+     * @param  axisType        the type of the grid axis, or {@code null} if 
unspecified.
+     * @param  low             the valid minimum grid coordinate, always 
inclusive.
+     * @param  high            the valid maximum grid coordinate, inclusive or 
exclusive depending on the next argument.
+     * @param  isHighIncluded  {@code true} if the {@code high} value is 
inclusive (as in ISO 19123 specification),
+     *                         or {@code false} if it is exclusive (as in 
Java2D usage).
+     * @throws IllegalArgumentException if {@code low} is greater than {@code 
high}.
+     *
+     * @since 1.5
+     */
+    public GridExtent(final DimensionNameType axisType, final long low, long 
high, final boolean isHighIncluded) {
+        if (!isHighIncluded) {
+            high = Math.decrementExact(high);
+        }
+        coordinates = new long[] {low, high};
+        types = validateAxisTypes(new DimensionNameType[] {axisType});
+        validateCoordinates();
+    }
+
     /**
      * Constructs a new grid extent set to the specified coordinates.
      * The given arrays contain a minimum (inclusive) and maximum value for 
each dimension of the grid coverage.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionAppenderTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionAppenderTest.java
new file mode 100644
index 0000000000..b3e40a1eec
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionAppenderTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.coverage.grid;
+
+import java.time.Instant;
+import java.time.Duration;
+import java.awt.image.BufferedImage;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.matrix.Matrix3;
+import org.apache.sis.referencing.operation.matrix.Matrix4;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.util.ArraysExt;
+
+// Test dependencies
+import org.junit.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.referencing.crs.HardCodedCRS;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import static org.opengis.test.Assert.assertMatrixEquals;
+
+
+/**
+ * Tests {@link DimensionAppender}. This is partially the converse of {@link 
DimensionalityReductionTest}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class DimensionAppenderTest extends TestCase {
+    /**
+     * Creates a new test case.
+     */
+    public DimensionAppenderTest() {
+    }
+
+    /**
+     * Returns a grid coverage to use as a starting point.
+     *
+     * @param  width   image width in pixels.
+     * @param  height  image height in pixels.
+     */
+    private static GridCoverage initial(final int width, final int height) {
+        var extent = new GridExtent(width, height);
+        var gridToCRS = new Matrix3(
+                4, 0, 100,
+                0, 3, -20,
+                0, 0,   1);
+
+        var gg = new GridGeometry(extent, PixelInCell.CELL_CORNER, 
MathTransforms.linear(gridToCRS), HardCodedCRS.WGS84);
+        return new GridCoverage2D(gg, null, new BufferedImage(width, height, 
BufferedImage.TYPE_BYTE_BINARY));
+    }
+
+    /**
+     * Asserts that the grid geometry of the given coverage has the expected 
properties.
+     *
+     * @param actual        the coverage for which to verify the grid geometry.
+     * @param gridToCRS     expected "grid to CRS" transform as a matrix.
+     * @param gridIndices   expected lower grid coordinate values.
+     */
+    private static void assertGridGeometryEquals(final GridCoverage actual, 
final Matrix gridToCRS, final long... gridIndices) {
+        final GridGeometry gg = actual.getGridGeometry();
+        assertMatrixEquals("gridToCRS", gridToCRS, 
MathTransforms.getMatrix(gg.getGridToCRS(PixelInCell.CELL_CORNER)), STRICT);
+        assertArrayEquals(gridIndices, 
gg.getExtent().getLow().getCoordinateValues());
+    }
+
+    /**
+     * Verifies that the conversion of lower grid coordinates to CRS produces 
the expected values.
+     *
+     * @param actual     the coverage for which to verify the coordinate 
conversion.
+     * @param  expected  expected coordinates in units of the CRS.
+     * @throws TransformException if the conversion failed.
+     */
+    private static void verifyTransformLower(final GridCoverage actual, final 
double... expected) throws TransformException {
+        final GridGeometry gg = actual.getGridGeometry();
+        final MathTransform gridToCRS = 
gg.getGridToCRS(PixelInCell.CELL_CORNER);
+        final double[] coordinates = 
ArraysExt.copyAsDoubles(gg.getExtent().getLow().getCoordinateValues());
+        gridToCRS.transform(coordinates, 0, coordinates, 0, 1);
+        assertArrayEquals(expected, coordinates);
+    }
+
+    /**
+     * Tests the {@link GridCoverageProcessor} convenience methods.
+     *
+     * @throws TransformException if the coordinate conversion failed.
+     */
+    @Test
+    public void testUsingProcessor() throws TransformException {
+        final var coverage2D = initial(16, 8);
+        final var processor  = new GridCoverageProcessor();
+        final var coverage3D = processor.appendDimension(coverage2D, 260, 5, 
HardCodedCRS.GRAVITY_RELATED_HEIGHT);
+        assertSame(coverage2D, processor.selectGridDimensions(coverage3D, 0, 
1));
+        verifyTransformLower(coverage3D, 100, -20, 260);
+        assertGridGeometryEquals(coverage3D, new Matrix4(
+                4, 0, 0, 100,
+                0, 3, 0, -20,
+                0, 0, 5,   0,
+                0, 0, 0,   1), 0, 0, 52);
+
+        final var coverage4D = processor.appendDimension(coverage3D, 
Instant.parse("2022-06-18T00:00:00Z"), Duration.parse("P21D"));
+        assertSame(coverage2D, processor.selectGridDimensions(coverage3D, 0, 
1));
+        verifyTransformLower(coverage4D, 100, -20, 260, 19748);
+        assertGridGeometryEquals(coverage4D, Matrices.create(5, 5, new 
double[] {
+                4, 0, 0,  0, 100,
+                0, 3, 0,  0, -20,
+                0, 0, 5,  0,   0,
+                0, 0, 0, 21,   8,
+                0, 0, 0, 0,    1}), 0, 0, 52, 940);
+
+        // Easy way to check that the correct dimensions were selected.
+        verifyTransformLower(processor.selectGridDimensions(coverage4D, 0, 1, 
2), 100, -20, 260);
+        verifyTransformLower(processor.selectGridDimensions(coverage4D, 0, 1, 
3), 100, -20, 19748);
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
index 03f123a749..2751ffd1a8 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionalityReductionTest.java
@@ -31,7 +31,7 @@ import org.apache.sis.util.Utilities;
 
 // Test dependencies
 import org.junit.Test;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 
@@ -127,7 +127,7 @@ public final class DimensionalityReductionTest extends 
TestCase {
      * @param target     expected reduced coordinates.
      */
     private static void testPosition(final DimensionalityReduction reduction, 
double[] source, double[] target) {
-        assertArrayEquals(target, reduction.apply(new 
DirectPositionView.Double(source)).getCoordinate(), STRICT);
+        assertArrayEquals(target, reduction.apply(new 
DirectPositionView.Double(source)).getCoordinate());
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
index eb78f22360..56979288ba 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
@@ -1535,6 +1535,8 @@ public enum CommonCRS {
          * to the proleptic Gregorian calendar for every dates. For parsing 
and formatting of Julian days,
          * the {@link java.text.SimpleDateFormat} class is closer to the 
common practice (but not ISO 8601
          * compliant).</p>
+         *
+         * @see <a href="https://en.wikipedia.org/wiki/Julian_day";>Julian day 
on Wikipedia</a>
          */
         JULIAN(Vocabulary.Keys.Julian, -2440588L * MILLISECONDS_PER_DAY + 
MILLISECONDS_PER_DAY/2,
                "JulianDate", true),
@@ -1543,6 +1545,9 @@ public enum CommonCRS {
          * Time measured as days since November 17, 1858 at 00:00 UTC.
          * A <cite>Modified Julian day</cite> (MJD) is defined relative to
          * <cite>Julian day</cite> (JD) as {@code MJD = JD − 2400000.5}.
+         * This variant was introduced by the Smithsonian Astrophysical 
Observatory (Massachusetts) in 1955.
+         *
+         * @see <a href="https://en.wikipedia.org/wiki/Julian_day";>Julian day 
on Wikipedia</a>
          */
         MODIFIED_JULIAN(Vocabulary.Keys.ModifiedJulian, -40587L * 
MILLISECONDS_PER_DAY,
                         "ModifiedJulianDate", false),
@@ -1552,6 +1557,9 @@ public enum CommonCRS {
          * This epoch was introduced by NASA for the space program.
          * A <cite>Truncated Julian day</cite> (TJD) is defined relative to
          * <cite>Julian day</cite> (JD) as {@code TJD = JD − 2440000.5}.
+         * This variant was introduced by National Aeronautics and Space 
Administration (NASA) in 1979.
+         *
+         * @see <a href="https://en.wikipedia.org/wiki/Julian_day";>Julian day 
on Wikipedia</a>
          */
         TRUNCATED_JULIAN(Vocabulary.Keys.TruncatedJulian, -587L * 
MILLISECONDS_PER_DAY,
                          "TruncatedJulianDate", true),
@@ -1560,6 +1568,9 @@ public enum CommonCRS {
          * Time measured as days since December 31, 1899 at 12:00 UTC.
          * A <cite>Dublin Julian day</cite> (DJD) is defined relative to
          * <cite>Julian day</cite> (JD) as {@code DJD = JD − 2415020}.
+         * This variant was introduced by the International Astronomical Union 
(IAU) in 1955.
+         *
+         * @see <a href="https://en.wikipedia.org/wiki/Julian_day";>Julian day 
on Wikipedia</a>
          */
         DUBLIN_JULIAN(Vocabulary.Keys.DublinJulian, -25568L * 
MILLISECONDS_PER_DAY + MILLISECONDS_PER_DAY/2,
                       "DublinJulian", false),
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java
index 87ccac229d..f6af6788a1 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java
@@ -1914,6 +1914,25 @@ public final class ArraysExt extends Static {
         return result;
     }
 
+    /**
+     * Returns a copy of the given array where each value has been casted to 
the {@code double} type.
+     * This method does not verify if the casts would cause data loss.
+     *
+     * @param  data  the array to copy, or {@code null}.
+     * @return a copy of the given array with values casted to the {@code 
double} type,
+     *         or {@code null} if the given array was null.
+     *
+     * @since 1.5
+     */
+    public static double[] copyAsDoubles(final long[] data) {
+        if (data == null) return null;
+        final double[] result = new double[data.length];
+        for (int i=0; i<data.length; i++) {
+            result[i] = data[i];
+        }
+        return result;
+    }
+
     /**
      * Returns a copy of the given array where each value has been casted to 
the {@code float} type.
      * This method does not verify if the casts would cause data loss.
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/Numerics.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/Numerics.java
index f7f6f62111..b8d3b055d3 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/Numerics.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/Numerics.java
@@ -132,6 +132,8 @@ public final class Numerics extends Static {
 
     /**
      * Maximal integer value which is convertible to {@code double} type 
without lost of precision digits.
+     *
+     * @see #clampForDouble(long)
      */
     public static final long MAX_INTEGER_CONVERTIBLE_TO_DOUBLE = 1L << 
DOUBLE_PRECISION;
 
@@ -267,6 +269,18 @@ public final class Numerics extends Static {
         return (result < x) ? Long.MAX_VALUE : Long.MIN_VALUE;
     }
 
+    /**
+     * Returns the value rounded to nearest integer and clamped to a range
+     * that can be converted to {@code double} without precision lost.
+     *
+     * @param  value  the value to round and clamp.
+     * @return the value clamped to the range convertible to {@code double} 
without precision lost.
+     */
+    public static long roundAndClamp(final double value) {
+        return Math.max(-MAX_INTEGER_CONVERTIBLE_TO_DOUBLE,
+               Math.min(+MAX_INTEGER_CONVERTIBLE_TO_DOUBLE, 
Math.round(value)));
+    }
+
     /**
      * Returns the given value clamped to the range on 32 bits integer.
      *

Reply via email to