This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new d0ccef4330 Reorganize the `TiledGridCoverage` base class in support for GDAL data store: d0ccef4330 is described below commit d0ccef43303f6bb1763c3bcf5a24eb690b2647e9 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sun Sep 15 01:09:30 2024 +0200 Reorganize the `TiledGridCoverage` base class in support for GDAL data store: * Rename `TiledGridCoverage.AOI` as `TileIterator` and define `AOI` as the parent class of `TileIterator` and `Snapshot`. * Rename some `TiledGridCoverage` methods for better clarity, but with few significant changes in the code. It allows the reuse of existing code for requestion to GDAL only the valid area of a tile. This commit also creates the temporary transfer buffer only once per read operation. --- .../org/apache/sis/storage/geotiff/DataSubset.java | 16 +- .../sis/storage/base/TiledDeferredImage.java | 6 +- .../apache/sis/storage/base/TiledGridCoverage.java | 692 ++++++++++++--------- .../main/org/apache/sis/storage/gdal/Band.java | 99 +-- .../org/apache/sis/storage/gdal/TiledCoverage.java | 33 +- .../org/apache/sis/storage/gdal/TiledResource.java | 63 +- .../storage/gimi/internal/MatrixGridRessource.java | 9 +- 7 files changed, 533 insertions(+), 385 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java index 5210fcb925..b1e5ca3be0 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java @@ -62,7 +62,7 @@ import static org.apache.sis.pending.jdk.JDK18.ceilDiv; * <h2>Cell Coordinates</h2> * When there is no subsampling, {@code DataSubset} uses the same cell coordinates as {@link DataCube}. * When there is a subsampling, cell coordinates in this subset are divided by the subsampling factors. - * Conversion is done by {@link #toFullResolution(long, int)}. + * Conversion is done by {@link #pixelToResourceCoordinate(long, int)}. * * <h2>Tile Matrix Coordinates</h2> * In each {@code DataSubset}, indices of tiles starts at (0, 0, …). This class does not use @@ -237,7 +237,7 @@ class DataSubset extends TiledGridCoverage implements Localized { */ private static final class Tile extends Snapshot implements Comparable<Tile> { /** - * Value of {@link DataSubset#tileOffsets} at index {@link #indexInTileVector}. + * Value of {@link DataSubset#tileOffsets} at index {@link #getTileIndexInResource()}. * If pixel data are stored in different planes ("banks" in Java2D terminology), * then current implementation takes only the offset of the first bank to read. * This field contains the value that we want in increasing order. @@ -256,7 +256,7 @@ class DataSubset extends TiledGridCoverage implements Localized { */ Tile(final AOI domain, final Vector tileOffsets, final int[] includedBanks, final int numTiles) { super(domain); - int p = indexInTileVector; + int p = getTileIndexInResource(); if (includedBanks != null) { p += includedBanks[0] * numTiles; } @@ -275,7 +275,7 @@ class DataSubset extends TiledGridCoverage implements Localized { final void notifyInputChannel(final Vector tileOffsets, final Vector tileByteCounts, int b, final int numTiles, final ChannelDataInput input) { - b = indexInTileVector + b * numTiles; + b = getTileIndexInResource() + b * numTiles; final long offset = tileOffsets.longValue(b); final long length = tileByteCounts.longValue(b); input.rangeOfInterest(offset, Numerics.saturatingAdd(offset, length)); @@ -292,7 +292,7 @@ class DataSubset extends TiledGridCoverage implements Localized { */ final void copyTileInfo(final Vector source, final long[] target, final int[] includedBanks, final int numTiles) { for (int j=0; j<target.length; j++) { - final int i = indexInTileVector + numTiles * (includedBanks != null ? includedBanks[j] : j); + final int i = getTileIndexInResource() + numTiles * (includedBanks != null ? includedBanks[j] : j); target[j] = source.longValue(i); } } @@ -324,7 +324,7 @@ class DataSubset extends TiledGridCoverage implements Localized { */ @Override @SuppressWarnings("try") - protected final Raster[] readTiles(final AOI iterator) throws IOException, DataStoreException { + protected final Raster[] readTiles(final TileIterator iterator) throws IOException, DataStoreException { /* * Prepare an array for all tiles to be returned. Tiles that are already in memory will be stored * in this array directly. Other tiles will be declared in the `missings` array and loaded later. @@ -341,7 +341,7 @@ class DataSubset extends TiledGridCoverage implements Localized { do { final Raster tile = iterator.getCachedTile(); if (tile != null) { - result[iterator.getIndexInResultArray()] = tile; + result[iterator.getTileIndexInResultArray()] = tile; } else { /* * Tile not yet loaded. Add to a queue of tiles to load later. @@ -407,7 +407,7 @@ class DataSubset extends TiledGridCoverage implements Localized { } else { r = readSlice(offsets, byteCounts, lower, upper, subsampling, origin); } - result[tile.indexInResultArray] = tile.cache(r); + result[tile.getTileIndexInResultArray()] = tile.cache(r); } else { needsCompaction = true; } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledDeferredImage.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledDeferredImage.java index 149ca9872d..f81ca880b9 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledDeferredImage.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledDeferredImage.java @@ -45,7 +45,7 @@ final class TiledDeferredImage extends BatchComputedImage { * Iterator over tiles. The iterator position should not be modified; * instead subsets of this iterator will be created when needed. */ - private final TiledGridCoverage.AOI iterator; + private final TiledGridCoverage.TileIterator iterator; /** * Creates a new tiled image. @@ -55,7 +55,7 @@ final class TiledDeferredImage extends BatchComputedImage { * @param properties image properties, or {@code null} if none. */ TiledDeferredImage(final int[] imageSize, final int[] tileLower, - final Map<String,Object> properties, final TiledGridCoverage.AOI iterator) + final Map<String,Object> properties, final TiledGridCoverage.TileIterator iterator) { super(iterator.getCoverage().model, properties); this.width = imageSize[TiledGridCoverage.X_DIMENSION]; @@ -100,7 +100,7 @@ final class TiledDeferredImage extends BatchComputedImage { */ @Override protected Raster[] computeTiles(final Rectangle tiles) throws Exception { - final TiledGridCoverage.AOI aoi = iterator.subset( + final TiledGridCoverage.TileIterator aoi = iterator.subset( new int[] { tiles.x, tiles.y diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java index 91731a7f96..c54f888654 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java @@ -62,15 +62,19 @@ import org.opengis.coordinate.MismatchedDimensionException; * This grid coverage may represent only a subset of the coverage resource. * Tiles are read from the storage only when first needed. * - * <h2>Cell Coordinates</h2> + * <h2>Cell coordinates</h2> * When there is no subsampling, this coverage uses the same cell coordinates as the originating resource. * When there is a subsampling, cell coordinates in this coverage are divided by the subsampling factors. - * Conversions are done by {@link #toFullResolution(long, int)}. + * Conversions are done by {@link #pixelToResourceCoordinates(Rectangle)}. * - * <h2>Tile coordinate matrix</h2> + * <p><b>DEsign note:</b> {@code TiledGridCoverage} use the same cell coordinates as the originating + * {@link TiledGridResource} (when no subsampling) because those two classes use {@code long} integers. + * There is no integer overflow to avoid.</p> + * + * <h2>Tile matrix coordinate (<abbr>TMC</abbr>)</h2> * In each {@code TiledGridCoverage}, indices of tiles starts at (0, 0, …). - * This class does not use the same tile indices as the coverage resource - * in order to avoid integer overflow. + * This class does not use the same tile indices as the coverage resource in order to avoid integer overflow. + * Each {@code TiledGridCoverage} instance uses its own, independent, Tile Matrix Coordinates (<abbr>TMC</abbr>). * * @author Martin Desruisseaux (Geomatys) */ @@ -119,6 +123,8 @@ public abstract class TiledGridCoverage extends GridCoverage { * All coverages created from the same {@link TiledGridResource} shall have the same tile size values. * The length of this array is the number of dimensions in the source {@link GridExtent}. * This is often {@value #BIDIMENSIONAL} but can also be more. + * + * @see #getTileSize(int) */ private final int[] tileSize; @@ -136,16 +142,21 @@ public abstract class TiledGridCoverage extends GridCoverage { private final int indexOfFirstTile; /** - * The Tile Matrix Coordinates (TMC) of the first tile. + * The Tile Matrix Coordinates (<abbr>TMC</abbr>) that tile (0,0) of this coverage + * would have in the originating {@code TiledGridResource}. * This is the value to subtract from tile indices computed from pixel coordinates. * + * <p>The current implementation assumes that the tile (0,0) in the resource starts + * at cell coordinates (0,0) of the resource.</p> + * * @see #indexOfFirstTile + * @see AOI#getTileCoordinatesInResource() */ private final long[] tmcOfFirstTile; /** * Conversion from pixel coordinates in this (potentially subsampled) coverage - * to pixel coordinates in the resource coverage at full resolution. + * to cell coordinates in the originating resource coverage at full resolution. * The conversion from (<var>x</var>, <var>y</var>) to (<var>x′</var>, <var>y′</var>) is as below, * where <var>s</var> are subsampling factors and <var>t</var> are subsampling offsets: * @@ -157,7 +168,7 @@ public abstract class TiledGridCoverage extends GridCoverage { * This transform maps {@linkplain org.apache.sis.coverage.grid.PixelInCell#CELL_CORNER pixel corners}. * * @see #getSubsampling(int) - * @see #toFullResolution(long, int) + * @see #pixelToResourceCoordinate(long, int) */ private final int[] subsampling, subsamplingOffsets; @@ -302,39 +313,43 @@ public abstract class TiledGridCoverage extends GridCoverage { } /** - * Converts a cell coordinate from this {@code TiledGridCoverage} coordinate space to full resolution. - * This method removes the subsampling effect. Note that since this {@code TiledGridCoverage} uses the - * same coordinate space as {@link TiledGridResource}, the converted coordinates should be valid in - * the full resource as well. + * Converts a cell coordinate from this coverage to the {@code TiledGridResource} coordinate space. + * This method removes the subsampling effect, i.e. returns the coordinate that we would have if this + * coverage was at full resolution. Such unsampled {@code TiledGridCoverage} uses the same coordinates + * as the originating {@link TiledGridResource}. + * + * <p>This method uses the "pixel" word for simplicity and because this method is used mostly + * for the first two dimensions, but "pixel" should be understood as "grid coverage cell".</p> * * @param coordinate coordinate in this {@code TiledGridCoverage} domain. * @param dimension dimension of the coordinate. - * @return coordinate in this {@code TiledGridCoverage} as if no subsampling was applied. + * @return coordinate in this {@code TiledGridResource} with no subsampling applied. * @throws ArithmeticException if the coordinate cannot be represented as a long integer. * - * @see #toFullResolution(Rectangle) + * @see #pixelToResourceCoordinates(Rectangle) */ - private long toFullResolution(final long coordinate, final int dimension) { + private long pixelToResourceCoordinate(final long coordinate, final int dimension) { return addExact(multiplyExact(coordinate, subsampling[dimension]), subsamplingOffsets[dimension]); } /** * Converts a cell coordinate from {@link TiledGridResource} space to {@code TiledGridCoverage} coordinate. - * This is the converse of {@link #toFullResolution(long, int)}. Note that there is a possible accuracy lost. + * This is the converse of {@link #pixelToResourceCoordinate(long, int)}. + * Note that there is a possible accuracy lost. * * @param coordinate coordinate in the {@code TiledGridResource} domain. * @param dimension dimension of the coordinate. * @return coordinates in this subsampled {@code TiledGridCoverage} domain. * @throws ArithmeticException if the coordinate cannot be represented as a long integer. */ - private long toSubsampledPixel(final long coordinate, final int dimension) { + private long resourceToPixelCoordinate(final long coordinate, final int dimension) { return floorDiv(subtractExact(coordinate, subsamplingOffsets[dimension]), subsampling[dimension]); } /** - * Converts a cell coordinate from this {@code TiledGridCoverage} coordinate space - * to the Tile Matrix Coordinate (TMC) of the tile which contains that cell. - * The TMC is relative to the full {@link TiledGridResource}, + * Converts a cell coordinate from this {@code TiledGridCoverage} coordinate space to + * the Tile Matrix Coordinate (<abbr>TMC</abbr>) of the tile which contains that cell. + * The <abbr>TMC</abbr> is relative to the full {@link TiledGridResource}, * i.e. without subtraction of {@link #tmcOfFirstTile}. * * @param coordinate coordinates in this {@code TiledGridCoverage} domain. @@ -342,8 +357,8 @@ public abstract class TiledGridCoverage extends GridCoverage { * @return Tile Matrix Coordinate (TMC) of the tile which contains the specified cell. * @throws ArithmeticException if the coordinate cannot be represented as an integer. */ - private long toTileMatrixCoordinate(final long coordinate, final int dimension) { - return floorDiv(toFullResolution(coordinate, dimension), tileSize[dimension]); + private long toResourceTileMatrixCoordinate(final long coordinate, final int dimension) { + return floorDiv(pixelToResourceCoordinate(coordinate, dimension), tileSize[dimension]); } /** @@ -425,8 +440,8 @@ public abstract class TiledGridCoverage extends GridCoverage { final long max = available .getHigh(i); // Highest valid coordinate, inclusive. final long aoiMin = sliceExtent.getLow (i); // Requested coordinate in subsampled image. final long aoiMax = sliceExtent.getHigh(i); - final long tileUp = incrementExact(toTileMatrixCoordinate(Math.min(aoiMax, max), i)); - final long tileLo = toTileMatrixCoordinate(Math.max(aoiMin, min), i); + final long tileUp = incrementExact(toResourceTileMatrixCoordinate(Math.min(aoiMax, max), i)); + final long tileLo = toResourceTileMatrixCoordinate(Math.max(aoiMin, min), i); if (tileUp <= tileLo) { final String message = Errors.forLocale(getLocale()) .getString(Errors.Keys.IllegalRange_2, aoiMin, aoiMax); @@ -437,8 +452,8 @@ public abstract class TiledGridCoverage extends GridCoverage { } } // Lower and upper coordinates in subsampled image, rounded to integer number of tiles and clipped to available data. - final long lower = /* inclusive */Math.max(toSubsampledPixel(/* inclusive */multiplyExact(tileLo, tileSize[i]), i), min); - final long upper = incrementExact(Math.min(toSubsampledPixel(decrementExact(multiplyExact(tileUp, tileSize[i])), i), max)); + final long lower = /* inclusive */Math.max(resourceToPixelCoordinate(/* inclusive */multiplyExact(tileLo, tileSize[i]), i), min); + final long upper = incrementExact(Math.min(resourceToPixelCoordinate(decrementExact(multiplyExact(tileUp, tileSize[i])), i), max)); imageSize[i] = toIntExact(subtractExact(upper, lower)); offsetAOI[i] = toIntExact(subtractExact(lower, aoiMin)); tileLower[i] = toIntExact(subtractExact(tileLo, tmcOfFirstTile[i])); @@ -448,7 +463,7 @@ public abstract class TiledGridCoverage extends GridCoverage { * Prepare an iterator over all tiles to read, together with the following properties: * - Two-dimensional conversion from pixel coordinates to "real world" coordinates. */ - final AOI iterator = new AOI(tileLower, tileUpper, offsetAOI, dimension); + final var iterator = new TileIterator(tileLower, tileUpper, offsetAOI, dimension); final Map<String,Object> properties = DeferredProperty.forGridGeometry(gridGeometry, selectedDimensions); if (deferredTileReading) { image = new TiledDeferredImage(imageSize, tileLower, properties, iterator); @@ -469,14 +484,289 @@ public abstract class TiledGridCoverage extends GridCoverage { return image; } + + + /** - * The Area Of Interest specified by user in a call to {@link #render(GridExtent)}. - * This class is also an iterator over tiles in the region of interest. + * An Area Of Interest (<abbr>AOI</abbr>) describing a tile area or sub-area to read in response to a user's request. + * {@code AOI} can be a mutable iterator over all the tiles to read ({@link TileIterator}) or an immutable snapshot + * of the iterator position as an instant ({@link Snapshot}). */ - protected final class AOI { + protected static abstract class AOI { + /** + * Tile Matrix Coordinates (TMC) relative to the enclosing {@link TiledGridCoverage}. + * Tile (0,0) is the tile in the upper-left corner of this {@link TiledGridCoverage}, + * not necessarily the tile in the upper-left corner of the image in the resource. + * + * <p>In the case of {@link Snapshot}, this array shall be considered unmodifiable. + * In the case of {@link TileIterator}, this array is initialized to a clone of + * {@link #tileLower} and is modified by calls to {@link TileIterator#next()}.</p> + */ + final int[] tmcInSubset; + + /** + * Current iterator position as an index in the array of tiles to be returned by {@link #readTiles(TileIterator)}. + * The initial position is zero. This field is incremented by calls to {@link TileIterator#next()}. + * + * @see #getTileIndexInResultArray() + */ + int indexInResultArray; + + /** + * Current iterator position as an index in the vector of tiles in the {@link TiledGridResource}. + * Tiles are assumed stored in a row-major fashion. This field is incremented by calls to {@link #next()}. + * This index is also used as key in the {@link TiledGridCoverage#rasters} map. + * + * <h4>Example</h4> + * In a GeoTIFF image, this is the index of the tile in the {@code tileOffsets} + * and {@code tileByteCounts} vectors. + */ + int indexInTileVector; + + /** + * Creates a new area of interest. + */ + AOI(final int[] tmcInSubset) { + this.tmcInSubset = tmcInSubset; + } + + /** + * Returns the enclosing coverage. + */ + abstract TiledGridCoverage getCoverage(); + + /** + * Returns the current <abbr>AOI</abbr> position in the tile matrix of the original resource. + * This method assumes that the upper-left corner of tile (0,0) in the resource starts at cell + * coordinates (0,0) of the resource. + * + * @return current <abbr>AOI</abbr> tile coordinates in original coverage resource. + */ + public final long[] getTileCoordinatesInResource() { + final long[] tmcOfFirstTile = getCoverage().tmcOfFirstTile; + final long[] coordinate = new long[tmcOfFirstTile.length]; + for (int i = 0; i < coordinate.length; i++) { + coordinate[i] = addExact(tmcOfFirstTile[i], tmcInSubset[i]); + } + return coordinate; + } + + /** + * Returns the current <abbr>AOI</abbr> position as an index in the vector of tiles of the original resource. + * Tiles are assumed stored in a row-major fashion. with the first tiles starting at index 0. + * + * @return current <abbr>AOI</abbr> tile index in original coverage resource. + */ + public final int getTileIndexInResource() { + return indexInTileVector; + } + + /** + * Returns the current <abbr>AOI</abbr> position as an index in the array of tiles to be returned + * by {@code TiledGridCoverage.readTiles(…)}. If this <abbr>AOI</abbr> is an iterator, the initial + * position is zero and is incremented by 1 in each call to {@link TileIterator#next()}. + * + * @return current <abbr>AOI</abbr> tile index in the result array of tiles. + * + * @see #readTiles(TileIterator) + */ + public final int getTileIndexInResultArray() { + return indexInResultArray; + } + + /** + * Returns the origin to assign to the tile at the current iterator position. + * See {@link TileIterator#getTileOrigin(int)} for more explanation. + * + * @see TileIterator#getTileOrigin(int) + */ + abstract int getTileOrigin(final int dimension); + + /** + * Returns the cached tile for current <abbr>AOI</abbr> position. + * + * @return cached tile at current <abbr>AOI</abbr> position, or {@code null} if none. + * + * @see #cache(Raster) + */ + public Raster getCachedTile() { + final TiledGridCoverage coverage = getCoverage(); + final Raster tile = coverage.getCachedTile(indexInTileVector); + if (tile != null) { + /* + * Found a tile, but the sample model may be different because band order may be different. + * In any cases, we need to make sure that the raster starts at the expected coordinates. + */ + final int x = getTileOrigin(X_DIMENSION); + final int y = getTileOrigin(Y_DIMENSION); + final SampleModel model = coverage.model; + if (model.equals(tile.getSampleModel())) { + if (tile.getMinX() == x && tile.getMinY() == y) { + return tile; + } + return tile.createTranslatedChild(x, y); + } + /* + * If the sample model is not the same (e.g. different bands), it must at least have the same size. + * Having a sample model of different size would probably be a bug, but we check anyway for safety. + * Note that the tile size is not necessarily equals to the sample model size. + */ + final SampleModel sm = tile.getSampleModel(); + if (sm.getWidth() == model.getWidth() && sm.getHeight() == model.getHeight()) { + final int width = tile.getWidth(); // May be smaller than sample model width. + final int height = tile.getHeight(); // Idem. + /* + * It is okay to have a different number of bands if the sample model is + * a view created by `SampleModel.createSubsetSampleModel(int[] bands)`. + * Bands can also be in a different order and still share the same buffer. + */ + Raster r = Raster.createRaster(model, tile.getDataBuffer(), new Point(x, y)); + if (r.getWidth() != width || r.getHeight() != height) { + r = r.createChild(x, y, width, height, x, y, null); + } + return r; + } + } + return null; + } + + /** + * Stores the given raster in the cache for the current <abbr>AOI</abbr> position. + * If another raster existed previously in the cache, the old raster will be reused if + * it has the same size and model, or discarded otherwise. The latter case may happen if + * {@link #getCachedTile()} determined that a cached raster exists but cannot be reused. + * + * @param tile the raster to cache. + * @return the cached raster. Should be the given {@code raster} instance, + * but this method check for concurrent caching as a paranoiac check. + * + * @see #getCachedTile() + */ + public Raster cache(final Raster tile) { + return getCoverage().cacheTile(indexInTileVector, tile); + } + + /** + * Creates an initially empty raster for the tile at the current <abbr>AOI</abbr> position. + * The sample model is {@link #model} and the minimum <var>x</var> and <var>y</var> position + * are set the the pixel coordinates in the two first dimensions of the <abbr>AOI</abbr>. + * + * <p>The raster is <em>not</em> filled with {@link #fillValues}. + * Filling, if needed, should be done by the caller.</p> + * + * @return a newly created, initially empty raster. + */ + public WritableRaster createRaster() { + final int x = getTileOrigin(X_DIMENSION); + final int y = getTileOrigin(Y_DIMENSION); + return Raster.createWritableRaster(getCoverage().model, new Point(x, y)); + } + + /** + * Returns the coordinates of the pixels to read <em>inside</em> the tile, ignoring subsampling. + * The tile upper-left corner is assumed (0,0). Therefore, the lower coordinates computed by this + * method are usually (0,0) and the rectangle size is usually the tile size, but those values may + * be different if the enclosing {@link TiledGridCoverage} contains only one (potentially big) tile. + * The rectangle may also be smaller when reading tiles on the last row or column of the tile matrix. + * + * @return pixel to read inside the tile, or {@code null} if the region is empty. + * @throws ArithmeticException if the tile coordinates overflow 32 bits integer capacity. + */ + public Rectangle getRegionInsideTile() { + final long[] lower = new long[BIDIMENSIONAL]; + final long[] upper = new long[BIDIMENSIONAL]; + if (getRegionInsideTile(lower, upper, null, BIDIMENSIONAL)) { + return new Rectangle( + toIntExact(lower[X_DIMENSION]), + toIntExact(lower[Y_DIMENSION]), + toIntExact(subtractExact(upper[X_DIMENSION], lower[X_DIMENSION])), + toIntExact(subtractExact(upper[Y_DIMENSION], lower[Y_DIMENSION]))); + } + return null; + } + + /** + * Returns the coordinates of the pixels to read <em>inside</em> the tile, ignoring subsampling. + * The tile upper-left corner is assumed (0,0). Therefore, the lower coordinates computed by this + * method are usually (0,0) and the upper coordinates are usually the tile size, but those values + * may be different if the enclosing {@link TiledGridCoverage} contains only one (potentially big) tile. + * In the latter case, the reading process is more like untiled image reading. + * The rectangle may also be smaller when reading tiles on the last row or column of the tile matrix. + * + * <p>The {@link TiledGridCoverage} subsampling is provided for convenience, + * but is constant for all tiles regardless the subregion to read. + * The same values can be obtained by {@link #getSubsampling(int)}.</p> + * + * <p>This method is a generalization of {@link #getRegionInsideTile()} to any number of dimensions.</p> + * + * @param lower a pre-allocated array where to store relative coordinates of the first pixel. + * @param upper a pre-allocated array where to store relative coordinates after the last pixel. + * @param subsampling a pre-allocated array where to store subsampling, or {@code null} if not needed. + * @param dimension number of elements to write in the {@code lower} and {@code upper} arrays. + * @return {@code true} on success, or {@code false} if the tile is empty. + */ + public boolean getRegionInsideTile(final long[] lower, final long[] upper, final int[] subsampling, int dimension) { + final TiledGridCoverage coverage = getCoverage(); + if (subsampling != null) { + System.arraycopy(coverage.subsampling, 0, subsampling, 0, dimension); + } + while (--dimension >= 0) { + final int tileSize = coverage.getTileSize(dimension); + final long tileIndex = addExact(coverage.tmcOfFirstTile[dimension], tmcInSubset[dimension]); + final long tileBase = multiplyExact(tileIndex, tileSize); + /* + * The `offset` value is usually zero or negative because the tile to read should be inside the AOI, + * e.g. at the right of the AOI left border. It may be positive if the `TiledGridCoverage` contains + * only one (potentially big) tile, so the tile reading process become a reading of untiled data. + */ + long offset = subtractExact(coverage.readExtent.getLow(dimension), tileBase); + long limit = Math.min(addExact(offset, coverage.readExtent.getSize(dimension)), tileSize); + if (offset < 0) { + /* + * Example: for `tileSize` = 10 pixels and `subsampling` = 3, + * the pixels to read are represented by black small squares below: + * + * -10 0 10 20 30 + * ┼──────────╫──────────┼──────────┼──────────╫ + * │▪▫▫▪▫▫▪▫▫▪║▫▫▪▫▫▪▫▫▪▫│▫▪▫▫▪▫▫▪▫▫│▪▫▫▪▫▫▪▫▫▪║ + * ┼──────────╫──────────┼──────────┼──────────╫ + * + * If reading the second tile, then `tileBase` = 10 and `offset` = -10. + * The first pixel to read in the second tile has a subsampling offset. + * We usually try to avoid this situation because it causes a variable + * number of white squares in tiles (4,3,3,4 in the above example), + * except when there is only 1 tile to read in which case offset is tolerated. + */ + final int s = coverage.getSubsampling(dimension); + offset %= s; + if (offset != 0) { + offset += s; + } + } + if (offset >= limit) { // Test for intersection before we adjust the limit. + return false; + } + if (dimension == X_DIMENSION && coverage.forceTileSize) { + limit = tileSize; + } + lower[dimension] = offset; + upper[dimension] = limit; + } + return true; + } + } + + + + + /** + * An iterator over the tiles to read. Instances of this class are computed by {@link #render(GridExtent)} + * and given to {@link #readTiles(TileIterator)}. The latter is the method that subclasses need to override. + */ + protected final class TileIterator extends AOI { /** * Total number of tiles in the AOI, from {@link #tileLower} inclusive to {@link #tileUpper} exclusive. - * This is the length of the array to be returned by {@link #readTiles(AOI)}. + * This is the length of the array to be returned by {@link #readTiles(TileIterator)}. */ public final int tileCountInQuery; @@ -498,12 +788,6 @@ public abstract class TiledGridCoverage extends GridCoverage { */ private final int[] offsetAOI; - /** - * Tile Matrix Coordinates (TMC) of current iterator position relative to enclosing {@code TiledGridCoverage}. - * Initial position is a clone of {@link #tileLower}. This array is modified by calls to {@link #next()}. - */ - private final int[] tmcInSubset; - /** * Pixel coordinates of current iterator position relative to the Area Of Interest specified by user. * Those coordinates are in units of the full resolution image. @@ -512,18 +796,6 @@ public abstract class TiledGridCoverage extends GridCoverage { */ private final long[] tileOffsetFull; - /** - * Current iterator position as an index in the array of tiles to be returned by {@link #readTiles(AOI)}. - * The initial position is zero. This field is incremented by calls to {@link #next()}. - */ - private int indexInResultArray; - - /** - * Current iterator position as an index in the vector of tiles in the {@link TiledGridResource}. - * Tiles are assumed stored in a row-major fashion. This field is incremented by calls to {@link #next()}. - */ - private int indexInTileVector; - /** * Creates a new Area Of Interest for the given tile indices. * @@ -532,7 +804,8 @@ public abstract class TiledGridCoverage extends GridCoverage { * @param offsetAOI pixel coordinates to assign to the upper-left corner of the subsampled region to render. * @param dimension number of dimension of the {@code TiledGridCoverage} grid extent. */ - AOI(final int[] tileLower, final int[] tileUpper, final int[] offsetAOI, final int dimension) { + TileIterator(final int[] tileLower, final int[] tileUpper, final int[] offsetAOI, final int dimension) { + super(tileLower.clone()); this.tileLower = tileLower; this.tileUpper = tileUpper; this.offsetAOI = offsetAOI; @@ -560,11 +833,10 @@ public abstract class TiledGridCoverage extends GridCoverage { assert max > Math.max(offsetAOI[i], 0) : max; } this.tileCountInQuery = tileCountInQuery; - this.tmcInSubset = tileLower.clone(); } /** - * Returns a new {@code AOI} instance over a sub-region of this Area Of Interest. + * Returns a new {@code TileIterator} instance over a sub-region of this Area Of Interest. * The region is specified by tile indices, with (0,0) being the first tile of the enclosing grid coverage. * The given region is intersected with the region of this {@code AOI}. * The {@code tileLower} and {@code tileUpper} array can have any length; @@ -573,9 +845,9 @@ public abstract class TiledGridCoverage extends GridCoverage { * * @param tileLower indices (relative to enclosing {@code TiledGridCoverage}) of the upper-left tile to read. * @param tileUpper indices (relative to enclosing {@code TiledGridCoverage}) after the bottom-right tile to read. - * @return a new {@code AOI} instance for the specified sub-region. + * @return a new {@code TileIterator} instance for the specified sub-region. */ - public AOI subset(final int[] tileLower, final int[] tileUpper) { + public TileIterator subset(final int[] tileLower, final int[] tileUpper) { final int[] offset = this.offsetAOI.clone(); final int[] lower = this.tileLower.clone(); for (int i = Math.min(tileLower.length, lower.length); --i >= 0;) { @@ -591,112 +863,43 @@ public abstract class TiledGridCoverage extends GridCoverage { for (int i = Math.min(tileUpper.length, upper.length); --i >= 0;) { upper[i] = Math.max(lower[i], Math.min(upper[i], tileUpper[i])); } - return new AOI(lower, upper, offset, offset.length); + return new TileIterator(lower, upper, offset, offset.length); } /** * Returns the enclosing coverage. */ + @Override final TiledGridCoverage getCoverage() { return TiledGridCoverage.this; } /** - * Returns the current iterator tile position in the original coverage resource. - * - * @return current iterator tile position in original coverage resource. - */ - public final long[] getTileCoordinatesInSource() { - final long[] coordinate = new long[tmcOfFirstTile.length]; - for (int i = 0; i < coordinate.length; i++) { - coordinate[i] = Math.addExact(tmcOfFirstTile[i], tmcInSubset[i]); - } - return coordinate; - } - - /** - * Returns the extent of this tile in units of the full coverage resource (without subsampling). + * Returns the extent of the current tile in units of the full coverage resource (without subsampling). * This method is a generalization to <var>n</var> dimensions of the rectangle computed by the * following code: * * {@snippet lang="java" : * WritableRaster tile = createRaster(); - * Rectangle bounds = tile.getBounds(); - * toFullResolution(bounds); // Convert in-place. + * Rectangle target = tile.getBounds(); + * Rectangle source = pixelToResourceCoordinates(bounds); * } * * @return extent of this tile in units of the full coverage resource. * - * @see #toFullResolution(Rectangle) + * @see #pixelToResourceCoordinates(Rectangle) */ - public final GridExtent getExtentInSource() { + public final GridExtent getTileExtentInResource() { final int dimension = tileOffsetFull.length; final var axes = new DimensionNameType[dimension]; final long[] lower = new long[dimension]; final long[] upper = new long[dimension]; for (int i=0; i<dimension; i++) { - lower[i] = toFullResolution(getTileOrigin(i), i); - upper[i] = Math.addExact(lower[i], getTileSize(i)); + lower[i] = pixelToResourceCoordinate(getTileOrigin(i), i); + upper[i] = addExact(lower[i], getTileSize(i)); axes [i] = readExtent.getAxisType(i).orElse(null); } - return new GridExtent(axes, lower, upper, true); - } - - /** - * Returns the current iterator position as an index in the array of tiles to be returned - * by {@link #readTiles(AOI)}. The initial position is zero. - * The position is incremented by 1 in each call to {@link #next()}. - * - * @return current iterator position in result array. - */ - public final int getIndexInResultArray() { - return indexInResultArray; - } - - /** - * Returns the cached tile for current iterator position. - * - * @return cached tile at current iterator position, or {@code null} if none. - * - * @see Snapshot#cache(Raster) - */ - public Raster getCachedTile() { - final Raster tile = rasters.get(createCacheKey(indexInTileVector)); - if (tile != null) { - /* - * Found a tile, but the sample model may be different because band order may be different. - * In both cases, we need to make sure that the raster starts at the expected coordinates. - */ - final int x = getTileOrigin(X_DIMENSION); - final int y = getTileOrigin(Y_DIMENSION); - if (model.equals(tile.getSampleModel())) { - if (tile.getMinX() == x && tile.getMinY() == y) { - return tile; - } - return tile.createTranslatedChild(x, y); - } - /* - * If the sample model is not the same (e.g. different bands), it must at least have the same size. - * Having a sample model of different size would probably be a bug, but we check anyway for safety. - * Note that the tile size is not necessarily equals to the sample model size. - */ - final SampleModel sm = tile.getSampleModel(); - if (sm.getWidth() == model.getWidth() && sm.getHeight() == model.getHeight()) { - final int width = tile.getWidth(); // May be smaller than sample model width. - final int height = tile.getHeight(); // Idem. - /* - * It is okay to have a different number of bands if the sample model is - * a view created by `SampleModel.createSubsetSampleModel(int[] bands)`. - * Bands can also be in a different order and still share the same buffer. - */ - Raster r = Raster.createRaster(model, tile.getDataBuffer(), new Point(x, y)); - if (r.getWidth() != width || r.getHeight() != height) { - r = r.createChild(x, y, width, height, x, y, null); - } - return r; - } - } - return null; + return new GridExtent(axes, lower, upper, false); } /** @@ -711,6 +914,7 @@ public abstract class TiledGridCoverage extends GridCoverage { * <li>If subsampling is larger than tile size.</li> * </ul> */ + @Override final int getTileOrigin(final int dimension) { /* * We really need `ceilDiv(…)` below, not `floorDiv(…)`. It makes no difference in the usual @@ -726,23 +930,6 @@ public abstract class TiledGridCoverage extends GridCoverage { return toIntExact(ceilDiv(tileOffsetFull[dimension], getSubsampling(dimension))); } - /** - * Creates an initially empty raster for the tile at the current iterator position. - * The sample model is {@link #model} and the minimum <var>x</var> and <var>y</var> - * coordinates are the values returned by {@link #getTileOrigin(int)} for dimensions - * of two-dimensional slices. - * - * <p>The raster is <em>not</em> filled with {@link #fillValues}. - * Filling, if needed, should be done by the caller.</p> - * - * @return a newly created, initially empty raster. - */ - public WritableRaster createRaster() { - final int x = getTileOrigin(X_DIMENSION); - final int y = getTileOrigin(Y_DIMENSION); - return Raster.createWritableRaster(model, new Point(x, y)); - } - /** * Moves the iterator position to next tile. This method should be invoked in a loop as below: * @@ -784,37 +971,21 @@ public abstract class TiledGridCoverage extends GridCoverage { } } + + + /** - * Snapshot of a {@link AOI} iterator position. Those snapshots can be created during an iteration - * for processing a tile later. For example, a {@link #readTiles(AOI)} method implementation may want - * to create a list of all tiles to load before to start the actual reading process in order to read - * the tiles in some optimal order, or for combining multiple read operations in a single operation. + * Snapshot of a {@link TileIterator} position. Those snapshots can be created during an iteration + * for processing a tile later. For example, a {@link #readTiles(TileIterator)} method implementation + * may want to create a list of all tiles to load before to start the actual reading process in order + * to read the tiles in some optimal order, or for combining multiple read operations in a single operation. */ - protected static class Snapshot { + protected static class Snapshot extends AOI { /** * The source coverage. */ private final TiledGridCoverage coverage; - /** - * Tile Matrix Coordinates (TMC) relative to the enclosing {@link TiledGridCoverage}. - * Tile (0,0) is the tile in the upper-left corner of this {@link TiledGridCoverage}, - * not necessarily the tile in the upper-left corner of the image in the resource. - */ - private final int[] tmcInSubset; - - /** - * Index of this tile in the array of tiles returned by {@link #readTiles(AOI)}. - */ - public final int indexInResultArray; - - /** - * Index of this tile in the {@link TiledGridResource}. In a GeoTIFF image, this is - * the index of the tile in the {@code tileOffsets} and {@code tileByteCounts} vectors. - * This index is also used as key in the {@link TiledGridCoverage#rasters} map. - */ - public final int indexInTileVector; - /** * Pixel coordinates of the upper-left corner of the tile. */ @@ -826,8 +997,8 @@ public abstract class TiledGridCoverage extends GridCoverage { * @param iterator the iterator for which to create a snapshot of its current position. */ public Snapshot(final AOI iterator) { + super(iterator.tmcInSubset.clone()); coverage = iterator.getCoverage(); - tmcInSubset = iterator.tmcInSubset.clone(); indexInResultArray = iterator.indexInResultArray; indexInTileVector = iterator.indexInTileVector; originX = iterator.getTileOrigin(X_DIMENSION); @@ -835,105 +1006,26 @@ public abstract class TiledGridCoverage extends GridCoverage { } /** - * Returns the coordinates of the pixel to read <em>inside</em> the tile, ignoring subsampling. - * The tile upper-left corner is assumed (0,0). Consequently, the lower coordinates are usually - * (0,0) and the upper coordinates are usually the tile size, but those values may be different - * if the enclosing {@link TiledGridCoverage} contains only one (potentially big) tile. - * In that case, the reading process is more like untiled image reading. - * - * <p>The {@link TiledGridCoverage} subsampling is provided for convenience, - * but is constant for all tiles regardless the subregion to read. - * The same values can be obtained by {@link #getSubsampling(int)}.</p> - * - * @param lower a pre-allocated array where to store relative coordinates of the first pixel. - * @param upper a pre-allocated array where to store relative coordinates after the last pixel. - * @param subsampling a pre-allocated array where to store subsampling. - * @param dimension number of elements to write in the {@code lower} and {@code upper} arrays. - * @return {@code true} on success, or {@code false} if the tile is empty. + * Returns the enclosing coverage. */ - public boolean getRegionInsideTile(final long[] lower, final long[] upper, final int[] subsampling, int dimension) { - System.arraycopy(coverage.subsampling, 0, subsampling, 0, dimension); - while (--dimension >= 0) { - final int tileSize = coverage.getTileSize(dimension); - final long tileIndex = addExact(coverage.tmcOfFirstTile[dimension], tmcInSubset[dimension]); - final long tileBase = multiplyExact(tileIndex, tileSize); - /* - * The `offset` value is usually zero or negative because the tile to read should be inside the AOI, - * e.g. at the right of the AOI left border. It may be positive if the `TiledGridCoverage` contains - * only one (potentially big) tile, so the tile reading process become a reading of untiled data. - */ - long offset = subtractExact(coverage.readExtent.getLow(dimension), tileBase); - long limit = Math.min(addExact(offset, coverage.readExtent.getSize(dimension)), tileSize); - if (offset < 0) { - /* - * Example: for `tileSize` = 10 pixels and `subsampling` = 3, - * the pixels to read are represented by black small squares below: - * - * -10 0 10 20 30 - * ┼──────────╫──────────┼──────────┼──────────╫ - * │▪▫▫▪▫▫▪▫▫▪║▫▫▪▫▫▪▫▫▪▫│▫▪▫▫▪▫▫▪▫▫│▪▫▫▪▫▫▪▫▫▪║ - * ┼──────────╫──────────┼──────────┼──────────╫ - * - * If reading the second tile, then `tileBase` = 10 and `offset` = -10. - * The first pixel to read in the second tile has a subsampling offset. - * We usually try to avoid this situation because it causes a variable - * number of white squares in tiles (4,3,3,4 in the above example), - * except when there is only 1 tile to read in which case offset is tolerated. - */ - final int s = coverage.subsampling[dimension]; - offset %= s; - if (offset != 0) { - offset += s; - } - } - if (offset >= limit) { // Test for intersection before we adjust the limit. - return false; - } - if (dimension == X_DIMENSION && coverage.forceTileSize) { - limit = tileSize; - } - lower[dimension] = offset; - upper[dimension] = limit; - } - return true; + @Override + final TiledGridCoverage getCoverage() { + return coverage; } /** - * Stores the given raster in the cache. If another raster existed previously in the cache, - * the old raster will be reused if it has the same size and model, or discarded otherwise. - * The latter case may happen if {@link AOI#getCachedTile()} determined that a cached raster - * exists but cannot be reused. + * Returns the origin to assign to the tile at the current iterator position. + * This is needed by the parent class only for the two first dimensions. * - * @param tile the raster to cache. - * @return the cached raster. Should be the given {@code raster} instance, - * but this method check for concurrent caching as a paranoiac check. - * - * @see AOI#getCachedTile() + * @see TileIterator#getTileOrigin(int) */ - public Raster cache(final Raster tile) { - final TiledGridResource.CacheKey key = coverage.createCacheKey(indexInTileVector); - Raster existing = coverage.rasters.put(key, tile); - /* - * If a tile already exists, verify if its layout is compatible with the given tile. - * If yes, we assume that the two tiles have the same content. We do this check as a - * safety but it should not happen if the caller synchronized the tile read actions. - */ - if (existing != null - && existing.getSampleModel().equals(tile.getSampleModel()) - && existing.getWidth() == tile.getWidth() - && existing.getHeight() == tile.getHeight()) - { - // Restore the existing tile in the cache, with its original position. - if (coverage.rasters.replace(key, tile, existing)) { - final int x = tile.getMinX(); - final int y = tile.getMinY(); - if (existing.getMinX() != x || existing.getMinY() != y) { - existing = existing.createTranslatedChild(x, y); - } - return existing; - } + @Override + final int getTileOrigin(final int dimension) { + switch (dimension) { + case X_DIMENSION: return originX; + case Y_DIMENSION: return originY; + default: throw new AssertionError(dimension); } - return tile; } } @@ -944,6 +1036,43 @@ public abstract class TiledGridCoverage extends GridCoverage { return new TiledGridResource.CacheKey(indexInTileVector, includedBands, subsampling, subsamplingOffsets); } + /** + * Returns a raster in the cache, or {@code null} if none. + * See {@link AOI#getCachedTile()} for more information. + */ + private Raster getCachedTile(final int indexInTileVector) { + return rasters.get(createCacheKey(indexInTileVector)); + } + + /** + * Caches the given raster. See {@link AOI#cache(Raster)} for more information. + */ + private Raster cacheTile(final int indexInTileVector, final Raster tile) { + final TiledGridResource.CacheKey key = createCacheKey(indexInTileVector); + Raster existing = rasters.put(key, tile); + /* + * If a tile already exists, verify if its layout is compatible with the given tile. + * If yes, we assume that the two tiles have the same content. We do this check as a + * safety but it should not happen if the caller synchronized the tile read actions. + */ + if (existing != null + && existing.getSampleModel().equals(tile.getSampleModel()) + && existing.getWidth() == tile.getWidth() + && existing.getHeight() == tile.getHeight()) + { + // Restore the existing tile in the cache, with its original position. + if (rasters.replace(key, tile, existing)) { + final int x = tile.getMinX(); + final int y = tile.getMinY(); + if (existing.getMinX() != x || existing.getMinY() != y) { + existing = existing.createTranslatedChild(x, y); + } + return existing; + } + } + return tile; + } + /** * Returns all tiles in the given area of interest. Tile indices are relative to this {@code TiledGridCoverage}: * (0,0) is the tile in the upper-left corner of this {@code TiledGridCoverage} (not necessarily the upper-left @@ -962,23 +1091,28 @@ public abstract class TiledGridCoverage extends GridCoverage { * @throws RuntimeException if the Java2D image cannot be created for another reason * (too many exception types to list them all). */ - protected abstract Raster[] readTiles(AOI iterator) throws IOException, DataStoreException; + protected abstract Raster[] readTiles(TileIterator iterator) throws IOException, DataStoreException; /** - * Converts raster coordinate from this {@code TiledGridCoverage} coordinate space to full resolution. - * This method removes the subsampling effect. Note that since this {@code TiledGridCoverage} uses the - * same coordinate space as {@link TiledGridResource}, the converted coordinates should be valid in - * the full resource as well. + * Converts raster coordinate from this coverage to {@code TiledGridResource} coordinate space. + * This method removes the subsampling effect, i.e. returns the coordinates that we would have if this + * coverage was at full resolution. Such unsampled {@code TiledGridCoverage} uses the same coordinates + * as the originating {@link TiledGridResource}. + * + * <p>This method uses the "pixel" word for simplicity and because this method is used for + * the two-dimensional case, but "pixel" should be understood as "grid coverage cell".</p> * - * @param bounds the rectangle to convert. Will be modified in-place. + * @param bounds the rectangle to convert. + * @return the converted rectangle. * @throws ArithmeticException if the coordinate cannot be represented as an integer. * - * @see AOI#getExtentInSource() + * @see TileIterator#getTileExtentInResource() */ - protected final void toFullResolution(final Rectangle bounds) { - bounds.x = Math.toIntExact(toFullResolution(bounds.x, X_DIMENSION)); - bounds.y = Math.toIntExact(toFullResolution(bounds.y, Y_DIMENSION)); - bounds.width = Math.multiplyExact(bounds.width, subsampling[X_DIMENSION]); - bounds.height = Math.multiplyExact(bounds.height, subsampling[Y_DIMENSION]); + protected final Rectangle pixelToResourceCoordinates(final Rectangle bounds) { + return new Rectangle( + toIntExact(pixelToResourceCoordinate(bounds.x, X_DIMENSION)), + toIntExact(pixelToResourceCoordinate(bounds.y, Y_DIMENSION)), + multiplyExact(bounds.width, subsampling[X_DIMENSION]), + multiplyExact(bounds.height, subsampling[Y_DIMENSION])); } } diff --git a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Band.java b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Band.java index ec1c0e1bf4..6da315a03f 100644 --- a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Band.java +++ b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/Band.java @@ -17,6 +17,7 @@ package org.apache.sis.storage.gdal; import java.util.List; +import java.nio.Buffer; import java.awt.Rectangle; import java.awt.image.ComponentSampleModel; import java.awt.image.DataBuffer; @@ -234,7 +235,7 @@ final class Band { } /** - * Transfers (reads or writes) sample values between <abbr>GDAL</abbr> raster and Java2D raster for one band. + * Transfers (reads or writes) sample values between <abbr>GDAL</abbr> raster and Java2D raster. * The full area of the Java2D raster is transferred. It may corresponds to a sub-area of the GDAL raster. * * <h4>Prerequisites</h4> @@ -243,52 +244,70 @@ final class Band { * <li>In read mode, the given raster shall be an instance of {@link WritableRaster}.</li> * </ul> * - * @param gdal set of handles for invoking <abbr>GDAL</abbr> functions. - * @param rwFlag {@link OpenFlag#READ} or {@link OpenFlag#WRITE}. - * @param image the <abbr>GDAL</abbr> raster which contains the band to read or write. - * @param aoi region of the image to read or write. (0,0) is the upper-left pixel. - * @param raster the Java2D raster where to store of fetch the values to read or write. - * @param band band of sample values in the Java2D raster. - * @return whether the operation was successful according <abbr>GDAL</abbr>. + * <h4>Alternatives</h4> + * {@code GDALReadBlock} would have been a more efficient method, but we do not use it because the actual + * tile size given to this method is sometime different than the natural block size of the data set. + * This difference happens when <abbr>GDAL</abbr> uses block size as width as the image and 1 row in height. + * Such block sizes are inefficient for Apache <abbr>SIS</abbr>, therefore we request a different size. + * + * <p>A yet more efficient approach would be to use {@code GDALRasterBlock::GetLockedBlockRef(…)} + * for copying the data from the cache without intermediate buffer. But the latter is C++ API. + * We cannot use it as of Java 22.</p> + * + * @param gdal set of handles for invoking <abbr>GDAL</abbr> functions. + * @param readWriteFlags {@link OpenFlag#READ} or {@link OpenFlag#WRITE}. + * @param selectedBands all bands to read, in the same order as they appear in the given Java2D raster. + * @param resourceType the <abbr>GDAL</abbr> data type of all specified bands, as stored in the resource. + * @param resourceBounds region to read or write in resource coordinates. (0,0) is the upper-left pixel. + * @param raster the Java2D raster where to store of fetch the values to read or write. + * @param rasterBounds region to write or read in raster coordinates. + * @param transferBuffer a temporary buffer used for copying data. + * @return whether the operation was successful according <abbr>GDAL</abbr>. * @throws ClassCastException if an above-documented prerequisite is not true. * @throws DataStoreException if <var>GDAL</var> reported a warning or fatal error. */ - final boolean transfer(final GDAL gdal, final int rwFlag, - final TiledResource image, final Rectangle aoi, // GDAL model - final Raster raster, final int band) // Java2D model + static boolean transfer(final GDAL gdal, + final int readWriteFlags, + final Band[] selectedBands, + final DataType resourceType, + final Rectangle resourceBounds, + final Raster raster, + final Rectangle rasterBounds, + final MemorySegment transferBuffer) throws DataStoreException { - if (rwFlag == OpenFlag.READ && !(raster instanceof WritableRaster)) { + if (readWriteFlags == OpenFlag.READ && !(raster instanceof WritableRaster)) { throw new ClassCastException(); } - final var model = (ComponentSampleModel) raster.getSampleModel(); // See prerequisites in Javadoc. - final var data = raster.getDataBuffer(); - final int dataSize = DataBuffer.getDataTypeSize(data.getDataType()) / Byte.SIZE; - final var buffer = RasterFactory.wrapAsBuffer(data, model.getBankIndices()[band]); - buffer.position(model.getOffset(raster.getMinX() - raster.getSampleModelTranslateX(), - raster.getMinY() - raster.getSampleModelTranslateY(), band)); - final int err; - try (Arena arena = Arena.ofConfined()) { - /* - * TODO: we wanted to use `MemorySegment.ofBuffer` but it does not work. - * We get an "IllegalArgumentException: Heap segment not allowed" error. - * For now we copy in a temporary array as a workaround, but it needs to - * be replaced by a call to GetLockedBlockRef. - */ - MemorySegment tmp = arena.allocate(Math.multiplyFull(buffer.remaining(), dataSize)); - err = (int) gdal.rasterIO.invokeExact(handle, rwFlag, - aoi.x, aoi.y, aoi.width, aoi.height, - tmp, - raster.getWidth(), - raster.getHeight(), - image.dataType.forDataBufferType(data.getDataType()).ordinal(), - Math.multiplyExact(dataSize, model.getPixelStride()), - Math.multiplyExact(dataSize, model.getScanlineStride())); - - MemorySegment.ofBuffer(buffer).copyFrom(tmp); - } catch (Throwable e) { - throw GDAL.propagate(e); + final var sampleModel = (ComponentSampleModel) raster.getSampleModel(); // See prerequisites in Javadoc. + final var dataBuffer = raster.getDataBuffer(); + final int dataSize = DataBuffer.getDataTypeSize(dataBuffer.getDataType()) / Byte.SIZE; + final int[] bankIndices = sampleModel.getBankIndices(); + for (int i=0; i < selectedBands.length; i++) { + assert raster.getBounds().contains(rasterBounds) : rasterBounds; + final Buffer buffer = RasterFactory.wrapAsBuffer(dataBuffer, bankIndices[i]) + .position(sampleModel.getOffset( + rasterBounds.x - raster.getSampleModelTranslateX(), + rasterBounds.y - raster.getSampleModelTranslateY(), i)); + final int err; + try { + assert transferBuffer.byteSize() >= Math.multiplyFull(rasterBounds.width, rasterBounds.height) * dataSize; + err = (int) gdal.rasterIO.invokeExact(selectedBands[i].handle, readWriteFlags, + resourceBounds.x, resourceBounds.y, resourceBounds.width, resourceBounds.height, + transferBuffer, + rasterBounds.width, + rasterBounds.height, + resourceType.forDataBufferType(dataBuffer.getDataType()).ordinal(), + Math.multiplyExact(dataSize, sampleModel.getPixelStride()), + Math.multiplyExact(dataSize, sampleModel.getScanlineStride())); + } catch (Throwable e) { + throw GDAL.propagate(e); + } + if (!ErrorHandler.checkCPLErr(err)) { + return false; + } + MemorySegment.ofBuffer(buffer).copyFrom(transferBuffer); } - return ErrorHandler.checkCPLErr(err); + return true; } } diff --git a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledCoverage.java b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledCoverage.java index b68297fff0..e4eccc4ec2 100644 --- a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledCoverage.java +++ b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledCoverage.java @@ -20,6 +20,8 @@ import java.io.IOException; import java.awt.Rectangle; import java.awt.image.Raster; import java.awt.image.WritableRaster; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; import org.opengis.util.GenericName; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.base.TiledGridCoverage; @@ -57,6 +59,13 @@ final class TiledCoverage extends TiledGridCoverage { return owner.getIdentifier().orElse(null); } + /** + * Returns the length of tiles in bytes. + */ + private long getTileLength() { + return Math.ceilDiv(Math.multiplyFull(model.getWidth(), model.getHeight()) * owner.dataType.numBits, Byte.SIZE); + } + /** * Returns all tiles in the given area of interest. Tile indices (0,0) locates the tile in the upper-left corner * of this {@code TiledGridCoverage} (not necessarily the upper-left corner of the {@link TiledGridResource}). @@ -65,21 +74,29 @@ final class TiledCoverage extends TiledGridCoverage { * * @param iterator an iterator over the tiles that intersect the Area Of Interest specified by user. * @return tiles decoded from the {@link TiledGridResource}. + * @throws ArithmeticException if an integer overflow occurred. */ @Override - protected Raster[] readTiles(final AOI iterator) throws IOException, DataStoreException { + protected Raster[] readTiles(final TileIterator iterator) throws IOException, DataStoreException { synchronized (owner.getSynchronizationLock()) { - final var result = new Raster[iterator.tileCountInQuery]; - try { + final Band[] bands = owner.bands(includedBands); + final GDAL gdal = owner.parent.getProvider().GDAL(); + final var result = new WritableRaster[iterator.tileCountInQuery]; + try (Arena arena = Arena.ofConfined()) { + final MemorySegment transferBuffer = arena.allocate(getTileLength()); do { final WritableRaster tile = iterator.createRaster(); - final Rectangle bounds = tile.getBounds(); - toFullResolution(bounds); - owner.transfer(OpenFlag.READ, bounds, tile, includedBands); - result[iterator.getIndexInResultArray()] = tile; + final Rectangle target = iterator.getRegionInsideTile(); + target.x = Math.addExact(target.x, tile.getMinX()); + target.y = Math.addExact(target.y, tile.getMinY()); + final Rectangle source = pixelToResourceCoordinates(target); + if (!Band.transfer(gdal, OpenFlag.READ, bands, owner.dataType, source, tile, target, transferBuffer)) { + break; // Exception will be thrown by `throwOnFailure(…)` + } + result[iterator.getTileIndexInResultArray()] = tile; } while (iterator.next()); } finally { - ErrorHandler.report(owner.parent, "read"); // Public caller of this method. + ErrorHandler.throwOnFailure(owner.parent, "read"); // Public caller of this method. } return result; } diff --git a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java index 3cfb673508..0c9d7fa60f 100644 --- a/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java +++ b/incubator/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java @@ -23,11 +23,9 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.Optional; import java.awt.Dimension; -import java.awt.Rectangle; import java.awt.image.ColorModel; import java.awt.image.SampleModel; import java.awt.image.BandedSampleModel; -import java.awt.image.Raster; import java.lang.foreign.Arena; import java.lang.foreign.ValueLayout; import java.lang.foreign.MemorySegment; @@ -363,6 +361,24 @@ final class TiledResource extends TiledGridResource { return 1; } + /** + * Returns the bands in the given indices. + * + * @param bandIndices indices of the selected bands, or {@code null} for all bands. + * @return specified bands. May be a reference to internal array: do not modify. + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + final Band[] bands(final int[] bandIndices) { + if (bandIndices == null) { + return bands; + } + var selectedBands = new Band[bandIndices.length]; + for (int i=0; i<bandIndices.length; i++) { + selectedBands[i] = bands[bandIndices[i]]; + } + return selectedBands; + } + /** * Creates the color model and sample model. * This method stores the results in {@link #sampleModel} and {@link #colorModel}, @@ -373,18 +389,10 @@ final class TiledResource extends TiledGridResource { * through the {@link Subset} constructor. Therefore, this method relies on the * error handling setup by {@code read(…)}. * - * @param bandIndices indices of the selected bands. + * @param bandIndices indices of the selected bands, or {@code null} for all bands. */ private void createColorAndSampleModel(final int[] bandIndices) throws DataStoreException { - final Band[] selectedBands; - if (bandIndices == null) { - selectedBands = bands; - } else { - selectedBands = new Band[bandIndices.length]; - for (int i=0; i<bandIndices.length; i++) { - selectedBands[i] = bands[bandIndices[i]]; - } - } + final Band[] selectedBands = bands(bandIndices); final GDAL gdal = parent.getProvider().GDAL(); int[] palette = null; int paletteIndex = 0; @@ -508,37 +516,6 @@ final class TiledResource extends TiledGridResource { return new int[] {tileWidth, tileHeight}; } - /** - * Transfers (reads or writes) sample values between <abbr>GDAL</abbr> raster and Java2D raster. - * The full area of the Java2D raster is transferred. It may corresponds to a sub-area of the GDAL raster. - * - * <h4>Prerequisites</h4> - * <ul> - * <li>The Java2D raster shall use a {@link ComponentSampleModel}.</li> - * <li>In read mode, the given raster shall be an instance of {@link WritableRaster}.</li> - * </ul> - * - * @param rwFlag {@link OpenFlag#READ} or {@link OpenFlag#WRITE}. - * @param aoi region of the image to read or write. (0,0) is the upper-left pixel. - * @param raster the Java2D raster where to store of fetch the values to read or write. - * @param bandIndices bands of sample values in the Java2D raster, or {@code null} for all. - * @return whether the operation was successful according <abbr>GDAL</abbr>. - * @throws ClassCastException if an above-documented prerequisite is not true. - * @throws DataStoreException if <var>GDAL</var> reported a warning or fatal error. - */ - final boolean transfer(final int rwFlag, final Rectangle aoi, final Raster raster, final int[] bandIndices) - throws DataStoreException - { - final GDAL gdal = parent.getProvider().GDAL(); - final int n = (bandIndices != null) ? bandIndices.length : bands.length; - boolean success = true; - for (int i=0; i<n; i++) { - final Band band = bands[(bandIndices != null) ? bandIndices[i] : i]; - success &= band.transfer(gdal, rwFlag, this, aoi, raster, i); - } - return success; - } - /** * Loads a subset of the grid coverage represented by this resource. * The actual loading may be deferred until a tile is requested for the first time. diff --git a/incubator/src/org.apache.sis.storage.gimi/main/org/apache/sis/storage/gimi/internal/MatrixGridRessource.java b/incubator/src/org.apache.sis.storage.gimi/main/org/apache/sis/storage/gimi/internal/MatrixGridRessource.java index 61f5328da3..6cbf6d0d35 100644 --- a/incubator/src/org.apache.sis.storage.gimi/main/org/apache/sis/storage/gimi/internal/MatrixGridRessource.java +++ b/incubator/src/org.apache.sis.storage.gimi/main/org/apache/sis/storage/gimi/internal/MatrixGridRessource.java @@ -113,17 +113,18 @@ public abstract class MatrixGridRessource extends TiledGridResource { } @Override - protected Raster[] readTiles(AOI iterator) throws IOException, DataStoreException { + protected Raster[] readTiles(final TileIterator iterator) throws IOException, DataStoreException { final Raster[] result = new Raster[iterator.tileCountInQuery]; synchronized (MatrixGridRessource.this.getSynchronizationLock()) { do { final Raster tile = iterator.getCachedTile(); if (tile != null) { - result[iterator.getIndexInResultArray()] = tile; + result[iterator.getTileIndexInResultArray()] = tile; } else { - long[] tileCoord = iterator.getTileCoordinatesInSource(); + long[] tileCoord = iterator.getTileCoordinatesInResource(); final RenderedImage image = getTileImage(tileCoord); - result[iterator.getIndexInResultArray()] = image instanceof BufferedImage ? ((BufferedImage)image).getRaster() : image.getData(); + result[iterator.getTileIndexInResultArray()] = + (image instanceof BufferedImage) ? ((BufferedImage)image).getRaster() : image.getData(); } } while (iterator.next()); }