You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/12/06 15:15:30 UTC

[sis] branch geoapi-4.0 updated (9824159 -> 570e087)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from 9824159  Handle reduced-resolution (overview) images as levels in a pyramid of images (Cloud Optimized GeoTIFF convention).
     new e96b37e  `ComputedImage.prefetch(Rectangle)` method for notifying that many tiles will be fetched. It allows implementations to allocate and release resources once for the group of tiles, or to compute them in some specific order (e.g. sequential order during I/O operations).
     new 57da403  Support the reading of tiles at `RenderedImage.getTile(int, int)` invocation time. This is enabled when loading mode is `RasterLoadingStrategy.AT_GET_TILE_TIME`.
     new 570e087  Parameter values were not properly compared when the value is a `double[]` array.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../apache/sis/image/BandedSampleConverter.java    |  20 +-
 .../java/org/apache/sis/image/ComputedImage.java   |  22 +++
 .../java/org/apache/sis/image/ImageAdapter.java    |  21 ++-
 .../java/org/apache/sis/image/PlanarImage.java     |  13 ++
 .../java/org/apache/sis/image/PrefetchedImage.java |  22 ++-
 .../java/org/apache/sis/image/ResampledImage.java  |  28 +++
 .../org/apache/sis/image/SourceAlignedImage.java   |  21 +++
 .../internal/coverage/j2d/BatchComputedImage.java  | 210 +++++++++++++++++++++
 .../sis/internal/coverage/j2d/ImageUtilities.java  |  73 ++++++-
 .../coverage/MultiResolutionCoverageLoader.java    |   2 +
 .../sis/parameter/DefaultParameterValue.java       |   4 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java |   2 +-
 .../sis/internal/storage/TiledDeferredImage.java   | 110 +++++++++++
 .../sis/internal/storage/TiledGridCoverage.java    |  66 +++++--
 .../sis/internal/storage/TiledGridResource.java    |  23 ++-
 15 files changed, 607 insertions(+), 30 deletions(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BatchComputedImage.java
 create mode 100644 storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledDeferredImage.java

[sis] 03/03: Parameter values were not properly compared when the value is a `double[]` array.

Posted by de...@apache.org.
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

commit 570e087f34d244025f41168eca00d3060565da23
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Dec 6 16:12:22 2021 +0100

    Parameter values were not properly compared when the value is a `double[]` array.
---
 .../src/main/java/org/apache/sis/parameter/DefaultParameterValue.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValue.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValue.java
index c1969a8..a3a4347 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValue.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValue.java
@@ -117,7 +117,7 @@ import static org.apache.sis.util.Utilities.deepEquals;
  * for modifying the behavior of all getter and setter methods.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.2
  *
  * @param  <T>  the type of the value stored in this parameter.
  *
@@ -836,7 +836,7 @@ convert:            if (componentType != null) {
                 if (getClass() == object.getClass()) {
                     final DefaultParameterValue<?> that = (DefaultParameterValue<?>) object;
                     return Objects.equals(descriptor, that.descriptor) &&
-                           Objects.equals(value,      that.value) &&
+                           Objects.deepEquals(value,  that.value) &&
                            Objects.equals(unit,       that.unit);
                 }
             } else if (object instanceof ParameterValue<?>) {

[sis] 01/03: `ComputedImage.prefetch(Rectangle)` method for notifying that many tiles will be fetched. It allows implementations to allocate and release resources once for the group of tiles, or to compute them in some specific order (e.g. sequential order during I/O operations).

Posted by de...@apache.org.
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

commit e96b37e1a3b919d40603926ebade38306ec2e92d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Dec 6 15:54:53 2021 +0100

    `ComputedImage.prefetch(Rectangle)` method for notifying that many tiles will be fetched.
    It allows implementations to allocate and release resources once for the group of tiles,
    or to compute them in some specific order (e.g. sequential order during I/O operations).
---
 .../apache/sis/image/BandedSampleConverter.java    | 20 +++++-
 .../java/org/apache/sis/image/ComputedImage.java   | 22 +++++++
 .../java/org/apache/sis/image/ImageAdapter.java    | 21 ++++++-
 .../java/org/apache/sis/image/PlanarImage.java     | 13 ++++
 .../java/org/apache/sis/image/PrefetchedImage.java | 22 +++++--
 .../java/org/apache/sis/image/ResampledImage.java  | 28 +++++++++
 .../org/apache/sis/image/SourceAlignedImage.java   | 21 +++++++
 .../sis/internal/coverage/j2d/ImageUtilities.java  | 73 +++++++++++++++++++---
 8 files changed, 206 insertions(+), 14 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
index 2d7522e..04fa3c8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
@@ -36,6 +36,7 @@ import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
 import org.apache.sis.internal.coverage.j2d.WriteSupport;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.util.Numbers;
+import org.apache.sis.util.Disposable;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.measure.NumberRange;
@@ -61,7 +62,7 @@ import org.apache.sis.measure.NumberRange;
  * In such case, writing converted values will cause the corresponding source values to be updated too.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -342,6 +343,23 @@ class BandedSampleConverter extends ComputedImage {
     }
 
     /**
+     * Notifies the source image that tiles will be computed soon in the given region.
+     * If the source image is an instance of {@link PlanarImage}, then this method
+     * forwards the notification to it. Otherwise default implementation does nothing.
+     */
+    @Override
+    protected Disposable prefetch(final Rectangle tiles) {
+        final RenderedImage source = getSource();
+        if (source instanceof PlanarImage) {
+            final Rectangle pixels = ImageUtilities.tilesToPixels(this, tiles);
+            ImageUtilities.clipBounds(source, pixels);
+            return ((PlanarImage) source).prefetch(ImageUtilities.pixelsToTiles(source, pixels));
+        } else {
+            return super.prefetch(tiles);
+        }
+    }
+
+    /**
      * A {@code BandedSampleConverter} capable to retro-propagate the changes to the source coverage.
      * This class contains the inverse of all {@link MathTransform1D} given to the parent class.
      */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
index 12d3b58..2ad2aa3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
@@ -573,6 +573,28 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
     }
 
     /**
+     * Notifies this image that tiles will be computed soon in the given region.
+     * This method is invoked by {@link ImageProcessor#prefetch(RenderedImage, Rectangle)}
+     * before to request (potentially in multi-threads) all tiles in the area of interest.
+     * If the returned {@code Disposable} is non-null, {@code ImageProcessor} guarantees
+     * that the {@link Disposable#dispose()} method will be invoked after the prefetch
+     * operation completed, successfully or not.
+     *
+     * <p>The default implementation does nothing. Subclasses can override this method
+     * if they need to allocate and release resources once for a group of tiles.</p>
+     *
+     * @param  tiles  indices of the tiles which will be prefetched.
+     * @return handler on which to invoke {@code dispose()} after the prefetch operation
+     *         completed (successfully or not), or {@code null} if none.
+     *
+     * @since 1.2
+     */
+    @Override
+    protected Disposable prefetch(Rectangle tiles) {
+        return null;
+    }
+
+    /**
      * Returns whether any tile is under computation or is checked out for writing.
      * There is two reasons why this method may return {@code true}:
      *
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
index c75124d..1482872 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
@@ -25,6 +25,7 @@ import java.awt.image.RenderedImage;
 import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Disposable;
 
 
 /**
@@ -41,7 +42,7 @@ import org.apache.sis.util.ArgumentChecks;
  * and forward {@link #getTile(int, int)}, {@link #getData()} and other data methods to the source image.</div>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -124,6 +125,24 @@ abstract class ImageAdapter extends PlanarImage {
     @Override public final WritableRaster copyData(WritableRaster r) {return source.copyData(r);}
 
     /**
+     * Notifies the source image that tiles will be computed soon in the given region.
+     * If the source image is an instance of {@link PlanarImage}, then this method
+     * forwards the notification to it. Otherwise default implementation does nothing.
+     */
+    @Override
+    protected Disposable prefetch(final Rectangle tiles) {
+        if (source instanceof PlanarImage) {
+            /*
+             * Forwarding directly is possible because the contract
+             * of this class said that tile indices must be the same.
+             */
+            return ((PlanarImage) source).prefetch(tiles);
+        } else {
+            return super.prefetch(tiles);
+        }
+    }
+
+    /**
      * Compares the given object with this image for equality. This method should be quick and compare
      * how images compute their values from their sources; it should not compare the actual pixel values.
      *
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index ff230d7..a778f23 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -28,6 +28,7 @@ import java.awt.image.RenderedImage;
 import java.util.Vector;
 import java.util.function.DoubleUnaryOperator;
 import org.apache.sis.util.Classes;
+import org.apache.sis.util.Disposable;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.util.Numerics;
@@ -510,6 +511,18 @@ public abstract class PlanarImage implements RenderedImage {
     }
 
     /**
+     * Notifies this image that tiles will be computed soon in the given region.
+     * The method contract is given by {@link ComputedImage#prefetch(Rectangle)}.
+     *
+     * @param  tiles  indices of the tiles which will be prefetched.
+     * @return handler on which to invoke {@code dispose()} after the prefetch operation
+     *         completed (successfully or not), or {@code null} if none.
+     */
+    Disposable prefetch(Rectangle tiles) {
+        return null;
+    }
+
+    /**
      * Verifies whether image layout information are consistent. This method verifies that the coordinates
      * of image upper-left corner are equal to the coordinates of the upper-left corner of the tile in the
      * upper-left corner, and that image size is equal to the sum of the sizes of all tiles. Compatibility
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
index fd15bb9..989c58e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
@@ -30,6 +30,7 @@ import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
 import org.apache.sis.internal.coverage.j2d.TilePlaceholder;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Disposable;
 
 
 /**
@@ -118,10 +119,23 @@ final class PrefetchedImage extends PlanarImage implements TileErrorHandler.Exec
         numYTiles = ti.height;
         tiles     = new Raster[Math.multiplyExact(numYTiles, numXTiles)];
         worker.setErrorHandler(errorHandler, ImageProcessor.class, "prefetch");
-        if (parallel) {
-            worker.parallelReadFrom(source);
-        } else {
-            worker.readFrom(source);
+        final Disposable ph = (source instanceof PlanarImage) ? ((PlanarImage) source).prefetch(ti) : null;
+        try {
+            if (parallel) {
+                worker.parallelReadFrom(source);
+            } else {
+                worker.readFrom(source);
+            }
+        } catch (Throwable ex) {
+            if (ph != null) try {
+                ph.dispose();
+            } catch (Throwable e) {
+                ex.addSuppressed(e);
+            }
+            throw ex;
+        }
+        if (ph != null) {
+            ph.dispose();
         }
         /*
          * If an error occurred during a tile computation, the array element corresponding
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
index f348c61..9f8ded6 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
@@ -43,6 +43,7 @@ import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Disposable;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
@@ -877,6 +878,33 @@ public class ResampledImage extends ComputedImage {
     }
 
     /**
+     * Notifies the source image that tiles will be computed soon in the given region.
+     * If the source image is an instance of {@link ComputedImage}, then this method
+     * forwards the notification to it. Otherwise default implementation does nothing.
+     *
+     * @since 1.2
+     */
+    @Override
+    protected Disposable prefetch(final Rectangle tiles) {
+        final RenderedImage source = getSource();
+        if (source instanceof PlanarImage) try {
+            final Dimension s = interpolation.getSupportSize();
+            Rectangle pixels = ImageUtilities.tilesToPixels(this, tiles);
+            final Rectangle2D bounds = new Rectangle2D.Double(
+                    pixels.x      -  0.5  *  s.width,
+                    pixels.y      -  0.5  *  s.height,
+                    pixels.width  + (double) s.width,
+                    pixels.height + (double) s.height);
+            pixels = (Rectangle) Shapes2D.transform(MathTransforms.bidimensional(toSource), bounds, pixels);
+            ImageUtilities.clipBounds(source, pixels);
+            return ((PlanarImage) source).prefetch(ImageUtilities.pixelsToTiles(source, pixels));
+        } catch (TransformException e) {
+            recoverableException("prefetch", e);
+        }
+        return super.prefetch(tiles);
+    }
+
+    /**
      * Compares the given object with this image for equality. This method returns {@code true}
      * if the given object is non-null, is an instance of the exact same class than this image,
      * has equal sources and do the same resampling operation (same interpolation method,
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java
index 1c3724e..e0602ef 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java
@@ -17,10 +17,12 @@
 package org.apache.sis.image;
 
 import java.util.Set;
+import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
 import java.awt.image.RenderedImage;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Disposable;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.internal.jdk9.JDK9;
 
@@ -184,4 +186,23 @@ abstract class SourceAlignedImage extends ComputedImage {
     @Override public final int getTileHeight()      {return getSource().getTileHeight();}
     @Override public final int getTileGridXOffset() {return getSource().getTileGridXOffset();}
     @Override public final int getTileGridYOffset() {return getSource().getTileGridYOffset();}
+
+    /**
+     * Notifies the source image that tiles will be computed soon in the given region.
+     * If the source image is an instance of {@link PlanarImage}, then this method
+     * forwards the notification to it. Otherwise default implementation does nothing.
+     */
+    @Override
+    protected Disposable prefetch(final Rectangle tiles) {
+        final RenderedImage source = getSource();
+        if (source instanceof PlanarImage) {
+            /*
+             * Forwarding directly is possible because the contract
+             * of this class said that tile indices must be the same.
+             */
+            return ((PlanarImage) source).prefetch(tiles);
+        } else {
+            return super.prefetch(tiles);
+        }
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
index 948e5fe..200d66a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
@@ -40,6 +40,10 @@ import org.apache.sis.util.resources.Vocabulary;
 
 import static java.lang.Math.abs;
 import static java.lang.Math.rint;
+import static java.lang.Math.floorDiv;
+import static java.lang.Math.addExact;
+import static java.lang.Math.toIntExact;
+import static org.apache.sis.internal.jdk9.JDK9.multiplyFull;
 import static org.apache.sis.internal.util.Numerics.COMPARISON_THRESHOLD;
 
 
@@ -49,7 +53,7 @@ import static org.apache.sis.internal.util.Numerics.COMPARISON_THRESHOLD;
  * (see {@code *Factory} classes for creating those objects).
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -498,7 +502,7 @@ public final class ImageUtilities extends Static {
      * @return tile index for the given pixel coordinate.
      */
     public static int pixelToTileX(final RenderedImage image, final int x) {
-        return Math.toIntExact(Math.floorDiv((x - (long) image.getTileGridXOffset()), image.getTileWidth()));
+        return toIntExact(floorDiv((x - (long) image.getTileGridXOffset()), image.getTileWidth()));
     }
 
     /**
@@ -509,7 +513,7 @@ public final class ImageUtilities extends Static {
      * @return tile index for the given pixel coordinate.
      */
     public static int pixelToTileY(final RenderedImage image, final int y) {
-        return Math.toIntExact(Math.floorDiv((y - (long) image.getTileGridYOffset()), image.getTileHeight()));
+        return toIntExact(floorDiv((y - (long) image.getTileGridYOffset()), image.getTileHeight()));
     }
 
     /**
@@ -517,11 +521,12 @@ public final class ImageUtilities extends Static {
      * The returned value is a coordinate of the pixel in upper-left corner.
      *
      * @param  image  the image containing tiles.
-     * @param  tileX  the tile coordinate for which to get pixel coordinate.
+     * @param  tileX  the tile index for which to get pixel coordinate.
      * @return smallest <var>x</var> pixel coordinate inside the tile.
      */
     public static int tileToPixelX(final RenderedImage image, final int tileX) {
-        return Math.addExact(Math.multiplyExact(tileX, image.getTileWidth()), image.getTileGridXOffset());
+        // Following `long` arithmetic never overflows even if all values are `Integer.MAX_VALUE`.
+        return toIntExact(multiplyFull(tileX, image.getTileWidth()) + image.getTileGridXOffset());
     }
 
     /**
@@ -529,11 +534,63 @@ public final class ImageUtilities extends Static {
      * The returned value is a coordinate of the pixel in upper-left corner.
      *
      * @param  image  the image containing tiles.
-     * @param  tileY  the tile coordinate for which to get pixel coordinate.
+     * @param  tileY  the tile index for which to get pixel coordinate.
      * @return smallest <var>y</var> pixel coordinate inside the tile.
      */
     public static int tileToPixelY(final RenderedImage image, final int tileY) {
-        return Math.addExact(Math.multiplyExact(tileY, image.getTileHeight()), image.getTileGridYOffset());
+        return toIntExact(multiplyFull(tileY, image.getTileHeight()) + image.getTileGridYOffset());
+    }
+
+    /**
+     * Converts pixel coordinates to pixel indices.
+     * This method does <strong>not</strong> clip the rectangle to image bounds.
+     *
+     * @param  image   the image containing tiles.
+     * @param  pixels  the pixel coordinates for which to get tile indices.
+     * @return tile indices that fully contain the pixel coordinates.
+     */
+    public static Rectangle pixelsToTiles(final RenderedImage image, final Rectangle pixels) {
+        final Rectangle r = new Rectangle();
+        if (!pixels.isEmpty()) {
+            int  size;
+            long offset, shifted;
+            size     = image.getTileWidth();
+            offset   = image.getTileGridXOffset();
+            shifted  = pixels.x - offset;
+            r.x      = toIntExact(floorDiv(shifted, size));
+            r.width  = toIntExact(floorDiv(shifted + (pixels.width - 1), size) - r.x + 1);
+            size     = image.getTileHeight();
+            offset   = image.getTileGridYOffset();
+            shifted  = pixels.y - offset;
+            r.y      = toIntExact(floorDiv(shifted, size));
+            r.height = toIntExact(floorDiv(shifted + (pixels.height - 1), size) - r.y + 1);
+        }
+        return r;
+    }
+
+    /**
+     * Converts tile indices to pixel coordinate inside the tiles.
+     * Tiles will be fully included in the returned range of pixel indices.
+     * This method does <strong>not</strong> clip the rectangle to image bounds.
+     *
+     * @param  image  the image containing tiles.
+     * @param  tiles  the tile indices for which to get pixel coordinates.
+     * @return pixel coordinates that fully contain the tiles.
+     */
+    public static Rectangle tilesToPixels(final RenderedImage image, final Rectangle tiles) {
+        final Rectangle r = new Rectangle();
+        if (!tiles.isEmpty()) {
+            int size, offset;
+            size     = image.getTileWidth();
+            offset   = image.getTileGridXOffset();
+            r.x      = toIntExact(multiplyFull(tiles.x, size) + offset);
+            r.width  = toIntExact(((((long) tiles.x) + tiles.width) * size) + offset - r.x);
+            size     = image.getTileHeight();
+            offset   = image.getTileGridYOffset();
+            r.y      = toIntExact(multiplyFull(tiles.y, size) + offset);
+            r.height = toIntExact(((((long) tiles.y) + tiles.height) * size) + offset - r.y);
+        }
+        return r;
     }
 
     /**
@@ -553,7 +610,7 @@ public final class ImageUtilities extends Static {
         if (bounds.isEmpty()) {
             throw new RasterFormatException(Resources.format(Resources.Keys.EmptyTileOrImageRegion));
         }
-        final int afterLastRow = Math.addExact(bounds.y, bounds.height);
+        final int afterLastRow = addExact(bounds.y, bounds.height);
         int size;
         try {
             size = DataBuffer.getDataTypeSize(dataType);

[sis] 02/03: Support the reading of tiles at `RenderedImage.getTile(int, int)` invocation time. This is enabled when loading mode is `RasterLoadingStrategy.AT_GET_TILE_TIME`.

Posted by de...@apache.org.
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

commit 57da403b954c198c6d61309e3ee102af5605aaa2
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Dec 6 16:04:21 2021 +0100

    Support the reading of tiles at `RenderedImage.getTile(int, int)` invocation time.
    This is enabled when loading mode is `RasterLoadingStrategy.AT_GET_TILE_TIME`.
---
 .../internal/coverage/j2d/BatchComputedImage.java  | 210 +++++++++++++++++++++
 .../coverage/MultiResolutionCoverageLoader.java    |   2 +
 .../org/apache/sis/storage/geotiff/DataSubset.java |   2 +-
 .../sis/internal/storage/TiledDeferredImage.java   | 110 +++++++++++
 .../sis/internal/storage/TiledGridCoverage.java    |  66 +++++--
 .../sis/internal/storage/TiledGridResource.java    |  23 ++-
 6 files changed, 399 insertions(+), 14 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BatchComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BatchComputedImage.java
new file mode 100644
index 0000000..2d596a8
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/BatchComputedImage.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.util.Map;
+import java.util.Collections;
+import java.awt.Rectangle;
+import java.awt.Image;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.awt.image.RenderedImage;
+import java.awt.image.SampleModel;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.image.ComputedImage;
+import org.apache.sis.internal.jdk9.JDK9;
+import org.apache.sis.util.Disposable;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * A computed image for which it is more efficient to compute tiles in batch instead of one-by-one.
+ * This class is useful only when users may prefetch in advance groups of tiles by calls to the
+ * {@link org.apache.sis.image.ImageProcessor#prefetch(RenderedImage, Rectangle)} method.
+ *
+ * <h2>Caching</h2>
+ * Implementations should manage their own cache for avoiding to compute the same tiles many times.
+ * The caching mechanism inherited from {@link ComputedImage} is less useful here.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+public abstract class BatchComputedImage extends ComputedImage {
+    /**
+     * Image properties, or an empty map if none.
+     * May contain instances of {@link DeferredProperty}.
+     */
+    private final Map<String,Object> properties;
+
+    /**
+     * Tiles fetched by a calls to {@link #prefetch(Rectangle)}, or {@code null} if none.
+     * This is a linked list, but the list will rarely have more than 1 element.
+     */
+    private Rasters prefetched;
+
+    /**
+     * The set of tiles fetched by a single call to {@link #prefetch(Rectangle)}.
+     * This is a node in a linked list.
+     */
+    private final class Rasters implements Disposable {
+        /** Tile indices of the fetched region. */
+        final int x, y, width, height;
+
+        /** The fetched tiles. */
+        final Raster[] tiles;
+
+        /** Next set of tiles in the linked list. */
+        Rasters next;
+
+        /** Creates a new set of fetched tiles. */
+        Rasters(final Rectangle r, final Raster[] tiles) {
+            x      = r.x;
+            y      = r.y;
+            width  = r.width;
+            height = r.height;
+            this.tiles = tiles;
+        }
+
+        /** Discards this set of tiles. */
+        @Override public void dispose() {
+            remove(this);
+        }
+    }
+
+    /**
+     * Creates an initially empty image with the given sample model.
+     *
+     * @param  sampleModel  the sample model shared by all tiles in this image.
+     * @param  properties   image properties ({@link DeferredProperty} supported), or {@code null} if none.
+     * @param  sources      sources of this image (may be an empty array), or a null array if unknown.
+     */
+    protected BatchComputedImage(final SampleModel sampleModel, final Map<String,Object> properties, final RenderedImage... sources) {
+        super(sampleModel, sources);
+        this.properties = (properties != null) ? JDK9.copyOf(properties) : Collections.emptyMap();
+    }
+
+    /**
+     * Gets a property from this image.
+     *
+     * @param  key  the name of the property to get.
+     * @return the property value, or {@link Image#UndefinedProperty} if none.
+     */
+    @Override
+    public Object getProperty(final String key) {
+        Object value = properties.getOrDefault(key, Image.UndefinedProperty);
+        if (value instanceof DeferredProperty) {
+            value = ((DeferredProperty) value).compute(this);
+        }
+        return value;
+    }
+
+    /**
+     * Returns the names of all recognized properties,
+     * or {@code null} if this image has no properties.
+     *
+     * @return names of all recognized properties, or {@code null} if none.
+     */
+    @Override
+    public String[] getPropertyNames() {
+        final int n = properties.size();
+        return (n == 0) ? null : properties.keySet().toArray(new String[n]);
+    }
+
+    /**
+     * Computes immediately and returns all tiles in the given ranges of tile indices.
+     * It is implementer responsibility to ensure that all rasters have consistent
+     * {@link Raster#getMinX()}/{@code getMinY()} values.
+     *
+     * @param  tiles  range of tile indices for which to precompute tiles.
+     * @return precomputed tiles for the given indices, in row-major fashion.
+     * @throws Exception if an error occurred when computing tiles.
+     */
+    protected abstract Raster[] computeTiles(Rectangle tiles) throws Exception;
+
+    /**
+     * Invoked when a single tile need to be computed.
+     * The default implementation delegates to {@link #computeTiles(Rectangle)}.
+     *
+     * @param  tileX     the column index of the tile to compute.
+     * @param  tileY     the row index of the tile to compute.
+     * @param  previous  ignored (this method creates a new raster on each invocation).
+     * @return computed tile for the given indices.
+     * @throws Exception if an error occurred while computing the tile.
+     */
+    @Override
+    protected final Raster computeTile(final int tileX, final int tileY, WritableRaster previous) throws Exception {
+        synchronized (this) {
+            for (Rasters r = prefetched; r != null; r = r.next) {
+                final int x = tileX - r.x;
+                final int y = tileY - r.y;
+                if ((x | y) >= 0 && x < r.width && y < r.height) {
+                    return r.tiles[x + y * r.width];
+                }
+            }
+        }
+        final Raster[] tiles = computeTiles(new Rectangle(tileX, tileY, 1, 1));
+        if (tiles.length == 1) {
+            return tiles[0];
+        }
+        throw new ImagingOpException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
+    }
+
+    /**
+     * Notifies this image that tiles will be computed soon in the given region.
+     *
+     * @param  region  indices of the tiles which will be prefetched.
+     * @return handler on which to invoke {@code dispose()} after the prefetch operation.
+     */
+    @Override
+    protected Disposable prefetch(final Rectangle region) {
+        final Raster[] tiles;
+        try {
+            tiles = computeTiles(region);
+        } catch (Exception e) {
+            throw (ImagingOpException) new ImagingOpException(null).initCause(e);
+        }
+        final Rasters r = new Rasters(region, tiles);
+        synchronized (this) {
+            r.next = prefetched;
+            prefetched = r;
+        }
+        return r;
+    }
+
+    /**
+     * Discards the given set of tiles. This method is invoked when the fetched tiles are no longer needed.
+     *
+     * @param  tiles  fetched tiles to removed from the {@link #prefetched} linked list.
+     */
+    private synchronized void remove(final Rasters tiles) {
+        Rasters previous = null;
+        Rasters r = prefetched;
+        while (r != tiles) {
+            if (r == null) return;
+            previous = r;
+            r = r.next;
+        }
+        r = r.next;
+        if (previous != null) {
+            previous.next = r;
+        } else {
+            prefetched = r;
+        }
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
index 2025215..93e0238 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java
@@ -28,6 +28,7 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.storage.RasterLoadingStrategy;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -143,6 +144,7 @@ public class MultiResolutionCoverageLoader {
             }
         }
         coverages = new Reference[Math.max(resolutions.length, 1)];
+        resource.setLoadingStrategy(RasterLoadingStrategy.AT_GET_TILE_TIME);
     }
 
     /**
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
index 0bbccfb..5da38e2 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -288,7 +288,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
      * The {@link Raster#getMinX()} and {@code getMinY()} coordinates of returned rasters
      * will start at the given {@code offsetAOI} values.
      *
-     * <p>This method is thread-safe.</p>
+     * <p>This method is thread-safe. Synchronization is done on {@link DataCube#getSynchronizationLock()}.</p>
      *
      * @param  iterator  an iterator over the tiles that intersect the Area Of Interest specified by user.
      * @return tiles decoded from the TIFF file.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledDeferredImage.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledDeferredImage.java
new file mode 100644
index 0000000..262be29
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledDeferredImage.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.storage;
+
+import java.util.Map;
+import java.awt.Rectangle;
+import java.awt.image.ColorModel;
+import java.awt.image.Raster;
+import org.apache.sis.internal.coverage.j2d.BatchComputedImage;
+
+
+/**
+ * A rendered image where tiles are loaded only when first needed.
+ * Used for {@link org.apache.sis.storage.RasterLoadingStrategy#AT_GET_TILE_TIME}.
+ * Other loading strategies should not instantiate this class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class TiledDeferredImage extends BatchComputedImage {
+    /**
+     * Number of pixels along X or Y axis in the whole rendered image.
+     */
+    private final int width, height;
+
+    /**
+     * Index of the first tile in the image.
+     */
+    private final int minTileX, minTileY;
+
+    /**
+     * 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;
+
+    /**
+     * Creates a new tiled image.
+     *
+     * @param imageSize   full image size, after subsampling.
+     * @param tileLower   indices of first tile to read, inclusive.
+     * @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)
+    {
+        super(iterator.getCoverage().model, properties);
+        this.width    = imageSize[TiledGridCoverage.X_DIMENSION];
+        this.height   = imageSize[TiledGridCoverage.Y_DIMENSION];
+        this.minTileX = tileLower[TiledGridCoverage.X_DIMENSION];
+        this.minTileY = tileLower[TiledGridCoverage.Y_DIMENSION];
+        this.iterator = iterator;
+    }
+
+    /** Returns the color model, or {@code null} if none. */
+    @Override public ColorModel getColorModel() {
+        return iterator.getCoverage().colors;
+    }
+
+    /** Returns the minimum <var>x</var> coordinate (inclusive) of this image. */
+    @Override public final int getMinX() {
+        return iterator.getTileOrigin(TiledGridCoverage.X_DIMENSION);
+    }
+
+    /** Returns the minimum <var>y</var> coordinate (inclusive) of this image. */
+    @Override public final int getMinY() {
+        return iterator.getTileOrigin(TiledGridCoverage.Y_DIMENSION);
+    }
+
+    /** Returns the number of pixels along X axis in the whole rendered image. */
+    @Override public final int getWidth() {return width;}
+
+    /** Returns the number of pixels along Y axis in the whole rendered image. */
+    @Override public final int getHeight() {return height;}
+
+    /** Returns the minimum tile index in the X direction. */
+    @Override public final int getMinTileX() {return minTileX;}
+
+    /** Returns the minimum tile index in the Y direction. */
+    @Override public final int getMinTileY() {return minTileY;}
+
+    /**
+     * Loads immediately and returns all tiles in the given ranges of tile indices.
+     *
+     * @param  tiles  range of tile indices for which to load tiles.
+     * @return loaded tiles for the given indices, in row-major fashion.
+     */
+    @Override
+    protected Raster[] computeTiles(final Rectangle tiles) throws Exception {
+        final TiledGridCoverage.AOI aoi = iterator.subset(new int[] {tiles.x, tiles.y},
+                new int[] {Math.addExact(tiles.x, tiles.width), Math.addExact(tiles.y, tiles.height)});
+        return aoi.getCoverage().readTiles(aoi);
+    }
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
index b5ffb7b..78490e0 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
@@ -196,6 +196,11 @@ public abstract class TiledGridCoverage extends GridCoverage {
     protected final Number fillValue;
 
     /**
+     * Whether the reading of tiles is deferred to {@link RenderedImage#getTile(int, int)} time.
+     */
+    private final boolean deferredTileReading;
+
+    /**
      * Creates a new tiled grid coverage. All parameters should have been validated before this call.
      *
      * @param  subset  description of the {@link TiledGridResource} subset to cover.
@@ -205,6 +210,7 @@ public abstract class TiledGridCoverage extends GridCoverage {
         super(subset.domain, subset.ranges);
         final GridExtent extent = subset.domain.getExtent();
         final int dimension = subset.sourceExtent.getDimension();
+        deferredTileReading = subset.deferredTileReading();
         readExtent          = subset.readExtent;
         subsampling         = subset.subsampling;
         subsamplingOffsets  = subset.subsamplingOffsets;
@@ -389,7 +395,7 @@ public abstract class TiledGridCoverage extends GridCoverage {
             // TODO
             throw new UnsupportedOperationException("Non-horizontal slices not yet implemented.");
         }
-        final TiledImage image;
+        final RenderedImage image;
         try {
             final int[] tileLower = new int[dimension];         // Indices of first tile to read, inclusive.
             final int[] tileUpper = new int[dimension];         // Indices of last tile to read, exclusive.
@@ -420,17 +426,23 @@ public abstract class TiledGridCoverage extends GridCoverage {
                 tileUpper[i] = toIntExact(subtractExact(tileUp, tmcOfFirstTile[i]));
             }
             /*
-             * Get all tiles in the specified region. I/O operations, if needed, happen here.
-             */
-            final Raster[] result = readTiles(new AOI(tileLower, tileUpper, offsetAOI, dimension));
-            /*
-             * Wraps in an image all the tiles that we just read, together with the following properties:
+             * 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 Map<String,Object> properties = DeferredProperty.forGridGeometry(getGridGeometry(), selectedDimensions);
-            image = new TiledImage(properties, colors,
-                    imageSize[X_DIMENSION], imageSize[Y_DIMENSION],
-                    tileLower[X_DIMENSION], tileLower[Y_DIMENSION], result);
+            if (deferredTileReading) {
+                image = new TiledDeferredImage(imageSize, tileLower, properties, iterator);
+            } else {
+                /*
+                 * If the loading strategy is not `RasterLoadingStrategy.AT_GET_TILE_TIME`, get all tiles
+                 * in the area of interest now. I/O operations, if needed, happen in `readTiles(…)` call.
+                 */
+                final Raster[] result = readTiles(iterator);
+                image = new TiledImage(properties, colors,
+                        imageSize[X_DIMENSION], imageSize[Y_DIMENSION],
+                        tileLower[X_DIMENSION], tileLower[Y_DIMENSION], result);
+            }
         } catch (Exception e) {     // Too many exception types for listing them all.
             throw new CannotEvaluateException(Resources.forLocale(getLocale()).getString(
                     Resources.Keys.CanNotRenderImage_1, getDisplayName()), e);
@@ -511,7 +523,7 @@ public abstract class TiledGridCoverage extends GridCoverage {
              * converts the `tileLower` coordinates to index in the `tileOffsets` and `tileByteCounts` vectors.
              */
             indexInTileVector = indexOfFirstTile;
-            int tileCountInQuery   =  1;
+            int tileCountInQuery = 1;
             for (int i=0; i<dimension; i++) {
                 final int lower   = tileLower[i];
                 final int count   = subtractExact(tileUpper[i], lower);
@@ -532,6 +544,37 @@ public abstract class TiledGridCoverage extends GridCoverage {
         }
 
         /**
+         * Returns a new {@code AOI} 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;
+         * extra indices are ignored and missing indices are inherited from this AOI.
+         * This method is independent to the iterator position of this {@code AOI}.
+         *
+         * @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.
+         */
+        public AOI 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;) {
+                final int base = lower[i];
+                final int s = tileLower[i];
+                if (s > base) {
+                    lower[i] = s;
+                    // Use of `ceilDiv(…)` is for consistency with `getTileOrigin(int)`.
+                    offset[i] = addExact(offset[i], ceilDiv(multiplyExact(s - base, tileSize[i]), subsampling[i]));
+                }
+            }
+            final int[] upper = this.tileUpper.clone();
+            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);
+        }
+
+        /**
          * Returns the enclosing coverage.
          */
         final TiledGridCoverage getCoverage() {
@@ -787,7 +830,8 @@ public abstract class TiledGridCoverage extends GridCoverage {
      * The {@link Raster#getMinX()} and {@code getMinY()} coordinates of returned rasters
      * shall start at the given {@code iterator.offsetAOI} values.
      *
-     * <p>This method must be thread-safe.</p>
+     * <p>This method must be thread-safe. It is implementer responsibility to ensure synchronization,
+     * for example using {@link TiledGridResource#getSynchronizationLock()}.</p>
      *
      * @param  iterator  an iterator over the tiles that intersect the Area Of Interest specified by user.
      * @return tiles decoded from the {@link TiledGridResource}.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
index ff37952..e4fc973 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
@@ -435,6 +435,21 @@ public abstract class TiledGridResource extends AbstractGridResource {
         public boolean isXContiguous() {
             return includedBands == null && subsampling[X_DIMENSION] == 1;
         }
+
+        /**
+         * Whether the reading of tiles is deferred to {@link RenderedImage#getTile(int, int)} time.
+         */
+        final boolean deferredTileReading() {
+            if (loadingStrategy != RasterLoadingStrategy.AT_GET_TILE_TIME) {
+                return false;
+            }
+            for (int i = subsampling.length; --i >= 0;) {
+                if (subsampling[i] >= tileSize[i]) {
+                    return false;
+                }
+            }
+            return true;
+        }
     }
 
     /**
@@ -449,7 +464,7 @@ public abstract class TiledGridResource extends AbstractGridResource {
     protected final GridCoverage preload(final GridCoverage coverage) throws DataStoreException {
         assert Thread.holdsLock(getSynchronizationLock());
         // Note: `loadingStrategy` may still be null if unitialized.
-        if (loadingStrategy != RasterLoadingStrategy.AT_RENDER_TIME) {
+        if (loadingStrategy == null || loadingStrategy == RasterLoadingStrategy.AT_READ_TIME) {
             /*
              * In theory the following condition is redundant with `supportImmediateLoading()`.
              * We apply it anyway in case the coverage geometry is not what was announced.
@@ -484,6 +499,8 @@ public abstract class TiledGridResource extends AbstractGridResource {
 
     /**
      * Whether this resource supports immediate loading of raster data.
+     * Current implementation does not support immediate loading if the data cube has more than 2 dimensions.
+     * Non-immediate loading allows users to specify two-dimensional slices.
      */
     private boolean supportImmediateLoading() {
         return getTileSize().length == TiledGridCoverage.BIDIMENSIONAL;
@@ -516,7 +533,9 @@ public abstract class TiledGridResource extends AbstractGridResource {
     @Override
     public final boolean setLoadingStrategy(final RasterLoadingStrategy strategy) throws DataStoreException {
         synchronized (getSynchronizationLock()) {
-            if (strategy != null) {
+            if (strategy == RasterLoadingStrategy.AT_GET_TILE_TIME) {
+                loadingStrategy = strategy;
+            } else if (strategy != null) {
                 setLoadingStrategy(strategy == RasterLoadingStrategy.AT_READ_TIME && supportImmediateLoading());
             }
             return super.setLoadingStrategy(strategy);