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 6d5eb71f51 Bug fix in `DefaultEvaluator` when a point has more 
dimensions than the coverage CRS. Fix image bounding box check in 
`GridCoverage2D.apply(DirectPosition)`. Improve error message in case of points 
outside the coverage domain.
6d5eb71f51 is described below

commit 6d5eb71f51cbbc198aa288c3280a073fb1f862b6
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Dec 12 15:32:57 2025 +0100

    Bug fix in `DefaultEvaluator` when a point has more dimensions than the 
coverage CRS.
    Fix image bounding box check in `GridCoverage2D.apply(DirectPosition)`.
    Improve error message in case of points outside the coverage domain.
---
 .../sis/coverage/grid/BufferedGridCoverage.java    |   3 +-
 .../apache/sis/coverage/grid/DefaultEvaluator.java |  87 +++++++++++------
 .../apache/sis/coverage/grid/GridCoverage2D.java   |  40 ++++----
 .../sis/coverage/grid/ValuesAtPointIterator.java   |  32 ++++---
 .../sis/coverage/grid/DefaultEvaluatorTest.java    | 106 +++++++++++++++++----
 5 files changed, 186 insertions(+), 82 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BufferedGridCoverage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BufferedGridCoverage.java
index 741fb9a20f..e939391523 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BufferedGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BufferedGridCoverage.java
@@ -40,7 +40,6 @@ import org.apache.sis.image.DataType;
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.coordinate.MismatchedDimensionException;
 import org.opengis.coverage.CannotEvaluateException;
