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

Reply via email to