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());
+ }
}