-import org.opengis.coverage.PointOutsideCoverageException;
 
 
 /**
@@ -353,7 +352,7 @@ public class BufferedGridCoverage extends GridCoverage {
                         if (isNullIfOutside()) {
                             return null;
                         }
-                        throw new 
PointOutsideCoverageException(pointOutsideCoverage(point), point);
+                        throw pointOutsideCoverage(point);
                     }
                     /*
                      * Following should never overflow, otherwise 
BufferedGridCoverage
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 bc59bf82be..24c1823ef1 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
@@ -333,7 +333,7 @@ abstract class DefaultEvaluator implements 
GridCoverage.Evaluator {
     public double[] apply(final DirectPosition point) throws 
CannotEvaluateException {
         try {
             final double[] gridCoords = toGridPosition(point);
-            final IntFunction<String> ifOutside;
+            final IntFunction<PointOutsideCoverageException> ifOutside;
             if (nullIfOutside) {
                 ifOutside = null;
             } else {
@@ -366,6 +366,7 @@ abstract class DefaultEvaluator implements 
GridCoverage.Evaluator {
         final GridCoverage coverage = getCoverage();
         final int dimension = coverage.gridGeometry.getDimension();
         double[] coordinates = ArraysExt.EMPTY_DOUBLE;
+        CoordinateReferenceSystem crs = inputCRS;
         MathTransform toGrid = inputToGrid;
         int srcDim = (toGrid == null) ? 0 : toGrid.getSourceDimensions();
         int inputCoordinateOffset = 0;
@@ -378,24 +379,24 @@ abstract class DefaultEvaluator implements 
GridCoverage.Evaluator {
              * the same CRS will be transformed in a single operation.
              */
             for (final DirectPosition point : points) {
-                if (setInputCRS(point.getCoordinateReferenceSystem())) {
+                if (crs != (crs = point.getCoordinateReferenceSystem())) {
                     if (numPointsToTransform > 0) {     // Because `toGrid` 
may be null.
                         assert toGrid.getTargetDimensions() == dimension;
                         toGrid.transform(coordinates, firstCoordToTransform,
                                          coordinates, firstCoordToTransform,
                                          numPointsToTransform);
                     }
-                    wraparound(coordinates, firstCoordToTransform, 
numPointsToTransform);
                     firstCoordToTransform += numPointsToTransform * dimension;
+                    inputCoordinateOffset = firstCoordToTransform;
                     numPointsToTransform = 0;
-                    toGrid = inputToGrid;
+                    toGrid = getInputToGrid(crs);
                     srcDim = toGrid.getSourceDimensions();
                 }
                 int offset = inputCoordinateOffset;
                 if ((inputCoordinateOffset += srcDim) > coordinates.length) {
                     int n = firstCoordToTransform / dimension;      // Number 
of points already transformed.
                     n = points.size() - n + numPointsToTransform;   // Number 
of points left to transform.
-                    coordinates = new double[Math.multiplyExact(n, 
Math.max(srcDim, dimension))];
+                    coordinates = Arrays.copyOf(coordinates, 
Math.multiplyExact(n, Math.max(srcDim, dimension)) + offset);
                 }
                 for (int i=0; i<srcDim; i++) {
                     coordinates[offset++] = point.getCoordinate(i);
@@ -412,27 +413,28 @@ abstract class DefaultEvaluator implements 
GridCoverage.Evaluator {
                                  coordinates, firstCoordToTransform,
                                  numPointsToTransform);
             }
-            wraparound(coordinates, firstCoordToTransform, 
numPointsToTransform);
             final int numPoints = firstCoordToTransform / dimension + 
numPointsToTransform;
+            wraparound(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.
              */
-            final IntFunction<String> ifOutside;
+            final IntFunction<PointOutsideCoverageException> ifOutside;
             if (nullIfOutside) {
                 ifOutside = null;
             } else {
                 final var listOfPoints = (points instanceof List<?>) ? (List<? 
extends DirectPosition>) points : null;
                 ifOutside = (index) -> {
+                    DirectPosition point = null;
                     if (listOfPoints != null) try {
-                        DirectPosition point = listOfPoints.get(index);
-                        if (point != null) {
-                            return pointOutsideCoverage(point);
-                        }
+                        point = listOfPoints.get(index);
                     } catch (IndexOutOfBoundsException e) {
                         recoverableException("pointOutsideCoverage", e);
                     }
-                    return 
Resources.format(Resources.Keys.PointOutsideCoverageDomain_1, "#" + index);
+                    if (point != null) {
+                        return pointOutsideCoverage(point);
+                    }
+                    return new 
PointOutsideCoverageException(Resources.format(Resources.Keys.PointOutsideCoverageDomain_1,
 "#" + index));
                 };
             }
             return StreamSupport.stream(ValuesAtPointIterator.create(coverage, 
coordinates, numPoints, ifOutside), parallel);
@@ -488,8 +490,7 @@ abstract class DefaultEvaluator implements 
GridCoverage.Evaluator {
          * If the `inputToGrid` transform has not yet been computed or is 
outdated, compute now.
          * The result will be cached and reused as long as the `inputCRS` is 
the same.
          */
-        setInputCRS(point.getCoordinateReferenceSystem());
-        gridCoordinates = inputToGrid.transform(point, gridCoordinates);
+        gridCoordinates = 
getInputToGrid(point.getCoordinateReferenceSystem()).transform(point, 
gridCoordinates);
         final int dimension = inputToGrid.getTargetDimensions();
         final double[] coordinates = point.getCoordinates();
         final double[] gridCoords = (dimension <= coordinates.length) ? 
coordinates : new double[dimension];
@@ -604,14 +605,19 @@ next:   while (--numPoints >= 0) {
      * This method should be invoked when the transform has not yet been 
computed
      * or may became outdated because {@link #inputCRS} needs to be changed.
      *
+     * <h4>Thread safety</h4>
+     * While {@code DefaultEvaluator} is not multi-thread, we nevertheless 
need to synchronize
+     * this method because it may be invoked by {@link 
#pointOutsideCoverage(DirectPosition)},
+     * which may be invoked from any thread if a stream is executed in 
parallel.
+     *
      * @param  crs  the new value to assign to {@link #inputCRS}. Can be 
{@code null}.
-     * @return whether the given <abbr>CRS</abbr> is different than the 
previous one.
+     * @return the new {@link #inputToGrid} value.
      */
-    private boolean setInputCRS(final CoordinateReferenceSystem crs)
+    private synchronized MathTransform getInputToGrid(final 
CoordinateReferenceSystem crs)
             throws FactoryException, NoninvertibleTransformException
     {
         if (crs == inputCRS && inputToGrid != null) {
-            return false;
+            return inputToGrid;
         }
         final GridCoverage coverage = getCoverage();
         final GridGeometry gridGeometry = coverage.getGridGeometry();
@@ -680,11 +686,11 @@ next:   while (--numPoints >= 0) {
         // Modify fields only after everything else succeeded.
         inputCRS    = crs;
         inputToGrid = crsToGrid;
-        return true;
+        return crsToGrid;
     }
 
     /**
-     * Creates an error message for a grid coordinates out of bounds.
+     * Creates an exception for a grid coordinates out of bounds.
      * This method tries to detect the dimension of the out-of-bounds
      * coordinate by searching for the dimension with largest error.
      *
@@ -693,20 +699,19 @@ next:   while (--numPoints >= 0) {
      * Therefore, it needs to be thread-safe even if {@link 
GridCoverage.Evaluator} is documented as not thread safe.
      *
      * @param  point  the point which is outside the grid.
-     * @return message to provide to {@link PointOutsideCoverageException}.
+     * @return the exception to throw
      */
-    final synchronized String pointOutsideCoverage(final DirectPosition point) 
{
+    final synchronized PointOutsideCoverageException 
pointOutsideCoverage(final DirectPosition point) {
         String details = null;
         final var buffer = new StringBuilder();
         final GridCoverage coverage = getCoverage();
         final GridExtent extent = coverage.gridGeometry.extent;
-        final MathTransform gridToCRS = coverage.gridGeometry.gridToCRS;
-        if (extent != null && gridToCRS != null) try {
+        if (extent != null) try {
+            gridCoordinates = 
getInputToGrid(point.getCoordinateReferenceSystem()).transform(point, 
gridCoordinates);
             int    axis     = 0;
             long   validMin = 0;
             long   validMax = 0;
             double distance = 0;
-            gridCoordinates = gridToCRS.inverse().transform(point, 
gridCoordinates);
             final int dimension = Math.min(gridCoordinates.getDimension(), 
extent.getDimension());
             for (int i=0; i<dimension; i++) {
                 final long low  = extent.getLow(i);
@@ -720,13 +725,33 @@ next:   while (--numPoints >= 0) {
                     distance = d;
                 }
             }
-            for (int i=0; i<dimension; i++) {
-                if (i != 0) buffer.append(' ');
-                
StringBuilders.trimFractionalPart(buffer.append(gridCoordinates.getCoordinate(i)));
+            /*
+             * Formats grid coordinates. Those coordinates are, in principle, 
integers.
+             * However if there is a fractional part, keep only the first 
non-zero digit.
+             * This is sufficient for seeing if the coordinate is outside 
because of that.
+             * Intentionally truncate, not round, the fraction digits for 
easier analysis.
+             *
+             * Note: if `distance` is zero, the point is not really outside. 
It should not happen,
+             * but if it happens anyway the error message written in this 
block would be misleading.
+             */
+            if (distance > 0) {
+                for (int i=0; i<dimension; i++) {
+                    if (i != 0) buffer.append(' ');
+                    int s = buffer.length();
+                    
StringBuilders.trimFractionalPart(buffer.append(gridCoordinates.getCoordinate(i)));
+                    if ((s = buffer.indexOf(".", s)) >= 0) {
+                        while (++s < buffer.length()) {
+                            if (buffer.charAt(s) != '0') {
+                                buffer.setLength(s + 1);
+                                break;
+                            }
+                        }
+                    }
+                }
+                details = 
Resources.format(Resources.Keys.GridCoordinateOutsideCoverage_4,
+                        extent.getAxisIdentification(axis, axis), validMin, 
validMax, buffer);
             }
-            details = 
Resources.format(Resources.Keys.GridCoordinateOutsideCoverage_4,
-                    extent.getAxisIdentification(axis, axis), validMin, 
validMax, buffer);
-        } catch (MismatchedDimensionException | TransformException e) {
+        } catch (MismatchedDimensionException | FactoryException | 
TransformException e) {
             recoverableException("pointOutsideCoverage", e);
         }
         /*
@@ -742,7 +767,7 @@ next:   while (--numPoints >= 0) {
         if (details != null) {
             message = message + System.lineSeparator() + details;
         }
-        return message;
+        return new PointOutsideCoverageException(message, point);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
index f746d99629..d40a5b56d3 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -59,7 +59,6 @@ import org.apache.sis.util.resources.Errors;
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.coordinate.MismatchedDimensionException;
 import org.opengis.coverage.CannotEvaluateException;
-import org.opengis.coverage.PointOutsideCoverageException;
 
 
 /**
@@ -500,10 +499,21 @@ public class GridCoverage2D extends GridCoverage {
      * Implementation of evaluator returned by {@link #evaluator()}.
      */
     private final class PixelAccessor extends DefaultEvaluator {
+        /**
+         * Bounding box of valid grid coordinate values, before conversion to 
pixel coordinates.
+         */
+        private final double xmin, ymin, xmax, ymax;
+
         /**
          * Creates a new evaluator for the enclosing coverage.
          */
         PixelAccessor() {
+            final long x = Math.subtractExact(data.getMinX(), gridToImageX);
+            final long y = Math.subtractExact(data.getMinY(), gridToImageY);
+            xmin = x - 0.5;
+            ymin = y - 0.5;
+            xmax = addExact(x, data.getWidth())  - 0.5;
+            ymax = addExact(y, data.getHeight()) - 0.5;
         }
 
         /**
@@ -521,23 +531,21 @@ public class GridCoverage2D extends GridCoverage {
          */
         @Override
         public double[] apply(final DirectPosition point) throws 
CannotEvaluateException {
-            RuntimeException cause = null;
             try {
                 final double[] gridCoords = toGridPosition(point);
                 final double cx = gridCoords[xDimension];
                 final double cy = gridCoords[yDimension];
-                if (cx >= ValuesAtPointIterator.DOMAIN_MINIMUM && cx <= 
ValuesAtPointIterator.DOMAIN_MAXIMUM &&
-                    cy >= ValuesAtPointIterator.DOMAIN_MINIMUM && cy <= 
ValuesAtPointIterator.DOMAIN_MAXIMUM)
-                {
-                    try {
-                        final int  x = toIntExact(addExact(round(cx), 
gridToImageX));
-                        final int  y = toIntExact(addExact(round(cy), 
gridToImageY));
-                        final int tx = ImageUtilities.pixelToTileX(data, x);
-                        final int ty = ImageUtilities.pixelToTileY(data, y);
-                        return values = data.getTile(tx, ty).getPixel(x, y, 
values);
-                    } catch (ArithmeticException | IndexOutOfBoundsException 
e) {
-                        cause = e;
-                    }
+                /*
+                 * We need to check the bounds ourselves instead of relying on 
the check done by `Raster.getPixel(…)`
+                 * for two reasons: 1) the tile bounds may not be fully valid 
if the image size is not a multiple of
+                 * the tile size, and 2) if a coordinate is NaN, the round 
result is 0 but which is incorrect here.
+                 */
+                if (cx >= xmin && cx < xmax && cy >= ymin && cy < ymax) {
+                    final int x  = toIntExact(addExact(round(cx), 
gridToImageX));
+                    final int y  = toIntExact(addExact(round(cy), 
gridToImageY));
+                    final int tx = ImageUtilities.pixelToTileX(data, x);
+                    final int ty = ImageUtilities.pixelToTileY(data, y);
+                    return values = data.getTile(tx, ty).getPixel(x, y, 
values);
                 }
             } catch (RuntimeException | FactoryException | TransformException 
ex) {
                 throw new CannotEvaluateException(ex.getMessage(), ex);
@@ -545,9 +553,7 @@ public class GridCoverage2D extends GridCoverage {
             if (isNullIfOutside()) {
                 return null;
             }
-            var ex = new 
PointOutsideCoverageException(pointOutsideCoverage(point), point);
-            ex.initCause(cause);
-            throw ex;
+            throw pointOutsideCoverage(point);
         }
     }
 
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ValuesAtPointIterator.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ValuesAtPointIterator.java
index 44f31eb5b5..b49dd56446 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ValuesAtPointIterator.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ValuesAtPointIterator.java
@@ -88,11 +88,11 @@ abstract class ValuesAtPointIterator implements 
Spliterator<double[]> {
     protected final int limitOfXY;
 
     /**
-     * Supplier of the message of the exception to throw if a point is outside 
the coverage bounds.
+     * Supplier of the exception to throw if a point is outside the coverage 
bounds.
      * The function argument is the index of the coordinate tuple which is 
outside the grid coverage.
      * If this supplier is {@code null}, then null arrays will be returned 
instead of throwing an exception.
      */
-    protected final IntFunction<String> ifOutside;
+    protected final IntFunction<PointOutsideCoverageException> ifOutside;
 
     /**
      * Creates a new iterator which will traverses a subset of the given grid 
coordinates.
@@ -100,9 +100,11 @@ abstract class ValuesAtPointIterator implements 
Spliterator<double[]> {
      *
      * @param nearestXY  grid coordinates of points to evaluate, or {@code 
null}.
      * @param limitOfXY  index after the last coordinate of the last point to 
evaluate.
-     * @param ifOutside  supplier of exception message for points outside the 
coverage bounds, or {@code null}.
+     * @param ifOutside  supplier of exception for points outside the coverage 
bounds, or {@code null}.
      */
-    protected ValuesAtPointIterator(final long[] nearestXY, final int 
limitOfXY, final IntFunction<String> ifOutside) {
+    protected ValuesAtPointIterator(final long[] nearestXY, final int 
limitOfXY,
+                                    final 
IntFunction<PointOutsideCoverageException> ifOutside)
+    {
         this.nearestXY = nearestXY;
         this.limitOfXY = limitOfXY;
         this.ifOutside = ifOutside;
@@ -116,11 +118,11 @@ abstract class ValuesAtPointIterator implements 
Spliterator<double[]> {
      * @param  coverage    the coverage which will be evaluated.
      * @param  gridCoords  the grid coordinates as floating-point numbers.
      * @param  numPoints   number of points in the array.
-     * @param  ifOutside   supplier of exception message for points outside 
the coverage bounds, or {@code null}.
+     * @param  ifOutside   supplier of exception for points outside the 
coverage bounds, or {@code null}.
      * @return the iterator.
      */
     static ValuesAtPointIterator create(final GridCoverage coverage, final 
double[] gridCoords, int numPoints,
-                                        final IntFunction<String> ifOutside)
+                                        final 
IntFunction<PointOutsideCoverageException> ifOutside)
     {
         return Slices.create(coverage, gridCoords, 0, numPoints, 
ifOutside).shortcut();
     }
@@ -216,13 +218,13 @@ abstract class ValuesAtPointIterator implements 
Spliterator<double[]> {
          *
          * @param nearestXY  grid coordinates of points to evaluate, or {@code 
null}.
          * @param limitOfXY  index after the last coordinate of the last point 
to evaluate.
-         * @param ifOutside  supplier of exception message for points outside 
the coverage bounds, or {@code null}.
+         * @param ifOutside  supplier of exception for points outside the 
coverage bounds, or {@code null}.
          */
         protected Group(final long[] nearestXY,
                         final int    limitOfXY,
                         final int[]  firstGridCoordOfChildren,
                         final int    upperChildIndex,
-                        final IntFunction<String> ifOutside)
+                        final IntFunction<PointOutsideCoverageException> 
ifOutside)
         {
             super(nearestXY, limitOfXY, ifOutside);
             this.firstGridCoordOfChildren = firstGridCoordOfChildren;
@@ -372,7 +374,7 @@ abstract class ValuesAtPointIterator implements 
Spliterator<double[]> {
                        final int[]        firstGridCoordOfChildren,
                        final int          upperChildIndex,
                        final GridExtent[] imageExtents,
-                       final IntFunction<String> ifOutside)
+                       final IntFunction<PointOutsideCoverageException> 
ifOutside)
         {
             super(nearestXY, limitOfXY, firstGridCoordOfChildren, 
upperChildIndex, ifOutside);
             this.coverage = coverage;
@@ -391,10 +393,10 @@ abstract class ValuesAtPointIterator implements 
Spliterator<double[]> {
          * @param gridCoords        the grid coordinates as floating-point 
numbers.
          * @param gridCoordsOffset  index of the first grid coordinate value.
          * @param numPoints         number of points in the array.
-         * @param ifOutside         supplier of exception message for points 
outside the coverage bounds, or {@code null}.
+         * @param ifOutside         supplier of exception for points outside 
the coverage bounds, or {@code null}.
          */
         static Slices create(final GridCoverage coverage, final double[] 
gridCoords, int gridCoordsOffset, int numPoints,
-                             final IntFunction<String> ifOutside)
+                             final IntFunction<PointOutsideCoverageException> 
ifOutside)
         {
             final int dimension  = coverage.gridGeometry.getDimension();
             final var extentLow  = new long[dimension];
@@ -413,7 +415,7 @@ abstract class ValuesAtPointIterator implements 
Spliterator<double[]> {
                     extentLow[i] = Math.round(c);
                 }
                 if (wasOutside && ifOutside != null) {
-                    throw new 
PointOutsideCoverageException(ifOutside.apply(indexOfXY / BIDIMENSIONAL));
+                    throw ifOutside.apply(indexOfXY / BIDIMENSIONAL);
                 }
                 long lowerX, upperX, lowerY, upperY;
                 nearestXY[limitOfXY++] = lowerX = upperX = 
extentLow[X_DIMENSION];
@@ -498,7 +500,7 @@ changeOfSlice:  while (numPoints != 0) {
                 return Image.create(this, stopAtXY, 
coverage.render(extent)).shortcut();
             } catch (DisjointExtentException cause) {
                 if (ifOutside != null) {
-                    var e = new 
PointOutsideCoverageException(ifOutside.apply(indexOfXY / BIDIMENSIONAL));
+                    var e = ifOutside.apply(indexOfXY / BIDIMENSIONAL);
                     e.initCause(cause);
                     throw e;
                 }
@@ -571,7 +573,7 @@ changeOfSlice:  while (numPoints != 0) {
                       final int    upperChildIndex,
                       final int[]  tileIndices,
                       final BitSet tileIsAbsent,
-                      final IntFunction<String> ifOutside)
+                      final IntFunction<PointOutsideCoverageException> 
ifOutside)
         {
             super(nearestXY, limitOfXY, firstGridCoordOfChildren, 
upperChildIndex, ifOutside);
             this.image        = image;
@@ -643,7 +645,7 @@ nextTile:   for (tileCount = 0; indexOfXY < limitOfXY; 
tileCount++) {
                      * (instead of the end of the sequence of points that are 
on the same tile).
                      */
                     if (parent.ifOutside != null) {
-                        throw new 
PointOutsideCoverageException(parent.ifOutside.apply(indexOfXY / 
BIDIMENSIONAL));
+                        throw parent.ifOutside.apply(indexOfXY / 
BIDIMENSIONAL);
                     }
                     wasOutside = true;
                 } while (indexOfXY < limitOfXY);
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DefaultEvaluatorTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DefaultEvaluatorTest.java
index 4d54bb30b6..49277787ad 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DefaultEvaluatorTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DefaultEvaluatorTest.java
@@ -19,11 +19,13 @@ package org.apache.sis.coverage.grid;
 import java.util.Random;
 import java.util.Arrays;
 import java.util.List;
+import java.util.HashSet;
 import java.util.stream.Stream;
 import java.awt.image.DataBuffer;
 import javax.measure.IncommensurableException;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -32,6 +34,9 @@ import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.coverage.PointOutsideCoverageException;
+
 // Test dependencies
 import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
@@ -124,6 +129,9 @@ public final class DefaultEvaluatorTest extends TestCase {
                 random.nextBoolean());          // banded or interleaved 
sample model
 
         image.initializeAllTiles();
+        assertEquals(width,  image.getWidth());
+        assertEquals(height, image.getHeight());
+
         final int dx = random.nextInt(200) - 100;
         final int dy = random.nextInt(200) - 100;
         gridGeometry = new GridGeometry(
@@ -133,6 +141,23 @@ public final class DefaultEvaluatorTest extends TestCase {
         numXTiles = image.getNumXTiles();
     }
 
+    /**
+     * Returns the "grid to CRS" transform targeting the given coordinate 
reference system.
+     *
+     * @param  crs  the target coordinate reference system.
+     * @return the "grid to CRS" to the given CRS.
+     * @throws TransformException if the transform cannot be created.
+     */
+    private MathTransform gridToCRS(final CoordinateReferenceSystem crs) 
throws TransformException {
+        try {
+            CoordinateSystem coverageCS = 
gridGeometry.getCoordinateReferenceSystem().getCoordinateSystem();
+            Matrix swap = 
CoordinateSystems.swapAndScaleAxes(crs.getCoordinateSystem(), coverageCS);
+            return MathTransforms.concatenate(gridGeometry.gridToCRS, 
MathTransforms.linear(swap).inverse());
+        } catch (IncommensurableException e) {
+            throw new AssertionError(e);
+        }
+    }
+
     /**
      * Creates test points, together with the expected values.
      * The expected values are set in the {@link #expectedValues} field.
@@ -148,29 +173,52 @@ public final class DefaultEvaluatorTest extends TestCase {
         final float   lowerY     = gridGeometry.extent.getLow(1);
         final float[] gridCoords = new float[2];
         MathTransform gridToCRS  = gridGeometry.gridToCRS;
-        CoordinateReferenceSystem crs = HardCodedCRS.WGS84;
-        final CoordinateSystem coverageCS = crs.getCoordinateSystem();
+        CoordinateReferenceSystem crs = 
gridGeometry.getCoordinateReferenceSystem();
         expectedValues = new float[numPoints];
+        /*
+         * Prepare in advance the indexes of points to put outside the 
coverage.
+         * Some tests need the guarantee that at least one point is outside, 
and
+         * this approach also makes easier to have two consecutive points 
outside.
+         */
+        final var indexOfPointsOutside = new HashSet<Integer>();
+        if (allowVariations) {
+            for (int j=0; j<5; j++) {
+                int i = random.nextInt(numPoints);
+                indexOfPointsOutside.add(i);
+                if (i != 0 && random.nextBoolean()) {
+                    indexOfPointsOutside.add(i - 1);
+                }
+            }
+        }
         for (int i=0; i<numPoints; i++) {
             /*
-             * Randomly change the CRS if this change is allowed.
+             * Randomly change the CRS if this change is allowed. The test 
needs at least one CRS
+             * with more dimensions than the grid CRS, in order to verify that 
internal arrays do
+             * not overflow.
              */
-            if (allowVariations && random.nextInt(5) == 0) try {
-                if (random.nextBoolean()) {
-                    crs = HardCodedCRS.WGS84_LATITUDE_FIRST;
-                    var swap = CoordinateSystems.swapAndScaleAxes(coverageCS, 
crs.getCoordinateSystem());
-                    gridToCRS = 
MathTransforms.concatenate(gridGeometry.gridToCRS, MathTransforms.linear(swap));
-                } else {
-                    crs = HardCodedCRS.WGS84;
-                    gridToCRS  = gridGeometry.gridToCRS;
+            if (allowVariations) {
+                switch (random.nextInt(10)) {
+                    case 0: {
+                        crs = HardCodedCRS.WGS84;
+                        gridToCRS  = gridGeometry.gridToCRS;
+                        break;
+                    }
+                    case 1: {
+                        crs = HardCodedCRS.WGS84_LATITUDE_FIRST;
+                        gridToCRS = gridToCRS(crs);
+                        break;
+                    }
+                    case 2: {
+                        crs = HardCodedCRS.WGS84_3D;
+                        gridToCRS = gridToCRS(crs);
+                        break;
+                    }
                 }
-            } catch (IncommensurableException e) {
-                throw new AssertionError(e);
             }
             final float expected;
-            if (allowVariations && random.nextInt(5) == 0) {
-                gridCoords[0] = lowerX + (random.nextBoolean() ? -10 : 10 + 
width);
-                gridCoords[1] = lowerY + (random.nextBoolean() ? -10 : 10 + 
height);
+            if (indexOfPointsOutside.remove(i)) {
+                gridCoords[0] = lowerX + (random.nextBoolean() ? -1 : width);
+                gridCoords[1] = lowerY + (random.nextBoolean() ? -1 : height);
                 expected = Float.NaN;
             } else {
                 final int x = random.nextInt(width);
@@ -188,6 +236,7 @@ public final class DefaultEvaluatorTest extends TestCase {
             gridToCRS.transform(gridCoords, 0, point.coordinates, 0, 1);
             points[i] = point;
         }
+        assertTrue(indexOfPointsOutside.isEmpty());
         return Arrays.asList(points);
     }
 
@@ -199,7 +248,14 @@ public final class DefaultEvaluatorTest extends TestCase {
      * @param  stream  the computed values.
      */
     private void runAndCompare(final Stream<double[]> stream) {
-        final double[][] actual = stream.map((samples) -> (samples != null) ? 
samples.clone() : null).toArray(double[][]::new);
+        final boolean isNullIfOutside = evaluator.isNullIfOutside();
+        final double[][] actual = stream.map((samples) -> {
+            if (samples != null) {
+                return samples.clone();
+            }
+            assertTrue(isNullIfOutside, "Unexpected null array of sample 
values.");
+            return null;
+        }).toArray(double[][]::new);
         assertEquals(expectedValues.length, actual.length);
         for (int i=0; i<actual.length; i++) {
             double expected = expectedValues[i];
@@ -208,6 +264,7 @@ public final class DefaultEvaluatorTest extends TestCase {
                 assertEquals(numBands, samples.length);
             }
             for (int band = 0; band < numBands; band++) {
+                assertEquals(Double.isNaN(expected), samples == null);
                 assertEquals(expected += 1000, (samples != null) ? 
samples[band] : Double.NaN);
             }
         }
@@ -291,4 +348,19 @@ public final class DefaultEvaluatorTest extends TestCase {
         assertTrue(evaluator.isNullIfOutside());
         runAndCompare(evaluator.stream(createTestPoints(true), true));
     }
+
+    /**
+     * Verifies the exception thrown for point outside the grid domain.
+     *
+     * @throws TransformException if a test point cannot be computed.
+     */
+    @Test
+    public void testPointOutsideCoverageException() throws TransformException {
+        evaluator.setNullIfOutside(false);
+        assertFalse(evaluator.isNullIfOutside());
+        PointOutsideCoverageException ex = 
assertThrows(PointOutsideCoverageException.class,
+                () -> runAndCompare(evaluator.stream(createTestPoints(true), 
true)));
+        assertNotNull(ex.getOffendingLocation());
+        assertNotNull(ex.getMessage());
+    }
 }


Reply via email to