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) &&


Reply via email to