You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2026/06/23 13:53:01 UTC
(sis) branch geoapi-4.0 updated (0a2fde0652 -> 0061e5aa33)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
from 0a2fde0652 Merge branch 'fix/geotiff-flaky-test' into geoapi-4.0 https://github.com/apache/sis/pull/44
add c8e75db2e8 chore(Build): ugrade Gradle wrapper to 8.14.4 to allow building with JDK 24
add a3b5fc9d28 test(Storage): add a test that shows a bug in ImageTileMatrix when trying to get the resource of a 3D tile
add c773e2ca4d fix(Storage): Fix ImageTileMatrix tile indices to work with slices in N dimensions.
add 0b70760026 chore(Storage): improve ImageTileMatrix test to verify multiple ways to acquire tiles
add 180ee38a6a fix(Storage): Properly handle user "getTiles" requests when requesting multidimensional extents
add 1d55e786a1 fix(Image): fix ReshapedImage.singleTile to strictly return the designated tile, and only this tile.
new 0061e5aa33 Merge branch 'fix/image-tile-matrix-get-tile' into geoapi-4.0. https://github.com/apache/sis/pull/43
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../main/org/apache/sis/image/ComputedImage.java | 15 -
.../main/org/apache/sis/image/PlanarImage.java | 21 ++
.../sis/image/internal/shared/ReshapedImage.java | 14 +-
.../sis/image/internal/shared/TiledImage.java | 23 +-
.../image/internal/shared/ReshapedImageTest.java | 25 +-
.../apache/sis/storage/tiling/ImageTileMatrix.java | 346 ++++++++++++++-----
.../apache/sis/storage/tiling/IterationDomain.java | 38 +--
.../org/apache/sis/storage/tiling/TileMatrix.java | 2 +
.../sis/storage/tiling/ImageTileMatrixTest.java | 371 +++++++++++++++++++++
9 files changed, 719 insertions(+), 136 deletions(-)
create mode 100644 endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/tiling/ImageTileMatrixTest.java
(sis) 01/01: Merge branch 'fix/image-tile-matrix-get-tile' into geoapi-4.0. https://github.com/apache/sis/pull/43
Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit 0061e5aa331a22fe13dc3bde2e7cd076a8ce7f93
Merge: 0a2fde0652 1d55e786a1
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Jun 23 15:49:12 2026 +0200
Merge branch 'fix/image-tile-matrix-get-tile' into geoapi-4.0.
https://github.com/apache/sis/pull/43
Summary of changes applied during the merge:
* `imageTilingExtent` modified for avoiding exclusion of tiles outside previous requests but nevertheless loaded.
* Iteration over slices moved after the computation of the intersection between requested bounds and available bounds.
* Bug fix in calculation of `imageToTileX` and `imageToTileX` (was a bug in previous code, not in the pull request).
* Renamed some variables for avoiding above confusion and for consistency with the names used in other classes.
* Documentation, in particular about assumption that each slice has a "tile size" of 1.
* In `ImageTileMatrixTest`, run the tests with sequential iterations before to test parallel iterations.
.../main/org/apache/sis/image/ComputedImage.java | 15 -
.../main/org/apache/sis/image/PlanarImage.java | 21 ++
.../sis/image/internal/shared/ReshapedImage.java | 14 +-
.../sis/image/internal/shared/TiledImage.java | 23 +-
.../image/internal/shared/ReshapedImageTest.java | 25 +-
.../apache/sis/storage/tiling/ImageTileMatrix.java | 346 ++++++++++++++-----
.../apache/sis/storage/tiling/IterationDomain.java | 38 +--
.../org/apache/sis/storage/tiling/TileMatrix.java | 2 +
.../sis/storage/tiling/ImageTileMatrixTest.java | 371 +++++++++++++++++++++
9 files changed, 719 insertions(+), 136 deletions(-)
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
index 7a342f5c28,83d65a92a5..934b8d0c32
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
@@@ -442,21 -442,21 +442,6 @@@ public abstract class ComputedImage ext
return sampleModel.getHeight();
}
-- /**
-- * Verifies that an index is inside the expected range of tile indices.
-- * If the index is out of bounds, then this method throws an {@code IndexOutOfBoundsException}
-- * for consistency with {@link java.awt.image.BufferedImage#getTile(int, int)} public contract.
-- *
-- * @throws IndexOutOfBoundsException if the given tile index is out of bounds.
-- */
-- private static void checkTileIndex(final String name, final int min, final int count, final int value) {
-- final int max = min + count;
-- if (value < min || value >= max) {
-- throw new IndexOutOfBoundsException(Errors.format(
-- Errors.Keys.ValueOutOfRange_4, name, min, max - 1, value));
-- }
-- }
--
/**
* Returns a tile of this image, computing it when needed.
* This method performs the first of the following actions that apply:
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
index 0e17b00ab4,a84309840b..38545fbc49
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
@@@ -485,6 -470,6 +485,27 @@@ public abstract class PlanarImage imple
return Math.toIntExact(getMinY() - multiplyFull(getMinTileY(), getTileHeight()));
}
++ /**
++ * Verifies that an index is inside the expected range of tile indices.
++ * If the index is out of bounds, then this method throws an {@code IndexOutOfBoundsException}
++ * for consistency with {@link java.awt.image.BufferedImage#getTile(int, int)} public contract.
++ *
++ * @param name name of the argument to validate (usually {@code "tileX"} or {@code "tileY"}).
++ * @param min the minimum valid tile index (usually {@link #getMinTileX()} or {@link #getMinTileY()}).
++ * @param count the number of tiles (usually {@link #getNumXTiles()} or {@link #getNumYTiles()}).
++ * @param index the tile index to validate.
++ * @throws IndexOutOfBoundsException if the given tile index is out of bounds.
++ *
++ * @since 1.7
++ */
++ protected static void checkTileIndex(final String name, final int min, final int count, final int index) {
++ final int max = min + (count - 1);
++ if (index < min || index > max) {
++ throw new IndexOutOfBoundsException(Errors.format(
++ Errors.Keys.ValueOutOfRange_4, name, min, max, index));
++ }
++ }
++
/**
* Creates a raster with the same sample model as this image and with the given size and location.
* This method does not verify argument validity.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java
index 7db4d35660,17bebfaab5..6c1469f3f0
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java
@@@ -42,6 -42,6 +42,7 @@@ import org.apache.sis.image.PlanarImage
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
++ * @author Alexis Manin (Geomatys)
*/
public final class ReshapedImage extends PlanarImage {
/**
@@@ -128,7 -128,7 +129,8 @@@
0, 0,
tileWidth,
tileHeight,
- 0, 0);
- tileX, tileY);
++ tileX,
++ tileY);
return image.isIdentity() ? image.source : image;
}
@@@ -295,9 -295,17 +297,19 @@@
* @param tileX the <var>x</var> index of the requested tile in the tile array.
* @param tileY the <var>y</var> index of the requested tile in the tile array.
* @return the tile specified by the specified indices.
++ * @throws IndexOutOfBoundsException if a given tile index is out of bounds.
*/
@Override
public Raster getTile(final int tileX, final int tileY) {
- // Ensure reshaped image strictly respect its boundaries and does not access source tiles outside its domain.
- if (!(
- getMinTileX() <= tileX && tileX < getMinTileX() + getNumXTiles()
- &&
- getMinTileY() <= tileY && tileY < getMinTileY() + getNumYTiles()
- )) {
- throw new IllegalArgumentException("Requested tile is outside Image domain");
- }
++ /*
++ * Checking the tile index is not strictly necessary because `RenderedImage` does not specify
++ * any behavior for invalid tile index. In particular, the exception to throw is unspecified.
++ * In this method, skipping the check would not be a violation of the method contract: we may
++ * return more tiles that what `ReshapedImage` declares, but those tiles are not wrong.
++ * However, checking the image boundaries may help to catch user's mistake.
++ */
++ checkTileIndex("tileX", minTileX, getNumXTiles(), tileX);
++ checkTileIndex("tileY", minTileY, getNumYTiles(), tileY);
return offset(source.getTile(tileX, tileY));
}
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TiledImage.java
index 80fbe92537,80fbe92537..f0c3869f3d
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TiledImage.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TiledImage.java
@@@ -23,7 -23,7 +23,6 @@@ import java.awt.image.ColorModel
import java.awt.image.SampleModel;
import org.apache.sis.image.PlanarImage;
import org.apache.sis.util.ArgumentChecks;
--import org.apache.sis.util.resources.Errors;
/**
@@@ -236,7 -236,7 +235,7 @@@ public class TiledImage extends PlanarI
* Returns the minimum tile index in the X direction.
*/
@Override
-- public int getMinTileX() {
++ public final int getMinTileX() {
return minTileX;
}
@@@ -244,7 -244,7 +243,7 @@@
* Returns the minimum tile index in the Y direction.
*/
@Override
-- public int getMinTileY() {
++ public final int getMinTileY() {
return minTileY;
}
@@@ -255,20 -255,20 +254,8 @@@
public Raster getTile(int tileX, int tileY) {
final int numXTiles = getNumXTiles();
final int numYTiles = getNumYTiles();
-- tileX = verifyTileIndex("tileX", tileX, minTileX, numXTiles);
-- tileY = verifyTileIndex("tileY", tileY, minTileY, numYTiles);
-- return tiles[tileX + tileY * numXTiles];
-- }
--
-- /**
-- * Verifies that the given tile index is inside expected bounds, then returns is zero-based value.
-- */
-- private static int verifyTileIndex(final String name, final int value, final int min, final int count) {
-- final int r = value - min;
-- if (r >= 0 && r < count) {
-- return r;
-- }
-- throw new IndexOutOfBoundsException(Errors.format(
-- Errors.Keys.ValueOutOfRange_4, name, min, min + count - 1, value));
++ checkTileIndex("tileX", minTileX, numXTiles, tileX);
++ checkTileIndex("tileY", minTileY, numYTiles, tileY);
++ return tiles[(tileX - minTileX) + (tileY - minTileY) * numXTiles];
}
}
diff --cc endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/internal/shared/ReshapedImageTest.java
index 35973b6908,10884f8d3b..7230007200
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/internal/shared/ReshapedImageTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/internal/shared/ReshapedImageTest.java
@@@ -20,6 -20,6 +20,7 @@@ import java.util.Random
import java.awt.image.DataBuffer;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
++import java.awt.image.Raster;
// Test dependencies
import org.junit.jupiter.api.Test;
@@@ -27,6 -27,6 +28,7 @@@ import static org.junit.jupiter.api.Ass
import org.apache.sis.test.TestCase;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.image.TiledImageMock;
++import static org.apache.sis.test.Assertions.assertMessageContains;
import static org.apache.sis.feature.Assertions.assertValuesEqual;
@@@ -35,6 -35,6 +37,7 @@@
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
++ * @author Alexis Manin (Geomatys)
*/
public final class ReshapedImageTest extends TestCase {
/**
@@@ -86,7 -86,7 +89,7 @@@
numYTiles = 1;
width = TILE_WIDTH;
height = TILE_HEIGHT;
-- final BufferedImage data = new BufferedImage(TILE_WIDTH, TILE_HEIGHT, BufferedImage.TYPE_BYTE_GRAY);
++ final var data = new BufferedImage(TILE_WIDTH, TILE_HEIGHT, BufferedImage.TYPE_BYTE_GRAY);
data.getRaster().setSamples(0, 0, TILE_WIDTH, TILE_HEIGHT, 0, new int[] {
1, 2, 3,
4, 7, 6
@@@ -198,4 -198,28 +201,24 @@@
{1210, 1211, 1212 , 1310, 1311, 1312 , 1410, 1411, 1412}
});
}
+
+ /**
+ * Verify a reshaped image created to expose a single tile from a source tiled image only serves the requested tile.
+ */
+ @Test
+ public void testExposeSingleTileFromTiledImage() {
+ var source = new TiledImageMock(DataBuffer.TYPE_USHORT, 1, 0, 0, 4, 4, 2, 2, 0, 0, false);
+ source.validate();
+ source.initializeAllTiles(0);
++ RenderedImage lastTile = ReshapedImage.singleTile(source, 1, 1);
++ assertMessageContains(assertThrows(IndexOutOfBoundsException.class,
++ () -> lastTile.getTile(0, 0),
++ "Tile (0, 0) should not be available"), "tileX");
+
- var lastTile = ReshapedImage.singleTile(source, 1, 1);
- try {
- lastTile.getTile(0, 0);
- fail("Tile (0, 0) should not be available");
- } catch (IllegalArgumentException e) {
- // Expected
- }
-
- final var exposedTile = lastTile.getTile(1, 1);
++ Raster exposedTile = lastTile.getTile(1, 1);
+ assertValuesEqual(exposedTile, 0, new int[][] {
+ { 400, 401 },
+ { 410, 411 }
+ });
+ }
}
diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
index a50ed64ab1,c0bf1791e2..3587faa0a7
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
@@@ -24,6 -24,6 +24,15 @@@ import java.awt.Rectangle
import java.awt.image.RenderedImage;
import java.nio.file.Path;
import java.util.logging.Logger;
++import static java.lang.Math.addExact;
++import static java.lang.Math.subtractExact;
++import static java.lang.Math.multiplyExact;
++import static java.lang.Math.incrementExact;
++import static java.lang.Math.decrementExact;
++import static java.lang.Math.floorDiv;
++import static java.lang.Math.toIntExact;
++import static java.lang.Math.min;
++import static java.lang.Math.max;
import org.opengis.util.GenericName;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.operation.matrix.Matrices;
@@@ -33,8 -33,7 +42,8 @@@ import org.apache.sis.storage.MemoryGri
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.NoSuchDataException;
import org.apache.sis.storage.UnsupportedQueryException;
- import org.apache.sis.storage.InternalDataStoreException;
import org.apache.sis.storage.Resource;
++import org.apache.sis.coverage.SubspaceNotSpecifiedException;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridCoverage2D;
@@@ -57,10 -56,10 +66,14 @@@ import org.apache.sis.storage.internal.
* The tile size must be specified at construction time and must be equal to the size of
* the tiles of the rendered image.
*
++ * If the coverage has more than two dimensions, the current implementation requires
++ * a tile size of 1 in all dimensions other than <var>X</var> and <var>Y</var>.
++ *
* <p>This class is needed only when the application needs details about the tiling scheme,
* for example in order to implement a Web Map Tile Service (<abbr>WMTS</abbr>).</p>
*
* @author Martin Desruisseaux (Geomatys)
++ * @author Alexis Manin (Geomatys)
*/
final class ImageTileMatrix implements TileMatrix {
/**
@@@ -109,6 -108,6 +122,8 @@@
* because some {@link TiledGridCoverage} implementations may use some metadata (typically the
* image date) as a third grid dimension, but still manage all tiles as two-dimensional.
* In such case, all extra dimensions are assumed to have a size of 1.
++ *
++ * @see TiledGridCoverageResource#getTileSize()
*/
private final int[] tileSize;
@@@ -129,9 -128,15 +144,25 @@@
* The image containing the tiles of the tile matrix. Computed when first needed, and may be
* recomputed multiple times with different offsets if the tile indices are larger than the
* capacity of 32-bits integers.
++ *
++ * <p>If the coverage has more than two dimensions, this image contains the tiles of only one slice.
++ * The current implementation does not cache more than one two-dimensional slice at a time, in order
++ * to avoid too large memory consumption.</p>
*/
private RenderedImage image;
+ /**
- * Extent of {@link #image} in the {@link #coverage} {@link TileMatrix#getTilingScheme() tiling scheme}.
- * Id {@link #image} is null, this should also be null. If image is not null, this must not be null.
++ * Indices in extra-dimensions (above two) of the tiles cached by {@link #image}.
++ * The length of this array shall be the number of dimensions of the {@linkplain #coverage}.
++ * The values at the <var>X</var> and <var>Y</var> dimensions will be ignored and <em>shall</em> be zero.
++ * The values in all other dimensions shall be the index of the slice rendered by {@linkplain #image}.
++ * The zeros in <var>X</var> and <var>Y</var> dimensions are important for making array comparisons simpler.
++ *
++ * <p>The content of this array shall not be modified.
++ * If new indices are needed, a new array shall be created.</p>
+ */
- private GridExtent imageTilingExtent;
++ private long[] imageSliceIndices;
+
/**
* The grid coverage processor to use when tiles use a subset of the bands.
*
@@@ -180,7 -185,7 +211,7 @@@
* using the minimum number needed for the largest index.
*/
if (i != 0) pattern.append(',');
-- pattern.append("%0").append(DecimalFunctions.floorLog10(Math.max(tileCount[i] - 1, 1)) + 1).append('d');
++ pattern.append("%0").append(DecimalFunctions.floorLog10(max(tileCount[i] - 1, 1)) + 1).append('d');
}
tilingScheme = new GridGeometry(cellGrid, extent.reshape(null, tileCount, false), MathTransforms.linear(toCells));
tileIndicesPattern = pattern.toString();
@@@ -278,13 -273,13 +309,13 @@@
imageToTileY = this.imageToTileY;
}
if (image != null) {
-- final long tileX = Math.subtractExact(indices[coverage.xDimension], imageToTileX);
++ final long tileX = subtractExact(indices[coverage.xDimension], imageToTileX);
final long x0 = image.getMinTileX();
if (tileX >= x0 && tileX < x0 + image.getNumXTiles()) {
-- final long tileY = Math.subtractExact(indices[coverage.yDimension], imageToTileY);
++ final long tileY = subtractExact(indices[coverage.yDimension], imageToTileY);
final long y0 = image.getMinTileY();
if (tileY >= y0 && tileY < y0 + image.getNumYTiles()) {
-- return getTileStatus(image, Math.toIntExact(tileX), Math.toIntExact(tileY));
++ return getTileStatus(image, toIntExact(tileX), toIntExact(tileY));
}
}
}
@@@ -331,7 -326,7 +362,14 @@@
public Optional<Tile> getTile(final long... indices) throws DataStoreException {
final GridExtent extent = tilingScheme.getExtent();
if (extent.contains(indices)) try {
-- final Tile tile = iterator(extent.reshape(indices, indices, true)).createFirstTile();
++ final int dimension = indices.length;
++ final long[] cellLow = new long[dimension];
++ final long[] cellHigh = new long[dimension];
++ for (int i=0; i < dimension; i++) {
++ cellLow [i] = tileToCell(indices[i], i);
++ cellHigh[i] = addExact(cellLow[i], tileSize[i] - 1); // Inclusive.
++ }
++ final Tile tile = builder(indices, indices, cellLow, cellHigh).createFirstTile();
return (tile.getStatus() == TileStatus.MISSING) ? Optional.empty() : Optional.of(tile);
} catch (ArithmeticException e) {
throw new UnsupportedQueryException(e);
@@@ -348,106 -343,155 +386,224 @@@
* <h4>Limitations</h4>
* The current implementation limits the size of the given extent to {@link Integer#MAX_VALUE}
* in each dimension. Note that this is a maximum size in tile indices, not in pixel coordinates.
++ * If the requested tile ranges exceed the capacity of the integer numbers used by this implementation,
++ * an {@link ArithmeticException} will be thrown either at the execution of this method,
++ * or during the execution of the returned stream.
*
-- * @param indiceRanges ranges of tile indices in all dimensions, or {@code null} for all tiles.
++ * <p>Another limitation is that the tile size in all dimensions other than <var>X</var> and <var>Y</var>
++ * must be one, otherwise an {@link SubspaceNotSpecifiedException} will be thrown.</p>
++ *
++ * @param tileRanges ranges of tile indices in all dimensions, or {@code null} for all tiles.
* @param parallel {@code true} for a parallel stream (if supported), or {@code false} for a sequential stream.
* @return stream of tiles, excluding missing tiles.
* @throws DataStoreException if the tiles cannot be fetched in the given ranges of tile indexes.
++ * @throws SubspaceNotSpecifiedException if tile size in dimensions other than X and Y is greater than 1.
++ * @throws ArithmeticException if some coordinates are too large.
++ * This exception may also happen latter, during the execution of the returned stream.
*/
@Override
-- public Stream<Tile> getTiles(GridExtent indiceRanges, final boolean parallel) throws DataStoreException {
-- ArgumentChecks.ensureDimensionMatches("indiceRanges", tilingScheme.getDimension(), indiceRanges);
-- if (indiceRanges == null) {
-- indiceRanges = tilingScheme.getExtent();
++ public Stream<Tile> getTiles(GridExtent tileRanges, final boolean parallel) throws DataStoreException {
++ /*
++ * Argument check: "indiceRanges" is the name used in public API, but we use `tileRanges` in this
++ * implementation for avoiding confusion with `cellRanges`. No need to expose this change to user.
++ */
++ ArgumentChecks.ensureDimensionMatches("indiceRanges", tilingScheme.getDimension(), tileRanges);
++ if (tileRanges == null) {
++ tileRanges = tilingScheme.getExtent();
}
- try {
- return StreamSupport.stream(iterator(indiceRanges).iterator(), parallel);
- } catch (ArithmeticException e) {
- throw new UnsupportedQueryException(e);
-
++ /*
++ * Gets the bounds of the images to read. If deferred reading is supported,
++ * we can expand to the bounds of the whole coverage in order to perform a
++ * read operation (deferred) only once. The bounds are intersected with the
++ * coverage extent.
++ */
++ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ final var coverage = coverage();
- for (int dim = 0 ; dim < indiceRanges.getDimension(); dim++) {
- if (dim != coverage.xDimension && dim != coverage.yDimension && dim > 1) {
- return slice(indiceRanges, coverage.xDimension, coverage.yDimension, parallel);
++ final int xDimension = coverage.xDimension;
++ final int yDimension = coverage.yDimension;
++ final GridExtent cellRanges = coverage.getGridGeometry().getExtent();
++ final int dimension = cellRanges.getDimension();
++ final var cellLow = new long[dimension];
++ final var tileLow = new long[dimension];
++ final var cellHigh = new long[dimension];
++ final var tileHigh = new long[dimension];
++ boolean is2D = true;
++ for (int i=0; i<dimension; i++) {
++ // Intersection between request and available data, in cell coordinates.
++ cellLow [i] = max(cellRanges.getLow (i), tileToCell(tileRanges.getLow(i), i));
++ cellHigh[i] = min(cellRanges.getHigh(i), decrementExact(tileToCell(incrementExact(tileRanges.getHigh(i)), i)));
++ final long span = cellHigh[i] - cellLow[i] + 1;
++ if (span < 0 || span > Integer.MAX_VALUE) {
++ throw new ArithmeticException(resource.errors().getString(Errors.Keys.IntegerOverflow_1, Integer.SIZE));
++ }
++ // Convert back to tile indices after intersection.
++ tileLow [i] = cellToTile(cellLow [i], i);
++ tileHigh[i] = cellToTile(cellHigh[i], i);
++ /*
++ * Special cases for (X,Y) dimensions: if deferred loading is enabled, expand the request
++ * to the maximum size that we can handle with 32-bits integers. This is okay because the
++ * actual loading will not happen at `read(…)` invocation time. Note that we do not expand
++ * `tileLow` and `tileHigh` because they should stay bounded to user's request.
++ */
++ if (i != xDimension && i != yDimension) {
++ is2D &= (cellLow[i] == cellHigh[i]);
++ } else if (coverage.deferredTileReading) {
++ final long remain = min(cellRanges.getSize(i), Integer.MAX_VALUE) - span;
++ final long after = min(remain / 2, cellRanges.getHigh(i) - cellHigh[i]);
++ final long before = min(remain - after, cellLow[i] - cellRanges.getLow(i));
++ cellLow [i] -= before;
++ cellHigh[i] += after;
+ }
}
-
- assert indiceRanges.getDegreesOfFreedom() <= 2 : "This code should only be reached if requested extent is 2D";
- try {
- return StreamSupport.stream(iterator(indiceRanges).iterator(), parallel);
- } catch (ArithmeticException e) {
- throw new UnsupportedQueryException(e);
++ if (is2D) {
++ // This code should only be reached if the requested extent is 2D.
++ assert tileRanges.getDegreesOfFreedom() <= TiledGridCoverageResource.BIDIMENSIONAL;
++ return StreamSupport.stream(builder(tileLow, tileHigh, cellLow, cellHigh).iterator(), parallel);
+ }
- }
-
- /**
- * Split given extent in a lazy sequence of 2D extents. For each 2D slice,
- * we query all tiles contained in user requested area.
- *
- * @param indiceRanges N-D tile extent to slice and to load tiles for.
- * @param xDim X dimension of the coverage, first axis of the 2D part to preserve in slices.
- * @param yDim Y dimension of the coverage, second axis of the 2D part to preserve in slices.
- * @param parallel True if we want a parallel stream returned, false otherwise.
- * @return A lazy sequence of all tiles contained in the given tile range.
- */
- private Stream<Tile> slice(GridExtent indiceRanges, int xDim, int yDim, boolean parallel) {
- final var slicingStart = indiceRanges.getLow().getCoordinateValues();
- final var slicingEnd = indiceRanges.getHigh().getCoordinateValues();
- final long xMax = slicingEnd[xDim];
- final long yMax = slicingEnd[yDim];
- slicingEnd[xDim] = slicingStart[xDim];
- slicingEnd[yDim] = slicingStart[yDim];
-
- final var slicingExtent = new GridExtent(null, slicingStart, slicingEnd, true);
- // NOTE: not sure here, but depending on stream fork policy,
- // allowing parallel extents ould hurt performance,
- // because it potentially allows to load tiles from different slices in parallel.
- // As this class image caching strategy is based on 2D extents,
- // we instead push parallelism down on tile loading level directly (see flatMap block).
- return slicingExtent.latticePointStream(false)
- .map(slicePoint -> {
- final var sliceHigh = Arrays.copyOf(slicePoint, slicePoint.length);
- sliceHigh[xDim] = xMax;
- sliceHigh[yDim] = yMax;
- return new GridExtent(null, slicePoint, sliceHigh, true);
- })
- .flatMap(slice -> {
++ /*
++ * Splits the given extent in a lazy sequence of 2D extents.
++ * For each 2D slice, we query all tiles contained in the user requested tile ranges.
++ *
++ * Note: the iteration over slices is executed in sequential order even if `parallel` is true.
++ * Allowing parallel iteration over slices could hurt performance, because it would cause the
++ * loading of rendered images from different slices in parallel. As this class `image` caching
++ * strategy is based on 2D extents, we instead push parallelism down on tile loading level directly
++ * (see `flatMap` block).
++ */
++ final long[] tileHighInExtraDimensions = tileHigh.clone();
++ tileHighInExtraDimensions[xDimension] = tileLow[xDimension];
++ tileHighInExtraDimensions[yDimension] = tileLow[yDimension];
++ return tileRanges.reshape(tileLow, tileHighInExtraDimensions, true).latticePointStream(false)
++ .flatMap((final long[] sliceLow) -> {
++ final long[] sliceHigh = sliceLow.clone();
++ sliceHigh[xDimension] = tileHigh[xDimension];
++ sliceHigh[yDimension] = tileHigh[yDimension];
++ for (int i=0; i<sliceLow.length; i++) {
++ if (i != xDimension && i != yDimension) {
++ cellLow [i] = tileToCell(sliceLow[i], i);
++ cellHigh[i] = addExact(cellLow[i], tileSize[i] - 1); // Inclusive.
++ }
++ }
++ final IteratorBuilder builder;
+ try {
- return StreamSupport.stream(iterator(slice).iterator(), parallel);
++ builder = builder(sliceLow, sliceHigh, cellLow, cellHigh);
+ } catch (DataStoreException e) {
+ throw new BackingStoreException("Cannot load tiles for 2D extent", e);
+ }
++ return StreamSupport.stream(builder.iterator(), parallel);
+ });
}
/**
-- * Creates an object which can be used for retrieving a single tile or a stream tiles.
++ * Creates an object which can be used for retrieving a single tile or a stream of tiles.
++ * All arguments in this method are in the coordinate system of the {@linkplain #coverage}.
++ * Note that a translation may exist between the coverage and the {@linkplain #image}.
++ *
++ * <p>The cell indices may describe a region larger to the tile indices if deferred loading
++ * of tiles is enabled. In such case, we will prepare more tiles than what user requested,
++ * but will iterate over only the requested tiles.</p>
*
-- * @param indiceRanges ranges of tile indices in all dimensions, or {@code null} for all tiles.
++ * @param tileLow tile index (inclusive) of the first valid tile requested by the user.
++ * @param tileHigh tile index (inclusive) of the last valid tile requested by the user.
++ * @param cellLow cell index (inclusive) of the first cell to load if necessary.
++ * @param cellHigh cell index (inclusive) of the last cell to load if necessary.
* @return a request which can be used for getting a tile or a stream of tiles in the given region.
* @throws DataStoreException if the tiles cannot be fetched in the given ranges of tile indexes.
* @throws ArithmeticException if coordinate computation exceeds the capacity of 64-bits integers.
++ * @throws SubspaceNotSpecifiedException if tile size in dimensions other than X and Y is greater than 1.
*/
-- private synchronized IterationDomain<Tile> iterator(final GridExtent indiceRanges) throws DataStoreException {
++ private synchronized IteratorBuilder builder(final long[] tileLow, final long[] tileHigh,
++ final long[] cellLow, final long[] cellHigh)
++ throws DataStoreException
++ {
@SuppressWarnings("LocalVariableHidesMemberVariable")
final TiledGridCoverage coverage = coverage();
- boolean retry = false;
- do { // This loop will be executed only 1 or 2 times.
- if (image != null) {
- final long xmin, ymin, xmax, ymax;
- xmin = Math.subtractExact(indiceRanges.getLow (coverage.xDimension), imageToTileX);
- xmax = Math.subtractExact(indiceRanges.getHigh(coverage.xDimension), imageToTileX);
- final long x0 = image.getMinTileX();
- if (xmin >= x0 && xmax < x0 + image.getNumXTiles()) {
- ymin = Math.subtractExact(indiceRanges.getLow (coverage.yDimension), imageToTileY);
- ymax = Math.subtractExact(indiceRanges.getHigh(coverage.yDimension), imageToTileY);
- final long y0 = image.getMinTileY();
- if (ymin >= y0 && ymax < y0 + image.getNumYTiles()) {
- return new Iterator(Math.toIntExact(xmin),
- Math.toIntExact(ymin),
- Math.toIntExact(xmax),
- Math.toIntExact(ymax));
- }
- }
- }
- assert Arrays.equals(indiceRanges.getSubspaceDimensions(2), new int[] {coverage.xDimension, coverage.yDimension})
- : "Iterator can only return tiles for a 2D slice over coverage XY dimensions.";
-
- // Returns currently cached image if it
- final var indiceRangesLow = indiceRanges.getLow().getCoordinateValues();
- if (image == null || !imageTilingExtent.contains(indiceRanges)) {
-- /*
-- * Gets the bounds of the image to read. If deferred reading is supported,
-- * we can expand to the bounds of the whole coverage in order to perform a
-- * read operation (deferred) only once.
-- */
-- final GridExtent extent = coverage.getGridGeometry().getExtent();
-- final int dimension = extent.getDimension();
-- final var low = new long[dimension];
-- final var high = new long[dimension];
-- for (int i=0; i<dimension; i++) {
-- final long limit = Math.incrementExact(extent.getHigh(i));
-- high[i] = Math.min(limit, tileToCell(Math.incrementExact(indiceRanges.getHigh(i)), i));
- low [i] = Math.max(extent.getLow(i), tileToCell(indiceRanges.getLow(i), i));
- low [i] = Math.max(extent.getLow(i), tileToCell(indiceRangesLow[i], i));
-- final long span = high[i] - low[i];
-- if (span < 0 || span > Integer.MAX_VALUE) {
-- throw new ArithmeticException(resource.errors().getString(Errors.Keys.IntegerOverflow_1, Integer.SIZE));
-- }
-- if (coverage.deferredTileReading) {
-- final long remain = Math.min(extent.getSize(i), Integer.MAX_VALUE) - span;
-- final long after = Math.min(remain >> 1, limit - high[i]);
-- final long before = Math.min(remain - after, low[i] - extent.getLow(i));
-- low [i] -= before;
-- high[i] += after;
-- }
++ final int xDimension = coverage.xDimension;
++ final int yDimension = coverage.yDimension;
++ final long[] slice = cellLow.clone();
++ slice[xDimension] = 0;
++ slice[yDimension] = 0;
++ if (image != null) {
++ final long tx0, ty0;
++ if (!Arrays.equals(imageSliceIndices, slice)
++ || tileLow [xDimension] < (tx0 = image.getMinTileX() + imageToTileX) || tileHigh[xDimension] >= tx0 + image.getNumXTiles()
++ || tileLow [yDimension] < (ty0 = image.getMinTileY() + imageToTileY) || tileHigh[yDimension] >= ty0 + image.getNumYTiles())
++ {
++ image = null;
}
- image = coverage.render(extent.reshape(low, high, false));
-
-
- final var imagePixelExtent = extent.reshape(low, high, false);
- imageTilingExtent = imagePixelExtent
- .translate(Arrays.stream(tileToCell).map(v -> -v).toArray())
- .subsample(Arrays.stream(tileSize).mapToLong(v -> v).toArray());
- image = coverage.render(imagePixelExtent);
-- imageToTileX = low[coverage.xDimension];
-- imageToTileY = low[coverage.yDimension];
- } while ((retry = !retry) == true);
- throw new InternalDataStoreException(); // Should never happen.
+ }
-
- return new Iterator(
- Math.toIntExact(Math.subtractExact(indiceRangesLow[coverage.xDimension], imageToTileX)),
- Math.toIntExact(Math.subtractExact(indiceRangesLow[coverage.yDimension], imageToTileY)),
- Math.toIntExact(Math.subtractExact(indiceRanges.getHigh(coverage.xDimension), imageToTileX)),
- Math.toIntExact(Math.subtractExact(indiceRanges.getHigh(coverage.yDimension), imageToTileY)),
- indiceRangesLow
- );
++ if (image == null) {
++ image = coverage.render(coverage.getGridGeometry().getExtent().reshape(cellLow, cellHigh, true));
++ imageToTileX = cellToImageTile(xDimension, cellLow, image.getTileGridXOffset(), image.getTileWidth());
++ imageToTileY = cellToImageTile(yDimension, cellLow, image.getTileGridYOffset(), image.getTileHeight());
++ imageSliceIndices = slice;
++ }
++ return new IteratorBuilder(toIntExact(subtractExact(tileLow [xDimension], imageToTileX)),
++ toIntExact(subtractExact(tileLow [yDimension], imageToTileY)),
++ toIntExact(subtractExact(tileHigh[xDimension], imageToTileX)),
++ toIntExact(subtractExact(tileHigh[yDimension], imageToTileY)));
}
/**
* Converts the give tile coordinate in the given dimension to cell coordinates.
++ * The cell and tile coordinates are in the {@linkplain #coverage} space, which
++ * may be translated compared to the coordinates of the rendered image.
*
* @param coordinate the tile coordinate to convert.
* @param dimension the dimension of the coordinate to convert.
-- * @return the cell coordinate.
++ * @return the cell coordinate in the grid coverage.
* @throws ArithmeticException if the result overflows the capacity of 64-bits integers.
*/
private long tileToCell(long coordinate, final int dimension) {
if (dimension < tileSize.length) {
-- coordinate = Math.multiplyExact(coordinate, tileSize[dimension]);
++ coordinate = multiplyExact(coordinate, tileSize[dimension]);
}
-- return Math.addExact(tileToCell[dimension], coordinate);
++ return addExact(tileToCell[dimension], coordinate);
++ }
++
++ /**
++ * Converts the give cell coordinate in the given dimension to tile coordinates.
++ * The cell and tile coordinates are in the {@linkplain #coverage} space, which
++ * may be translated compared to the coordinates of the rendered image.
++ *
++ * @param coordinate the cell coordinate to convert.
++ * @param dimension the dimension of the coordinate to convert.
++ * @return the tile coordinate in the grid coverage.
++ * @throws ArithmeticException if the result overflows the capacity of 64-bits integers.
++ */
++ private long cellToTile(long coordinate, final int dimension) {
++ coordinate = subtractExact(coordinate, tileToCell[dimension]);
++ if (dimension < tileSize.length) {
++ coordinate = floorDiv(coordinate, tileSize[dimension]);
++ }
++ return coordinate;
++ }
++
++ /**
++ * Converts a coverage cell coordinates to the index of a tile in the rendered image.
++ * This is used for computing {@link #imageToTileX} and {@link #imageToTileY}.
++ *
++ * @param dimension the dimension of the coordinate to convert.
++ * @param origin lower values of the grid coordinates of the extent which has been requested for rendering.
++ * @param offset {@link RenderedImage#getTileGridXOffset()} or {@link RenderedImage#getTileGridYOffset()}.
++ * @param size {@link RenderedImage#getTileWidth()} or {@link RenderedImage#getTileHeight()}.
++ * @return difference between index of the tile in the coverage and index of the tile in the rendered image.
++ * @throws ArithmeticException if the result overflows the capacity of 64-bits integers.
++ */
++ private long cellToImageTile(final int dimension, final long[] origin, final int offset, final int size) {
++ /*
++ * `cellToTile` is the tile coordinate in the grid coverage, assuming that tile indices start at (0,0).
++ * The corresponding tile coordinate in the image can be anything. Since `origin` gave the coordinates
++ * of pixel (0,0) in the rendered image, `cellToTile` corresponds to the rendered image tile which
++ * contains pixel (0,0). This is the tile at index `-tileGridoffset / tileSize` (note the minus sign).
++ */
++ return addExact(cellToTile(origin[dimension], dimension), floorDiv(offset, size));
}
/**
* Factory for an iterator over tiles in ranges of user-specified tile indices.
*/
-- private final class Iterator extends IterationDomain<Tile> {
++ private final class IteratorBuilder extends IterationDomain<Tile> {
/**
* Snapshot of {@link ImageTileMatrix#image}.
*/
@@@ -458,19 -502,42 +614,42 @@@
*/
private final long offsetX, offsetY;
+ /**
- * Dimension indices for the X and Y axes in the tile matrix coordinate system.
++ * Dimension indices for the <var>X</var> and <var>Y</var> axes in the tile matrix coordinate system.
+ */
- private final int xDim, yDim;
++ private final int xDimension, yDimension;
+
+ /**
- * Base template of tile indices.
- * Used when returning {@link Tile#getIndices() tile indices}.
- * Tiles will use these coordinates, replacing {link #xDim X} and {link #yDim Y} dimensions.
++ * Tile coordinates of the slice on which to iterate, except for values in X and Y dimensions.
++ * Used when returning {@linkplain Tile#getIndices() tile indices}.
++ * Tiles will use these coordinates, replacing {@link #xDimension X} and {@linkplain #yDimension Y} dimensions.
+ */
- private final long[] baseIndices;
++ private final long[] sliceIndices;
+
/**
* Creates a new request for tile iterators.
*
- * @param xmin first column index of tiles, inclusive.
- * @param xmin first row index of tiles, inclusive.
- * @param xmax last column index of tiles, inclusive.
- * @param ymax last row index of tiles, inclusive.
- * @param xmin first column index of tiles, inclusive.
- * @param ymin first row index of tiles, inclusive.
- * @param xmax last column index of tiles, inclusive.
- * @param ymax last row index of tiles, inclusive.
- * @param baseIndices tile coordinate template.
- * It can be any valid coordinate of the tiles managed by this iterator.
- * It serves as base to build correct tile coordinate for each returned tile.
- * Each tile will clone this array and replace its X and Y indices with its own.
- * Therefore, it is important that it represent properly the "slice" of extra-dimensions
- * this iterator operates on.
++ * <h4>Iteration in a slice of a <var>n</var>-dimensional cube.</h4>
++ * The {@link #imageSliceIndices} field gives the tile coordinates of the slice on which to iterate.
++ * It can be any valid coordinates of the tiles managed by this iterator.
++ * It serves as base to build correct tile coordinates for each returned tile.
++ * Each tile will clone this array and replace its X and Y indices by its own.
++ * Therefore, it is important that it represents properly the coordinates of extra-dimensions
++ * this iterator operates on.
++ *
++ * @param minTileX first column index of tiles, inclusive.
++ * @param minTileY first row index of tiles, inclusive.
++ * @param maxTileX last column index of tiles, inclusive.
++ * @param maxTileY last row index of tiles, inclusive.
*/
- Iterator(final int xmin, final int ymin, final int xmax, final int ymax) {
- Iterator(final int xmin, final int ymin, final int xmax, final int ymax,
- final long[] baseIndices)
- {
-- super(xmin, ymin, xmax, ymax);
-- tiles = image;
-- offsetX = imageToTileX;
-- offsetY = imageToTileY;
- xDim = coverage.xDimension;
- yDim = coverage.yDimension;
- this.baseIndices = baseIndices;
++ IteratorBuilder(final int minTileX, final int minTileY, final int maxTileX, final int maxTileY) {
++ super(minTileX, minTileY, maxTileX, maxTileY);
++ tiles = image;
++ offsetX = imageToTileX;
++ offsetY = imageToTileY;
++ xDimension = coverage.xDimension;
++ yDimension = coverage.yDimension;
++ sliceIndices = imageSliceIndices;
}
/**
@@@ -491,7 -558,10 +670,10 @@@
/** Returns the indices of this tile in the {@code TileMatrix}. */
@Override public long[] getIndices() {
- return new long[] {offsetX + tileX, offsetY + tileY};
- final long[] indices = baseIndices.clone();
- indices[xDim] = offsetX + tileX;
- indices[yDim] = offsetY + tileY;
++ final long[] indices = sliceIndices.clone();
++ indices[xDimension] = offsetX + tileX;
++ indices[yDimension] = offsetY + tileY;
+ return indices;
}
/** Returns information about whether the tile failed to load. */
@@@ -528,8 -598,8 +710,8 @@@
final long[] high = new long[indices.length];
for (int i=0; i<indices.length; i++) {
final long size = (i < tileSize.length) ? tileSize[i] : 1;
-- low [i] = Math.addExact(tileToCell[i], Math.multiplyExact(indices[i], size));
-- high[i] = Math.addExact(low[i], size - 1);
++ low [i] = addExact(tileToCell[i], multiplyExact(indices[i], size));
++ high[i] = addExact(low[i], size - 1);
}
@SuppressWarnings("LocalVariableHidesMemberVariable")
final TiledGridCoverage coverage = coverage();
diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/IterationDomain.java
index 0f26f25fd7,0f26f25fd7..81556220a9
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/IterationDomain.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/IterationDomain.java
@@@ -42,18 -42,18 +42,18 @@@ abstract class IterationDomain<T>
/**
* Minimal column index of the region on which to iterate.
*/
-- private final int xmin;
++ private final int minTileX;
/**
* Minimal row index of the region on which to iterate.
*/
-- private final int ymin;
++ private final int minTileY;
/**
* Number of columns on which to iterate.
-- * This is {@code (xmax + 1) - xmin} where {@code xmax} is inclusive.
++ * This is {@code (maxTileX + 1) - minTileX} where {@code maxTileX} is inclusive.
*/
-- private final long width;
++ private final long numXTiles;
/**
* Maximum value (exclusive) of the indexes of the tiles on which to iterate.
@@@ -64,16 -64,16 +64,16 @@@
/**
* Creates a new request for tile iterators.
*
-- * @param xmin first column index of tiles, inclusive.
-- * @param xmin first row index of tiles, inclusive.
-- * @param xmax last column index of tiles, inclusive.
-- * @param ymax last row index of tiles, inclusive.
++ * @param minTileX first column index of tiles, inclusive.
++ * @param minTileY first row index of tiles, inclusive.
++ * @param maxTileX last column index of tiles, inclusive.
++ * @param maxTileY last row index of tiles, inclusive.
*/
-- protected IterationDomain(final int xmin, final int ymin, final int xmax, final int ymax) {
-- this.xmin = xmin;
-- this.ymin = ymin;
-- this.width = (xmax + 1L) - xmin;
-- this.limit = Math.multiplyExact(width, (ymax + 1L) - ymin);
++ protected IterationDomain(final int minTileX, final int minTileY, final int maxTileX, final int maxTileY) {
++ this.minTileX = minTileX;
++ this.minTileY = minTileY;
++ this.numXTiles = (maxTileX + 1L) - minTileX;
++ this.limit = Math.multiplyExact(numXTiles, (maxTileY + 1L) - minTileY);
}
/**
@@@ -91,7 -91,7 +91,7 @@@
* Creates the first tile, or returns {@code null} if the tile is missing.
*/
final T createFirstTile() {
-- return createTile(xmin, ymin);
++ return createTile(minTileX, minTileY);
}
/**
@@@ -121,7 -121,7 +121,7 @@@
* Creates a new iterator which will initially traverse all tiles.
*/
Iterator() {
-- index = Math.multiplyExact(width, ymin);
++ index = Math.multiplyExact(numXTiles, minTileY);
increment = 1;
}
@@@ -188,8 -188,8 +188,8 @@@
@Override
public boolean tryAdvance(final Consumer<? super T> action) {
while (index < limit) {
-- final int tileY = Math.toIntExact(index / width);
-- final int tileX = Math.toIntExact(index % width + xmin);
++ final int tileY = Math.toIntExact(index / numXTiles);
++ final int tileX = Math.toIntExact(index % numXTiles + minTileX);
final T tile = createTile(tileX, tileY);
index += increment;
if (tile != null) {
@@@ -208,8 -208,8 +208,8 @@@
@Override
public void forEachRemaining(final Consumer<? super T> action) {
while (index < limit) {
-- final int tileY = Math.toIntExact(index / width);
-- final int tileX = Math.toIntExact(index % width + xmin);
++ final int tileY = Math.toIntExact(index / numXTiles);
++ final int tileX = Math.toIntExact(index % numXTiles + minTileX);
final T tile = createTile(tileX, tileY);
index += increment;
if (tile != null) {
diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrix.java
index 257ef1afb2,257ef1afb2..e3e8abcc67
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrix.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrix.java
@@@ -228,6 -228,6 +228,8 @@@ public interface TileMatrix
* @return stream of tiles, excluding {@linkplain TileStatus#MISSING missing} tiles.
* Iteration order of the stream may vary from one implementation to another and from one call to another.
* @throws DataStoreException if the tiles cannot be fetched in the given ranges of tile indexes.
++ * @throws ArithmeticException if some coordinates are too large.
++ * This exception may also happen latter, during the execution of the returned stream.
*/
Stream<Tile> getTiles(GridExtent indiceRanges, boolean parallel) throws DataStoreException;
}
diff --cc endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/tiling/ImageTileMatrixTest.java
index 0000000000,899f80e4b7..6862a52b74
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/tiling/ImageTileMatrixTest.java
+++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/tiling/ImageTileMatrixTest.java
@@@ -1,0 -1,288 +1,371 @@@
+ /*
+ * 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.storage.tiling;
+
-import java.awt.image.BufferedImage;
-import java.awt.image.ColorModel;
++import java.util.List;
++import java.util.Arrays;
+ import java.awt.image.Raster;
-import java.awt.image.SampleModel;
++import java.awt.image.RenderedImage;
+ import java.awt.image.WritableRaster;
-import java.util.Arrays;
-import java.util.List;
-import org.apache.sis.storage.GridCoverageResource;
+ import org.opengis.metadata.spatial.DimensionNameType;
+ import org.opengis.util.GenericName;
+ import org.apache.sis.coverage.SampleDimension;
+ import org.apache.sis.coverage.grid.GridExtent;
+ import org.apache.sis.coverage.grid.GridGeometry;
+ import org.apache.sis.coverage.grid.PixelInCell;
-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.storage.DataStoreException;
++import org.apache.sis.storage.GridCoverageResource;
+ import org.apache.sis.util.iso.Names;
+
+ // Test dependencies
+ import org.junit.jupiter.api.Test;
++import org.junit.jupiter.api.TestInstance;
+ import static org.junit.jupiter.api.Assertions.*;
+ import org.apache.sis.test.TestCase;
+ import org.apache.sis.referencing.crs.HardCodedCRS;
++import static org.apache.sis.test.Assertions.assertSingleton;
+
+
+ /**
+ * Verify behavior of {@link ImageTileMatrix} in a 3D space.
- * </br>
+ * This test creates a {@link MockTiledResource coverage mockup} that contains two 2D slices.
+ * Each slice contains {@link #NUM_COLS} tile columns and {@link #NUM_ROWS} tile rows.
+ * Each tile is a single band image whose diagonal is filled with its tile indices.
+ *
+ * @author Alexis Manin (Geomatys)
++ * @author Martin Desruisseaux (Geomatys)
+ */
+ @SuppressWarnings("exports")
++@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ public final class ImageTileMatrixTest extends TestCase {
++ /**
++ * Number of dimensions of the coverage in this test. The dimension of X and Y are 0 and 1 respectively.
++ * These dimensions should use different values for increasing the chances to detect wrong dimension index.
++ */
++ private static final int DIMENSION = 3;
+
- private static final BufferedImage MODEL = new BufferedImage(4, 4, BufferedImage.TYPE_BYTE_GRAY);
- private static final int TILE_WIDTH = 4;
- private static final int TILE_HEIGHT = 4;
- private static final int NUM_COLS = 3;
- private static final int NUM_ROWS = 3;
- private static final int NUM_SLICES = 2;
++ /**
++ * Size of all tiles in number of pixels.
++ * Image should be non-square for increasing the chances to detect bug in use of width and height.
++ */
++ private static final int TILE_WIDTH = 4,
++ TILE_HEIGHT = 3;
++
++ /**
++ * Number of tiles in each dimension to be tested.
++ */
++ private static final int NUM_COLS = 3,
++ NUM_ROWS = 5,
++ NUM_SLICES = 2;
++
++ /**
++ * Value returned by {@link MockTiledResource#getGridGeometry()}.
++ * Shared by each test method since this value is unmodifiable.
++ */
++ private final GridGeometry gridGeometry;
+
+ /**
- * Test that a tile from a 3D dataset can return its associated resource.
- * This is an anti-regression test because of a bug encountered while fetching tile resource.
++ * Value returned by {@link MockTiledResource#getSampleDimensions()}.
++ * Shared by each test method since this value is unmodifiable.
++ */
++ private final List<SampleDimension> sampleDimensions;
++
++ /**
++ * Creates a new text case.
++ */
++ public ImageTileMatrixTest() {
++ final var extent = new GridExtent(
++ new DimensionNameType[] {
++ DimensionNameType.COLUMN,
++ DimensionNameType.ROW,
++ DimensionNameType.VERTICAL
++ },
++ null, // Means all zeros.
++ new long[] {
++ NUM_COLS * TILE_WIDTH,
++ NUM_ROWS * TILE_HEIGHT,
++ NUM_SLICES
++ },
++ false);
++
++ gridGeometry = new GridGeometry(extent, PixelInCell.CELL_CORNER,
++ MathTransforms.scale(0.5, 0.5, 100), HardCodedCRS.WGS84_3D);
++
++ sampleDimensions = List.of(new SampleDimension.Builder().setName("data").build());
++ }
++
++ /**
++ * Tests that a tile from a 3D dataset can return its associated resource.
++ *
++ * @throws DataStoreException if an error occurred while querying the tiles.
+ */
+ @Test
+ public void testGetIndividualTiles3D() throws DataStoreException {
- final var matrix = get3DMockupTileMatrix();
++ testGetIndividualTiles3D(get3DMockupTileMatrix(), false);
++ }
++
++ /**
++ * Implementation of {@link #testGetIndividualTiles3D()} and {@link #testParallelism()}.
++ */
++ private void testGetIndividualTiles3D(final TileMatrix matrix, final boolean parallel) throws DataStoreException {
+ final var tilingExtent = matrix.getTilingScheme().getExtent();
- assertEquals(3, tilingExtent.getDimension());
- tilingExtent.latticePointStream(true)
++ assertEquals(DIMENSION, tilingExtent.getDimension());
++ tilingExtent.latticePointStream(parallel)
+ .map(tileCoord -> loadTile(tileCoord, matrix))
+ .forEach(ImageTileMatrixTest::checkTileContent);
+ }
+
+ /**
- * Check another path to obtain and validate tiles: get a batch of (all in fact) tiles using {@link TileMatrix#getTiles(GridExtent, boolean)}
++ * Checks another path to obtain and validate tiles.
++ * This path gets a batch of (all in fact) tiles using {@link TileMatrix#getTiles(GridExtent, boolean)}.
++ *
++ * @throws DataStoreException if an error occurred while querying the tiles.
+ */
+ @Test
+ public void testGetTileBatch3D() throws DataStoreException {
- final var matrix = get3DMockupTileMatrix();
- matrix.getTiles(null, true)
- .forEach(ImageTileMatrixTest::checkTileContent);
++ testGetTileBatch3D(get3DMockupTileMatrix(), false);
+ }
+
+ /**
- * Check tile content by querying a full rendering on each slice in the 3D coverage mockup.
++ * Implementation of {@link #testGetTileBatch3D()} and {@link #testParallelism()}.
++ */
++ private void testGetTileBatch3D(final TileMatrix matrix, final boolean parallel) throws DataStoreException {
++ matrix.getTiles(null, parallel).forEach(ImageTileMatrixTest::checkTileContent);
++ }
++
++ /**
++ * Checks tile content by querying a full rendering on each slice in the 3D coverage mockup.
+ * The aim is to ensure that coverage rendering properly returns a tiled image,
+ * and that each tile in the rendered image gives back its associated Tile content.
+ * Said otherwise, it checks that there's no tile mismatch or mixup when using ImageIO accessor.
++ *
++ * @throws DataStoreException if an error occurred while querying the tiles.
+ */
+ @Test
+ public void testGetTilesViaRender() throws DataStoreException {
++ testGetTilesViaRender(false);
++ }
++
++ /**
++ * Implementation of {@link #testGetTilesViaRender()} and {@link #testParallelism()}.
++ */
++ private void testGetTilesViaRender(final boolean parallel) throws DataStoreException {
+ final var resource = new MockTiledResource();
- final var coverage = resource.read((GridGeometry) null);
++ final var coverage = resource.read(null, null);
+ final var overallExtent = coverage.getGridGeometry().getExtent();
- overallExtent.resize(1, 1).latticePointStream(true)
++ overallExtent.resize(1, 1).latticePointStream(parallel)
+ .forEach(temporalSlice -> {
- var sliceHigh = temporalSlice.clone();
++ final long[] sliceHigh = temporalSlice.clone();
+ sliceHigh[0] = overallExtent.getHigh(0);
+ sliceHigh[1] = overallExtent.getHigh(1);
+ final var renderExtent = new GridExtent(null, temporalSlice, sliceHigh, true);
-
- final var image = coverage.render(renderExtent);
++ final RenderedImage image = coverage.render(renderExtent);
+ for (int col = 0; col < NUM_COLS; col++) {
+ for (int row = 0; row < NUM_ROWS; row++) {
- final var tileIndices = temporalSlice.clone();
- tileIndices[0] = col;
- tileIndices[1] = row;
- final var tileRaster = image.getTile(col, row);
- checkTileOrigin(tileRaster, tileIndices);
- checkTileRaster(tileRaster, tileIndices);
++ final Raster tileRaster = image.getTile(col, row);
++ assertEquals(col * TILE_WIDTH, tileRaster.getMinX());
++ assertEquals(row * TILE_HEIGHT, tileRaster.getMinY());
++ checkTileRaster(tileRaster, new long[] {col, row, temporalSlice[2]});
+ }
+ }
+ });
+ }
+
- private void checkTileOrigin(Raster tileRaster, long[] tileIndices) {
- final var expectedTileOrigin = new int[] {
- Math.toIntExact(Math.multiplyExact(tileIndices[0], TILE_WIDTH)),
- Math.toIntExact(Math.multiplyExact(tileIndices[1], TILE_HEIGHT))
- };
- final var actualTileOrigin = new int[] { tileRaster.getMinX(), tileRaster.getMinY() };
- assertArrayEquals(expectedTileOrigin, actualTileOrigin,
- () -> String.format(
- "Tile (%s): raster origin does not match rendering location. Expected: (%s) but was (%s)",
- Arrays.toString(tileIndices), Arrays.toString(expectedTileOrigin), Arrays.toString(actualTileOrigin)
- )
- );
++ /**
++ * Same tests as {@link #testGetIndividualTiles3D()}, {@link #testGetTileBatch3D()}
++ * and {@link #testGetTilesViaRender()} but executed in parallel.
++ *
++ * @throws DataStoreException if an error occurred while querying the tiles.
++ */
++ @Test
++ public void testParallelism() throws DataStoreException {
++ final var matrix = get3DMockupTileMatrix();
++ testGetIndividualTiles3D(matrix, true);
++ testGetTileBatch3D(matrix, true);
++ testGetTilesViaRender(true);
+ }
+
++ /**
++ * Returns the tile matrix from the three-dimensional resource mock.
++ *
++ * @return tile matrix from the three-dimensional resource mock.
++ * @throws DataStoreException should never be thrown with the mock.
++ */
+ private TileMatrix get3DMockupTileMatrix() throws DataStoreException {
+ final var resource = new MockTiledResource();
- final var tileMatrixSets = resource.getTileMatrixSets();
- assertFalse(tileMatrixSets.isEmpty());
- final var tms = tileMatrixSets.iterator().next();
- final var tileMatrixIterator = tms.getTileMatrices().values().iterator();
- assertTrue(tileMatrixIterator.hasNext());
- final var matrix = tileMatrixIterator.next();
- assertFalse(tileMatrixIterator.hasNext());
- return matrix;
++ final var tms = assertSingleton(resource.getTileMatrixSets());
++ return assertSingleton(tms.getTileMatrices().values());
+ }
+
+ /**
- * Load a specific tile from the given tile matrix.
- * This method expects that tile content will respect this test
++ * Loads the tile at the specified tile indices. This method does not perform any verification.
++ * Caller will typically invoke {@link #checkTileContent(Tile)} after this method.
++ *
++ * @param tileIndices indices of the tile to load.
++ * @param matrix tile matrix from which to load the time.
+ */
- private Tile loadTile(long[] requestedTileIndices, TileMatrix matrix) {
++ private static Tile loadTile(final long[] tileIndices, final TileMatrix matrix) {
+ try {
- final var optTile = matrix.getTile(requestedTileIndices);
- assertTrue(optTile.isPresent());
- final var tile = optTile.get();
- final var tIndices = tile.getIndices();
- assertArrayEquals(requestedTileIndices, tIndices, "Tile indices differ from request");
++ final Tile tile = matrix.getTile(tileIndices).orElseThrow(AssertionError::new);
++ assertArrayEquals(tileIndices, tile.getIndices(), "Tile indices differ from request.");
+ return tile;
+ } catch (DataStoreException e) {
- throw new AssertionError("Extraction of tile ("+ Arrays.toString(requestedTileIndices)+") failed", e);
++ // Because this method is invoked in lambda function, avoid checked exceptions.
++ throw new AssertionError("Cannot read tile (" + Arrays.toString(tileIndices) + ").", e);
+ }
+ }
+
- private static void checkTileContent(Tile tile) {
++ /**
++ * Verifies the properties and the sample values stored in the given tile.
++ * These sample values should contain the tile coordinates on a diagonal starting from the corner.
++ *
++ * @param tile the tile to verify.
++ */
++ private static void checkTileContent(final Tile tile) {
+ final var tileIndices = tile.getIndices();
++ assertEquals(DIMENSION, tileIndices.length);
++ assertEquals(TileStatus.EXISTS, tile.getStatus());
++ final RenderedImage tileImage;
+ try {
- assertEquals(TileStatus.EXISTS, tile.getStatus());
- final var tResource = tile.getResource();
- assertNotNull(tResource);
- if (!(tResource instanceof GridCoverageResource tileGridResource)) {
- throw new AssertionError("Tile resource is not a grid resource");
- }
- final var tileImage = tileGridResource.read(null).render(null);
- assertEquals(TILE_WIDTH, tileImage.getWidth());
- assertEquals(TILE_HEIGHT, tileImage.getHeight());
-
- assertEquals(1, tileImage.getNumXTiles() * tileImage.getNumYTiles(),
- "Tile image should contain only a single raster tile.");
- final var tileRaster = tileImage.getTile(tileImage.getMinTileX(), tileImage.getMinTileY());
- checkTileRaster(tileRaster, tileIndices);
++ var resource = assertInstanceOf(GridCoverageResource.class, tile.getResource());
++ tileImage = resource.read(null, null).render(null);
+ } catch (DataStoreException e) {
- fail("Validation of tile ("+ Arrays.toString(tileIndices)+") failed", e);
++ // Because this method is invoked in lambda function, avoid checked exceptions.
++ throw new AssertionError("Cannot read tile (" + Arrays.toString(tileIndices) + ").", e);
+ }
++ assertEquals(TILE_WIDTH, tileImage.getWidth());
++ assertEquals(TILE_HEIGHT, tileImage.getHeight());
++ assertEquals(1, tileImage.getNumXTiles());
++ assertEquals(1, tileImage.getNumYTiles());
++ checkTileRaster(tileImage.getTile(tileImage.getMinTileX(), tileImage.getMinTileY()), tileIndices);
+ }
+
- private static void checkTileRaster(Raster tileRaster, long[] tileIndices) {
- for (int i=0 ; i < tileIndices.length ; i++) {
++ /**
++ * Verifies the sample values stored on the diagonal starting in the tile upper-left corner.
++ * These sample values should contain the tile coordinates on a diagonal starting from the corner.
++ *
++ * @param tile the tile to verify.
++ * @param tileIndices expected tile coordinates.
++ */
++ private static void checkTileRaster(final Raster tile, final long[] tileIndices) {
++ assertEquals(DIMENSION, tileIndices.length);
++ for (int i=0 ; i<DIMENSION; i++) {
+ final var index = i;
- assertEquals(tileIndices[i], tileRaster.getSample(tileRaster.getMinX() + i, tileRaster.getMinY() + i, 0),
- () -> String.format("Tile sample at coordinate (%1$d, %1$d) should be the tile coordinate at dimension %1$d", index));
++ final int x = tile.getMinX() + i;
++ final int y = tile.getMinY() + i;
++ assertEquals(tileIndices[i], tile.getSample(x, y, 0),
++ () -> String.format("Tile sample at coordinate (%d, %d) should be the tile coordinate at dimension %d.", x, y, index));
+ }
+ }
+
+ /**
+ * A minimal {@link TiledGridCoverageResource} for testing.
++ * The size of all rasters is {@value #TILE_WIDTH} × {@value #TILE_HEIGHT} pixels.
++ * The tile coordinates are stored as sample values on the diagonal starting from
++ * the upper-left corner.
+ */
- private static final class MockTiledResource extends TiledGridCoverageResource {
-
- private final GridGeometry gridGeometry;
- private final List<SampleDimension> sampleDimensions;
-
++ private final class MockTiledResource extends TiledGridCoverageResource {
++ /**
++ * Creates a new resource mock.
++ */
+ MockTiledResource() {
+ super(null);
- final var extent = new GridExtent(
- new DimensionNameType[] {
- DimensionNameType.COLUMN,
- DimensionNameType.ROW,
- DimensionNameType.VERTICAL
- },
- new long[3],
- new long[] {
- NUM_COLS * TILE_WIDTH - 1,
- NUM_ROWS * TILE_HEIGHT - 1,
- NUM_SLICES - 1
- },
- true);
-
- final int dimension = 3;
- final MatrixSIS gridToCRS = Matrices.createIdentity(dimension + 1);
- gridToCRS.setNumber(0, 0, 0.5);
- gridToCRS.setNumber(1, 1, 0.5);
- gridToCRS.setNumber(2, 2, 100);
- gridGeometry = new GridGeometry(extent, PixelInCell.CELL_CORNER,
- MathTransforms.linear(gridToCRS), HardCodedCRS.WGS84_3D);
-
- sampleDimensions = List.of(new SampleDimension.Builder().setName("data").build());
+ }
+
++ /**
++ * Returns the same grid geometry for all mocks.
++ */
+ @Override
+ public GridGeometry getGridGeometry() {
+ return gridGeometry;
+ }
+
++ /**
++ * Returns the same sample dimensions for all mocks.
++ */
+ @Override
++ @SuppressWarnings("ReturnOfCollectionOrArrayField")
+ public List<SampleDimension> getSampleDimensions() {
+ return sampleDimensions;
+ }
+
++ /**
++ * Returns the size of all tiles for all images in this mock.
++ * The size is {@value #TILE_WIDTH} × {@value #TILE_HEIGHT} × 1.
++ */
+ @Override
+ protected int[] getTileSize() {
+ return new int[] {TILE_WIDTH, TILE_HEIGHT, 1};
+ }
+
- @Override
- protected ColorModel getColorModel(int[] bands) {
- return MODEL.getColorModel();
- }
-
- @Override
- protected SampleModel getSampleModel(int[] bands) {
- return MODEL.getSampleModel();
- }
-
++ /**
++ * Simulates a read operation, actually returning generated tiles.
++ */
+ @Override
+ protected TiledGridCoverage read(Subset subset) {
+ return new MockTiledCoverage(subset);
+ }
+ }
+
+ /**
- * A minimal {@link TiledGridCoverage} that creates empty rasters on demand.
++ * A minimal {@link TiledGridCoverage} that creates almost empty rasters on demand.
++ * The size of all rasters is {@value #TILE_WIDTH} × {@value #TILE_HEIGHT} pixels.
++ * The tile coordinates are stored as sample values on the diagonal starting from
++ * the upper-left corner.
+ */
+ private static final class MockTiledCoverage extends TiledGridCoverage {
-
++ /**
++ * Creates a new mock pretending to be the result of a read operation.
++ */
+ MockTiledCoverage(TiledGridCoverageResource.Subset subset) {
+ super(subset);
+ }
+
++ /**
++ * Returns a dummy identifier. This is not used by the test.
++ */
+ @Override
+ protected GenericName getIdentifier() {
+ return Names.createLocalName(null, null, "test");
+ }
+
++ /**
++ * Returns all tiles in the given area of interest.
++ * For each raster, tile coordinates are written as sample
++ * values on a diagonal starting in the upper-left corner.
++ *
++ * @see #checkTileRaster(Raster, long[])
++ */
+ @Override
- protected Raster[] readTiles(TileIterator iterator) {
++ protected Raster[] readTiles(final TileIterator iterator) {
+ final Raster[] tiles = new Raster[iterator.tileCountInQuery];
+ do {
- final WritableRaster raster = iterator.createRaster();
- final var tileCoords = iterator.getTileCoordinatesInResource();
- for (int i = 0; i < tileCoords.length; i++) {
- raster.setSample(raster.getMinX() + i, raster.getMinY() + i, 0, tileCoords[i]);
++ final WritableRaster tile = iterator.createRaster();
++ final int xmin = tile.getMinX();
++ final int ymin = tile.getMinY();
++ final long[] tileIndices = iterator.getTileCoordinatesInResource();
++ assertEquals(DIMENSION, tileIndices.length);
++ for (int i=0; i<DIMENSION; i++) {
++ tile.setSample(xmin + i, ymin + i, 0, StrictMath.toIntExact(tileIndices[i]));
+ }
- tiles[iterator.getTileIndexInResultArray()] = raster;
++ tiles[iterator.getTileIndexInResultArray()] = tile;
+ } while (iterator.next());
+ return tiles;
+ }
+ }
-}
++}