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 <[email protected]> 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; + } + } -} ++}
