This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 1c135038b1 Make `DefaultEvaluator` more robust to the case where the
`gridToCRS` transform has a scale factor of NaN.
1c135038b1 is described below
commit 1c135038b1aa346c337fb819edff8d147b3b1e88
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Jan 8 16:40:17 2026 +0100
Make `DefaultEvaluator` more robust to the case where the `gridToCRS`
transform has a scale factor of NaN.
---
.../apache/sis/coverage/grid/DefaultEvaluator.java | 17 +-
.../org/apache/sis/coverage/grid/GridGeometry.java | 6 +-
.../sis/coverage/grid/TranslatedTransform.java | 263 +++++++++++++++++++++
.../org/apache/sis/feature/internal/Resources.java | 2 +-
.../sis/feature/internal/Resources.properties | 2 +-
.../sis/feature/internal/Resources_fr.properties | 2 +-
.../sis/coverage/grid/GridCoverage2DTest.java | 24 +-
.../sis/referencing/operation/matrix/Matrices.java | 27 ++-
.../referencing/operation/matrix/MatrixSIS.java | 7 +-
.../referencing/operation/matrix/package-info.java | 2 +-
.../operation/transform/AbstractMathTransform.java | 5 +
.../operation/transform/ConcatenatedTransform.java | 1 +
.../operation/transform/MathTransforms.java | 4 +-
.../operation/transform/ProjectiveTransform.java | 2 +-
14 files changed, 343 insertions(+), 21 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
index b0a33aed2f..e1198e6489 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
@@ -76,7 +76,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
abstract class DefaultEvaluator implements GridCoverage.Evaluator {
/**
* The coordinate reference system of input points given to this converter,
- * or {@code null} if assumed the same as the coverage <abbr>CRS</abbr>.
+ * or {@code null} if assumed to be the same as the coverage
<abbr>CRS</abbr>.
* Used by {@link #toGridPosition(DirectPosition)} for checking if {@link
#inputToGrid} needs to be recomputed.
* As long at the evaluated points have the same <abbr>CRS</abbr>, the
same transform is reused.
*/
@@ -414,7 +414,7 @@ abstract class DefaultEvaluator implements
GridCoverage.Evaluator {
numPointsToTransform);
}
final int numPoints = firstCoordToTransform / dimension +
numPointsToTransform;
- wraparound(coordinates, 0, numPoints);
+ postTransform(coordinates, 0, numPoints);
/*
* Create the iterator. The `ValuesAtPointIterator.create(…)`
method will identify the slices in
* n-dimensional coverage, get the rendered images for the regions
of interest and get the tiles.
@@ -495,19 +495,20 @@ abstract class DefaultEvaluator implements
GridCoverage.Evaluator {
final double[] coordinates = point.getCoordinates();
final double[] gridCoords = (dimension <= coordinates.length) ?
coordinates : new double[dimension];
inputToGrid.transform(coordinates, 0, gridCoords, 0, 1);
- wraparound(gridCoords, 0, 1);
+ postTransform(gridCoords, 0, 1);
return gridCoords;
}
/**
- * If a coordinate is outside the coverage extent, check if a wraparound
on some axes
- * would bring the coordinates inside the extent. Coordinates are adjusted
in-place.
+ * Post-processing on grid coordinates after conversions from
<abbr>CRS</abbr> coordinates.
+ * If a coordinate is outside the coverage's extent, this method checks if
a wraparound on
+ * some axes would bring the coordinates inside the extent. Coordinates
are adjusted in-place.
*
* @param gridCoords the grid coordinates.
* @param offset index of the first grid coordinate value.
* @param numPoints number of points in the array.
*/
- private void wraparound(final double[] gridCoords, int offset, int
numPoints) throws TransformException {
+ private void postTransform(final double[] gridCoords, int offset, int
numPoints) throws TransformException {
if (wraparoundAxes == 0) {
return;
}
@@ -622,7 +623,7 @@ next: while (--numPoints >= 0) {
final GridCoverage coverage = getCoverage();
final GridGeometry gridGeometry = coverage.getGridGeometry();
MathTransform gridToCRS =
gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER);
- MathTransform crsToGrid = gridToCRS.inverse();
+ MathTransform crsToGrid =
TranslatedTransform.resolveNaN(gridToCRS.inverse(), gridGeometry);
if (crs != null) {
final CoordinateReferenceSystem stepCRS =
coverage.getCoordinateReferenceSystem();
final GeographicBoundingBox areaOfInterest =
gridGeometry.geographicBBox();
@@ -642,7 +643,7 @@ next: while (--numPoints >= 0) {
try {
CoordinateOperation op = CRS.findOperation(stepCRS, crs,
areaOfInterest);
gridToCRS = MathTransforms.concatenate(gridToCRS,
op.getMathTransform());
- final TransformSeparator ts = new
TransformSeparator(gridToCRS);
+ final var ts = new TransformSeparator(gridToCRS);
final int crsDim = gridToCRS.getTargetDimensions();
final int gridDim = gridToCRS.getSourceDimensions();
int[] mandatory = new int[gridDim];
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index ea203379e0..6aeef85771 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
@@ -1201,7 +1201,7 @@ public class GridGeometry implements LenientComparable,
Serializable {
} else try {
cornerToCRS.transform(new
double[cornerToCRS.getSourceDimensions()], 0, origin, 0, 1);
} catch (TransformException e) {
- throw new IllegalGridGeometryException(e, "gridToCRS");
+ throw new IllegalGridGeometryException(e, "origin");
}
return origin;
}
@@ -2011,6 +2011,8 @@ public class GridGeometry implements LenientComparable,
Serializable {
/**
* Returns a hash value for this grid geometry. This value needs not to
remain
* consistent between different implementations of the same class.
+ *
+ * @return a hash code value.
*/
@Override
public int hashCode() {
@@ -2067,6 +2069,8 @@ public class GridGeometry implements LenientComparable,
Serializable {
* Current implementation is equivalent to a call to {@link
#toTree(Locale, int)} with
* at least {@link #EXTENT}, {@link #ENVELOPE} and {@link #CRS} flags.
* Whether more flags are present or not is unspecified.
+ *
+ * @return a human-readable, multi-line string representation.
*/
@Override
public String toString() {
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/TranslatedTransform.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/TranslatedTransform.java
new file mode 100644
index 0000000000..e7bbeb6ea0
--- /dev/null
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/TranslatedTransform.java
@@ -0,0 +1,263 @@
+/*
+ * 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.util.List;
+import java.util.Arrays;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.AbstractMathTransform;
+import org.apache.sis.referencing.internal.shared.DirectPositionView;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * A transform which forces a translation in one specific dimension before to
apply another transform.
+ * This class intentionally blocks the optimization which consists in
optimizing two consecutive linear
+ * transforms with a matrix multiplication. The purpose of this transform is
to replace some coordinate
+ * values by zero before the matrix multiplication is applied, because Apache
<abbr>SIS</abbr> handles
+ * 0 × NaN in a special resulting in 0 instead of NaN (okay if NaN is
interpreted as "any finite number").
+ *
+ * <p>The current implementation has no tolerance threshold,
+ * but a future implementation could add such tolerance if it appears
useful.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+final class TranslatedTransform extends AbstractMathTransform {
+ /**
+ * The dimension where to apply the translation.
+ */
+ private final int dimension;
+
+ /**
+ * The offset to apply in the specified dimension.
+ */
+ private final double offset;
+
+ /**
+ * The transform to apply after the translation.
+ */
+ private final MathTransform transform;
+
+ /**
+ * Creates a new transform applying the given offset.
+ *
+ * @param dimension the dimension where to apply the translation.
+ * @param offset the offset to apply in the specified dimension.
+ * @param transform the transform to apply after the translation.
+ */
+ private TranslatedTransform(final int dimension, final double offset,
final MathTransform transform) {
+ this.dimension = dimension;
+ this.offset = offset;
+ this.transform = transform;
+ }
+
+ /**
+ * Returns a transform equivalent to the given {@code crsToGrid}
transform, but potentially with
+ * workarounds for making possible to transform grid origin despite NaN
values in scale factors.
+ * The workaround consists in translating the input coordinates ("real
world") in such a way that,
+ * for the point that corresponds to grid origin, the NaN scale factors
are multiplied by zero.
+ * This workaround relies on Apache <abbr>SIS</abbr> making a special case
for NaN × 0 = 0.
+ *
+ * @param crsToGrid the transform on which to apply workaround.
+ * @param gridGeometry the grid geometry from which the transform was
extracted.
+ * @return {@code crsToGrid} or a transform equivalent to {@code
crsToGrid} with workarounds.
+ * @throws NoninvertibleTransformException if this method cannot compute
the inverse of a {@code crsToGrid} step.
+ */
+ static MathTransform resolveNaN(MathTransform crsToGrid, final
GridGeometry gridGeometry)
+ throws NoninvertibleTransformException
+ {
+ MathTransform analyzing = MathTransforms.getLastStep(crsToGrid);
+ final Matrix toGrid = MathTransforms.getMatrix(analyzing);
+ if (toGrid != null) {
+ long dimensionBitMask = 0; // Bitmask of dimensions for which
to create `TranslatedTransform`.
+ long zeroingGridIndex = 0; // Bitmask of grid dimensions
where to force translation term to zero.
+ /*
+ * A NaN value in the translation column may be the consequence of
a NaN scale factor.
+ * Typically, a real non-zero value existed in the "grid to CRS"
column, which was the
+ * real world coordinate at grid index 0 in a dimension of unknown
resolution. But for
+ * the inverse of that matrix ("CRS to grid"), there is no
translation term which will
+ * give the desired result for a non-zero real world coordinate
while resulting in NaN
+ * for all other real world coordinates. Since no number exists
with those properties,
+ * the translation term had to be NaN. However, it become possible
to get the desired
+ * behavior if we translate the real world coordinate from
non-zero to zero.
+ */
+ for (int j = toGrid.getNumRow() - 1; --j >= 0;) {
+ int i = toGrid.getNumCol() - 1;
+ if (Double.isNaN(toGrid.getElement(j, i))) {
+ while (--i >= 0) {
+ if (Double.isNaN(toGrid.getElement(j, i))) {
+ dimensionBitMask |= (1L << i);
+ zeroingGridIndex |= (1L << j); // Set only if at
least one scale factor is NaN.
+ if ((i | j) >= Long.SIZE) {
+ throw new ArithmeticException(Errors.format(
+
Errors.Keys.ExcessiveNumberOfDimensions_1, Math.max(i, j) + 1));
+ }
+ }
+ }
+ }
+ }
+ /*
+ * If at least one scale factor is NaN, get the translation to
apply for allowing those
+ * scale factors to be multiplied by coordinate value 0 when the
point is at grid origin.
+ * We use the "pixel center" convention when possible, or "pixel
corner" as a fallback.
+ * Since the resolution is unknown, only one of center/corner
conventions may be available.
+ */
+ if (zeroingGridIndex != 0) {
+ Matrix fromGrid, fallback;
+ final MathTransform fromCenter =
MathTransforms.getFirstStep(gridGeometry.gridToCRS);
+ final MathTransform fromCorner =
MathTransforms.getFirstStep(gridGeometry.cornerToCRS);
+ analyzing = analyzing.inverse();
+ if (analyzing.equals(fromCenter) ||
analyzing.equals(fromCorner)) {
+ fromGrid = MathTransforms.getMatrix(fromCenter);
+ fallback = MathTransforms.getMatrix(fromCorner);
+ if (fromGrid == null) {
+ fromGrid = fallback;
+ fallback = null;
+ }
+ } else { // Happens if we have more than one step.
+ fromGrid = MathTransforms.getMatrix(analyzing);
+ fallback = null;
+ }
+ if (fromGrid != null) {
+ /*
+ * Get the translation terms of the matrix, but only the
ones that are real numbers
+ * and only in the dimensions where at least one scale
factor is NaN.
+ */
+ final int translationColumn = fromGrid.getNumCol() - 1;
+ final double[] offsets = new double[fromGrid.getNumRow()];
+ offsets[offsets.length - 1] = 1;
+ while (dimensionBitMask != 0) {
+ final int j =
Long.numberOfTrailingZeros(dimensionBitMask);
+ dimensionBitMask &= ~(1L << j);
+ double offset = fromGrid.getElement(j,
translationColumn);
+ if (Double.isNaN(offset) && fallback != null) {
+ offset = fallback.getElement(j, translationColumn);
+ if (Double.isNaN(offset)) continue;
+ }
+ offsets[j] = offset;
+ }
+ /*
+ * Since we are going to subtract `offsets` from input
coordinates, we need to add `offsets`
+ * back in order to get the same result. This is done by
`translate(…)` call. It will have no
+ * effect on the translation terms that are NaN, despite
those NaN being the reason why we do
+ * all this stuff. But the coordinates modified by
`offsets` may have an impact on some terms
+ * in other rows.
+ */
+ final MatrixSIS translated = Matrices.copy(toGrid);
+ translated.translate(offsets);
+ do {
+ final int j =
Long.numberOfTrailingZeros(zeroingGridIndex);
+ translated.setElement(j, translated.getNumCol() - 1,
0);
+ zeroingGridIndex &= ~(1L << j);
+ } while (zeroingGridIndex != 0);
+ /*
+ * Rebuild a chain of transforms from last step to first
step, but with translations
+ * before the affine transform in order to get NaN × 0 = 0
operations when possible.
+ */
+ final List<MathTransform> steps =
MathTransforms.getSteps(crsToGrid);
+ crsToGrid = MathTransforms.linear(translated);
+ for (int dimension = offsets.length - 2; dimension >= 0;
dimension--) {
+ final double offset = offsets[dimension];
+ if (offset != 0) {
+ crsToGrid = new TranslatedTransform(dimension,
offset, crsToGrid);
+ }
+ }
+ for (int i = steps.size() - 2; i >= 0; i--) { // Omit
last step because it has been replaced.
+ crsToGrid = MathTransforms.concatenate(steps.get(i),
crsToGrid);
+ }
+ }
+ }
+ }
+ return crsToGrid;
+ }
+
+ /**
+ * Returns the number of dimensions of input points.
+ */
+ @Override
+ public int getSourceDimensions() {
+ return transform.getSourceDimensions();
+ }
+
+ /**
+ * Returns the number of dimensions of output points.
+ */
+ @Override
+ public int getTargetDimensions() {
+ return transform.getTargetDimensions();
+ }
+
+ /**
+ * Transforms a single coordinate tuple in an array, optionally with the
transform derivative at that location.
+ *
+ * @param srcPts the array containing the source coordinates (cannot
be {@code null}).
+ * @param srcOff the offset to the point to be transformed in the
source array.
+ * @param dstPts the array into which the transformed coordinates is
returned.
+ * @param dstOff the offset to the location of the transformed point
that is stored in the destination array.
+ * @param derivate {@code true} for computing the derivative, or {@code
false} if not needed.
+ * @return the matrix of the transform derivative at the given source
position, or {@code null}.
+ * @throws TransformException if the point or the derivative cannot be
computed.
+ */
+ @Override
+ public Matrix transform(final double[] srcPts, final int srcOff,
+ final double[] dstPts, final int dstOff,
+ final boolean derivate) throws TransformException
+ {
+ Matrix derivative = null;
+ if (derivate) {
+ final double[] coordinates = Arrays.copyOfRange(srcPts, srcOff,
srcOff + getSourceDimensions());
+ coordinates[dimension] -= offset;
+ derivative = transform.derivative(new
DirectPositionView.Double(coordinates));
+ }
+ transform(srcPts, srcOff, dstPts, dstOff, 1);
+ return derivative;
+ }
+
+ /**
+ * Transforms a list of coordinate tuples.
+ *
+ * <h4>Implementation note</h4>
+ * In principle, we should not modify source coordinates as below.
+ * However, this transform is for {@link DefaultEvaluator} internal usage
+ * and is used in contexts where it is okay to overwrite source
coordinates.
+ *
+ * @param srcPts the array containing the source point coordinates.
+ * @param srcOff the offset to the first point to be transformed in the
source array.
+ * @param dstPts the array into which the transformed point coordinates
are returned.
+ * @param dstOff the offset to the location of the first transformed
point that is stored in the destination array.
+ * @param numPts the number of point objects to be transformed.
+ * @throws TransformException if a point cannot be transformed.
+ */
+ @Override
+ public void transform(final double[] srcPts, final int srcOff,
+ final double[] dstPts, final int dstOff, final int
numPts)
+ throws TransformException
+ {
+ final int srcDim = getSourceDimensions();
+ final int stop = srcOff + numPts * srcDim;
+ for (int i = srcOff + dimension; i < stop; i+= srcDim) {
+ srcPts[i] -= offset;
+ }
+ transform.transform(srcPts, srcOff, dstPts, dstOff, numPts);
+ }
+}
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
index 145b08cb89..8f099fc9d1 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
@@ -218,7 +218,7 @@ public class Resources extends IndexedResourceBundle {
public static final short IllegalGridEnvelope_3 = 27;
/**
- * Cannot create a grid geometry with the given “{0}” component.
+ * Invalid “{0}” component for the grid geometry.
*/
public static final short IllegalGridGeometryComponent_1 = 28;
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
index 1dda1a0cd7..5f66a5a6ea 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
@@ -51,7 +51,7 @@ IllegalCategoryRange_2 = Sample value range {1}
for \u201c{0}\u201d c
IllegalCharacteristicsType_3 = Expected an instance of \u2018{1}\u2019
for the \u201c{0}\u201d characteristics, but got an instance of \u2018{2}\u2019.
IllegalFeatureType_4 = The \u201c{1}\u201d
{0,choice,0#association|1#operation} expects features of type \u2018{2}\u2019,
but an instance of \u2018{3}\u2019 has been given.
IllegalGridEnvelope_3 = Illegal grid envelope [{1,number} \u2026
{2,number}] for dimension {0}.
-IllegalGridGeometryComponent_1 = Cannot create a grid geometry with the
given \u201c{0}\u201d component.
+IllegalGridGeometryComponent_1 = Invalid \u201c{0}\u201d component for the
grid geometry.
IllegalPropertyType_2 = Type or result of \u201c{0}\u201d property
cannot be \u2018{1}\u2019 for this operation.
IllegalPropertyValueClass_3 = Property \u201c{0}\u201d does not accept
values of type \u2018{2}\u2019. Expected an instance of \u2018{1}\u2019 or
derived type.
IllegalTransferFunction_1 = Illegal transfer function for
\u201c{0}\u201d category.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
index 356619b3a6..9ec75f2a56 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
@@ -56,7 +56,7 @@ IllegalCategoryRange_2 = La plage de valeurs {1}
pour la cat\u00e9gor
IllegalCharacteristicsType_3 = Une instance \u2018{1}\u2019 \u00e9tait
attendue pour la caract\u00e9ristique \u00ab\u202f{0}\u202f\u00bb, mais la
valeur donn\u00e9e est une instance de \u2018{2}\u2019.
IllegalFeatureType_4 =
L\u2019{0,choice,0#association|1#op\u00e9ration} \u00ab\u202f{1}\u202f\u00bb
attend des entit\u00e9s de type \u2018{2}\u2019, mais une instance de
\u2018{3}\u2019 a \u00e9t\u00e9 donn\u00e9e.
IllegalGridEnvelope_3 = La plage d\u2019index [{1,number} \u2026
{2,number}] de la dimension {0} n\u2019est pas valide.
-IllegalGridGeometryComponent_1 = Ne peut pas construire une
g\u00e9om\u00e9trie de grille avec la composante \u00ab\u202f{0}\u202f\u00bb
donn\u00e9e.
+IllegalGridGeometryComponent_1 = La composante \u00ab\u202f{0}\u202f\u00bb
est invalide pour une g\u00e9om\u00e9trie de grille.
IllegalPropertyType_2 = Le type ou le r\u00e9sultat de la
propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb ne peut pas \u00eatre
\u2018{1}\u2019 pour cette op\u00e9ration.
IllegalPropertyValueClass_3 = La propri\u00e9t\u00e9
\u00ab\u202f{0}\u202f\u00bb n\u2019accepte pas les valeurs de type
\u2018{2}\u2019. Une instance de \u2018{1}\u2019 ou d\u2019un type
d\u00e9riv\u00e9 \u00e9tait attendue.
IllegalTransferFunction_1 = Fonction de transfert ill\u00e9gale pour
la cat\u00e9gorie \u00ab\u202f{0}\u202f\u00bb.
diff --git
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverage2DTest.java
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverage2DTest.java
index 81b6477f2f..322134b981 100644
---
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverage2DTest.java
+++
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverage2DTest.java
@@ -25,8 +25,9 @@ import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.WritableRenderedImage;
import org.opengis.geometry.DirectPosition;
-import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.TransformException;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.geometry.DirectPosition2D;
import org.apache.sis.image.internal.shared.RasterFactory;
@@ -246,6 +247,27 @@ public class GridCoverage2DTest extends TestCase {
assertArrayEquals(new double[] {2}, evaluator.apply(new
DirectPosition2D(100 - 360, 0)));
}
+ /**
+ * Tests {@link GridCoverage.Evaluator#apply(DirectPosition)} with a scale
factor of NaN.
+ * This case happens, for example, in the temporal dimension of a slice of
unknown temporal resolution.
+ *
+ * @throws TransformException if an error occurred while testing a
conversion to grid coordinates.
+ */
+ @Test
+ public void testEvaluatorWithScaleNaN() throws TransformException {
+ final var gridToCRS = new Matrix3(0, 100, 17, Double.NaN, 0, 21, 0, 0,
1);
+ final GridCoverage.Evaluator evaluator =
createTestCoverage(MathTransforms.linear(gridToCRS)).evaluator();
+ FractionalGridCoordinates fc;
+ fc = evaluator.toGridCoordinates(new DirectPosition2D(18, 23));
+ assertEquals(2, fc.getDimension());
+ assertEquals(0.01, fc.getCoordinateFractional(1), 1E-12);
+ assertEquals(Double.NaN, fc.getCoordinateFractional(0));
+ fc = evaluator.toGridCoordinates(new DirectPosition2D(17, 21));
+ assertEquals(2, fc.getDimension());
+ assertEquals(0, fc.getCoordinateFractional(1), 1E-12);
+ assertEquals(0, fc.getCoordinateFractional(0));
+ }
+
/**
* Verifies that calling {@link GridCoverage#render(GridExtent)} with a
sub-extent (crop operation)
* returns precisely the requested area, not a smaller or bigger one.
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
index 9dd334e783..4665b95ef7 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
@@ -74,7 +74,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
* </ul>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.6
*
* @see org.apache.sis.parameter.MatrixParameters
*
@@ -946,10 +946,10 @@ search: while (freeColumn < numCol) {
}
/**
- * Creates a new matrix which is a copy of the given matrix.
+ * Creates a new matrix which is a modifiable copy of the given matrix.
*
* @param matrix the matrix to copy, or {@code null}.
- * @return a copy of the given matrix, or {@code null} if the given matrix
was null.
+ * @return a modifiable copy of the given matrix, or {@code null} if the
given matrix was null.
*
* @see MatrixSIS#clone()
* @see MatrixSIS#castOrCopy(Matrix)
@@ -1128,6 +1128,27 @@ search: while (freeColumn < numCol) {
return true;
}
+ /**
+ * Returns whether the given matrix has any NaN value.
+ *
+ * @param matrix the matrix to test.
+ * @return {@code true} if at least one matrix element is NaN.
+ *
+ * @since 1.6
+ */
+ public static boolean hasNaN(final Matrix matrix) {
+ final int numCol = matrix.getNumCol();
+ final int numRow = matrix.getNumRow();
+ for (int j=0; j<numRow; j++) {
+ for (int i=0; i<numCol; i++) {
+ if (Double.isNaN(matrix.getElement(j, i))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Compares the given matrices for equality, using the given relative or
absolute tolerance threshold.
* The matrix elements are compared as below:
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
index 51f0bcd4f8..199be4856f 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
@@ -584,8 +584,11 @@ public abstract class MatrixSIS implements Matrix,
LenientComparable, Cloneable,
for (int j=0; j<numRow; j++) {
Number sum = null;
for (int i=0; i<numCol; i++) {
- final Number element = getElementOrNull(j, i);
- sum = Arithmetic.add(sum, Arithmetic.multiply(element,
vector[i]));
+ final double value = vector[i];
+ if (value != 0) { // This is not just an optimization, as we
want 0 × NaN = 0 instead of NaN.
+ final Number element = getElementOrNull(j, i);
+ sum = Arithmetic.add(sum, Arithmetic.multiply(element,
value));
+ }
}
setNumber(j, numCol-1, sum);
}
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/package-info.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/package-info.java
index b4e850875e..ba7e3de7be 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/package-info.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/package-info.java
@@ -71,7 +71,7 @@
* Like SIS, Vecmath is optimized for small matrices of interest for 2D and 3D
graphics.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.6
* @since 0.4
*/
package org.apache.sis.referencing.operation.matrix;
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
index 82d7417d89..4a107a899a 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
@@ -271,6 +271,8 @@ public abstract class AbstractMathTransform extends
FormattableObject
/**
* Tests whether this transform does not move any points.
* The default implementation always returns {@code false}.
+ *
+ * @return whether this transform does not move any points.
*/
@Override
public boolean isIdentity() {
@@ -830,6 +832,9 @@ public abstract class AbstractMathTransform extends
FormattableObject
*
* <h4>Implementation note</h4>
* The {@link Inverse} inner class can be used as a base for inverse
transform implementations.
+ *
+ * @return the inverse of this transform.
+ * @throws NoninvertibleTransformException if the inverse transform cannot
be computed.
*/
@Override
public MathTransform inverse() throws NoninvertibleTransformException {
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
index f9168216a3..c3a3279cce 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
@@ -98,6 +98,7 @@ class ConcatenatedTransform extends AbstractMathTransform
implements Serializabl
* @param transform1 the first math transform.
* @param transform2 the second math transform.
*/
+ @SuppressWarnings("OverridableMethodCallInConstructor")
protected ConcatenatedTransform(final MathTransform transform1,
final MathTransform transform2)
{
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
index 86e777f0c0..ae4e96a9b0 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@ -207,7 +207,9 @@ public final class MathTransforms {
DoubleDouble.of(m.getNumber(0, 1), true));
}
case 2: {
- return AffineTransform2D.create(matrix);
+ if (!Matrices.hasNaN(matrix)) {
+ return AffineTransform2D.create(matrix);
+ }
}
}
} else if (sourceDimension == 2) {
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ProjectiveTransform.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ProjectiveTransform.java
index d0cdbbc30b..4ac46dd528 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ProjectiveTransform.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ProjectiveTransform.java
@@ -640,7 +640,7 @@ class ProjectiveTransform extends AbstractLinearTransform
implements ExtendedPre
*/
@Override
protected boolean equalsSameClass(final Object object) {
- final ProjectiveTransform that = (ProjectiveTransform) object;
+ final var that = (ProjectiveTransform) object;
return numRow == that.numRow &&
numCol == that.numCol &&
Arrays.equals(elt, that.elt) &&