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

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

commit 84de818e9feb0b64f50fc7c2b9ee2825611405ba
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Jun 14 15:26:15 2026 +0200

    Implement pyramid support in the GeoHEIF reader.
---
 .../org/apache/sis/coverage/grid/GridGeometry.java |  19 ++-
 .../apache/sis/storage/tiling/TileReadEvent.java   |   7 +-
 .../storage/tiling/TiledGridCoverageResource.java  |  30 ++--
 .../apache/sis/util/internal/shared/Numerics.java  |  14 ++
 .../apache/sis/storage/geoheif/GeoHeifStore.java   |   6 +-
 .../apache/sis/storage/geoheif/ImageResource.java  |  27 +++-
 .../org/apache/sis/storage/geoheif/Pyramid.java    | 177 +++++++++++++++++++--
 .../sis/storage/geoheif/ResourceBuilder.java       |   5 +-
 8 files changed, 251 insertions(+), 34 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index 8406c91627..93f547f599 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
@@ -88,6 +88,7 @@ import org.apache.sis.util.logging.Logging;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.xml.NilObject;
 import org.apache.sis.xml.NilReason;
+import static org.apache.sis.referencing.CRS.getDimensionOrZero;
 import static org.apache.sis.referencing.CRS.findOperation;
 import static org.apache.sis.referencing.CRS.SeparationMode;
 
@@ -1934,19 +1935,23 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * to the given <abbr>CRS</abbr>. The {@linkplain #getGeographicExtent() 
geographic bounding box} of this grid
      * geometry is used as the desired domain of validity.
      *
+     * <p>If this grid geometry has no <abbr>CRS</abbr> but the number of 
dimensions of the "real world" space is
+     * equal to the number of dimensions of the {@code target}, then this 
method returns an identity operation.</p>
+     *
      * @param  target  the target <abbr>CRS</abbr> of the desired operation.
      * @return coordinate operation from the <abbr>CRS</abbr> of this grid 
geometry to the given <abbr>CRS</abbr>.
-     * @throws IncompleteGridGeometryException if this grid geometry has no 
<abbr>CRS</abbr>.
-     * @throws TransformException if the coordinate operation cannot be found.
+     * @throws IncompleteGridGeometryException if this grid geometry does not 
have sufficient information.
+     * @throws FactoryException if the coordinate operation cannot be found.
      *
      * @since 1.7
      */
-    public CoordinateOperation createChangeOfCRS(final 
CoordinateReferenceSystem target) throws TransformException {
-        try {
-            return findOperation(getCoordinateReferenceSystem(), target, 
geographicBBox());
-        } catch (FactoryException e) {
-            throw new TransformException(e);
+    public CoordinateOperation createChangeOfCRS(final 
CoordinateReferenceSystem target) throws FactoryException {
+        ArgumentChecks.ensureNonNull("target", target);
+        CoordinateReferenceSystem crs = getCoordinateReferenceSystem(envelope);
+        if (crs == null) {
+            crs = (getDimensionOrZero(target) == getTargetDimension()) ? 
target : getCoordinateReferenceSystem();
         }
+        return findOperation(crs, target, geographicBBox());
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
index 8c8716b0f1..bc28951b95 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
@@ -20,6 +20,7 @@ import java.io.Serializable;
 import java.awt.Shape;
 import java.awt.Rectangle;
 import java.awt.geom.Rectangle2D;
+import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform2D;
 import org.opengis.referencing.operation.TransformException;
@@ -46,7 +47,7 @@ import org.apache.sis.util.internal.shared.Strings;
  * @since   1.7
  * @version 1.7
  */
-public class TileReadEvent extends StoreEvent {
+public final class TileReadEvent extends StoreEvent {
     /**
      * For cross-version compatibility.
      */
@@ -125,13 +126,15 @@ public class TileReadEvent extends StoreEvent {
         final synchronized MathTransform2D imageToObjective(final 
CoordinateReferenceSystem crs) throws TransformException {
             @SuppressWarnings("LocalVariableHidesMemberVariable")
             CoordinateOperation crsToObjective = this.crsToObjective;
-            if (crsToObjective == null || 
!CRS.equivalent(crsToObjective.getTargetCRS(), crs)) {
+            if (crsToObjective == null || 
!CRS.equivalent(crsToObjective.getTargetCRS(), crs)) try {
                 crsToObjective = sliceGeometry.createChangeOfCRS(crs);
                 MathTransform tr = MathTransforms.translation(offsetX, 
offsetY);
                 tr = MathTransforms.concatenate(tr, 
sliceGeometry.getGridToCRS(PixelInCell.CELL_CORNER));
                 tr = MathTransforms.concatenate(tr, 
crsToObjective.getMathTransform());
                 imageToObjective = MathTransforms.bidimensional(tr);
                 this.crsToObjective = crsToObjective;   // Store only after 
the rest was successful.
+            } catch (FactoryException e) {
+                throw new TransformException(e);
             }
             return imageToObjective;
         }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
index 886d952c6f..5234c3ec1b 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
@@ -534,7 +534,7 @@ check:  if (dataType.isInteger()) {
      * Instances of this class are temporary and used only for transferring 
information from {@link TiledGridCoverageResource}
      * to {@link TiledGridCoverage}. This class does not perform I/O 
operations.
      */
-    public final class Subset {
+    public class Subset {
         /**
          * The full size of the coverage in the enclosing {@link 
TiledGridCoverageResource}.
          * This is taken from {@link #getGridGeometry()} and does not take 
sub-sampling in account.
@@ -551,10 +551,15 @@ check:  if (dataType.isInteger()) {
         final GridExtent readExtent;
 
         /**
-         * The sub-region extent, CRS and conversion from cell indices to CRS.
-         * This is the domain of the grid coverage to create.
+         * The sub-region extent, <abbr>CRS</abbr> and conversion from cell 
indices to <abbr>CRS</abbr>.
+         * This is the domain of the grid coverage to create. The 
<abbr>CRS</abbr> of this domain is the
+         * <abbr>CRS</abbr> of the {@linkplain #getGridGeometry() base grid 
geometry}, or a subset of it.
+         *
+         * <p>This value is derived from the {@code domain} argument given to 
the constructor,
+         * but is not necessarily identical since it has been converted to the 
<abbr>CRS</abbr>
+         * of the base grid geometry.</p>
          */
-        final GridGeometry domain;
+        public final GridGeometry domain;
 
         /**
          * Sample dimensions for each image band. This is the range of the 
grid coverage to create.
@@ -637,6 +642,9 @@ check:  if (dataType.isInteger()) {
 
         /**
          * Creates parameters for the given domain and range.
+         * The arguments given to this constructor are the arguments
+         * given to the {@link #read(GridGeometry, int...)} method.
+         * This constructor should not need to be invoked directly, except by 
subclasses.
          *
          * @param  domain  the domain argument specified by user in a call to 
{@code GridCoverageResource.read(…)}.
          * @param  range   the range argument specified by user in a call to 
{@code GridCoverageResource.read(…)}.
@@ -647,7 +655,7 @@ check:  if (dataType.isInteger()) {
          * @throws IllegalArgumentException if an error occurred in an 
operation
          *         such as creating the {@code SampleModel} subset for 
selected bands.
          */
-        public Subset(GridGeometry domain, final int[] range) throws 
DataStoreException {
+        protected Subset(GridGeometry domain, final int[] range) throws 
DataStoreException {
             // Validate argument first, before more expensive computations.
             List<SampleDimension> bands = getSampleDimensions();
             final RangeArgument rangeIndices = 
RangeArgument.validate(bands.size(), range, listeners);
@@ -814,23 +822,27 @@ check:  if (dataType.isInteger()) {
          *
          * @return whether the values to read on a row are contiguous.
          */
-        public boolean isXContiguous() {
+        public final boolean isXContiguous() {
             return includedBands == null && subsampling[xDimension()] == 1;
         }
 
         /**
          * Returns dimension of the grid which is mapped to the <var>x</var> 
axis (column indexes) in rendered images.
          * This is usually 0.
+         *
+         * @return grid dimension mapped to image columns (usually 0).
          */
-        final int xDimension() {
+        public final int xDimension() {
             return xDimension;
         }
 
         /**
          * Returns dimension of the grid which is mapped to the <var>y</var> 
axis (row indexes) in rendered images.
          * This is usually 1.
+         *
+         * @return grid dimension mapped to image rows (usually 1).
          */
-        final int yDimension() {
+        public final int yDimension() {
             return yDimension;
         }
 
@@ -1186,7 +1198,7 @@ check:  if (dataType.isInteger()) {
      * @version 1.7
      * @since   1.7
      */
-    protected static interface Pyramid {
+    public static interface Pyramid {
         /**
          * Returns an identifier for this pyramid. The default implementation 
returns <abbr>TMS</abbr>
          * as the abbreviation of "Tile Matrix Set". This is often sufficient 
in the common case where
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
index 3a03b0fe2f..2c6a4cab9a 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
@@ -315,6 +315,20 @@ public final class Numerics {
         return (int) value;
     }
 
+    /**
+     * Returns the result of {@code numerator / denominator} but potentially 
more accurate.
+     * If any argument cannot be converted to {@code double} without accuracy 
lost but the
+     * integer part of the result of the division can be represented 
accurately, then this
+     * method provides a better result than {@code numerator / (double) 
denominator}.
+     *
+     * @param  numerator    numerator of the division.
+     * @param  denominator  denominator of the division.
+     * @return value of {@code numerator / denominator} but potentially more 
accurate.
+     */
+    public static double divide(long numerator, long denominator) {
+        return (numerator / denominator) + (numerator % denominator) / 
(double) denominator;
+    }
+
     /**
      * Returns the given fraction as a {@link Fraction} instance if possible,
      * or as a {@link DoubleDouble} approximation otherwise.
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java
index 91a945075b..514bc3b37f 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java
@@ -31,6 +31,7 @@ import org.opengis.util.GenericName;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.ChannelImageInputStream;
 import org.apache.sis.io.stream.IOUtilities;
@@ -39,6 +40,7 @@ import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreClosedException;
+import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.metadata.MetadataBuilder;
@@ -91,7 +93,7 @@ public class GeoHeifStore extends DataStore implements 
Aggregate {
      *
      * @see #createComponentName(String)
      */
-    private final NameFactory nameFactory;
+    final NameFactory nameFactory;
 
     /**
      * The stream from which to read the data, or {@code null} if this store 
has been closed.
@@ -306,6 +308,8 @@ public class GeoHeifStore extends DataStore implements 
Aggregate {
             content = builder.build();
         } catch (IOException e) {
             throw new DataStoreException(e);
+        } catch (TransformException e) {
+            throw new DataStoreReferencingException(e);
         }
         return Containers.viewAsUnmodifiableList(content);
     }
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
index 274aa98072..b0e49061fe 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
@@ -35,7 +35,10 @@ import javax.imageio.ImageReader;
 import javax.imageio.spi.ImageReaderSpi;
 import org.opengis.metadata.Metadata;
 import org.opengis.util.GenericName;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.storage.DataStore;
@@ -45,6 +48,7 @@ import org.apache.sis.storage.tiling.TiledGridCoverage;
 import org.apache.sis.storage.tiling.TiledGridCoverageResource;
 import org.apache.sis.storage.isobmff.ByteRanges;
 import org.apache.sis.io.stream.ChannelDataInput;
+import org.apache.sis.util.internal.shared.Numerics;
 
 
 /**
@@ -59,7 +63,7 @@ final class ImageResource extends TiledGridCoverageResource 
implements StoreReso
      *
      * @see #getOriginator()
      */
-    private final GeoHeifStore store;
+    final GeoHeifStore store;
 
     /**
      * Identifier of this resource.
@@ -83,7 +87,7 @@ final class ImageResource extends TiledGridCoverageResource 
implements StoreReso
      *
      * @see #getGridGeometry()
      */
-    private final GridGeometry gridGeometry;
+    private GridGeometry gridGeometry;
 
     /**
      * Description of the bands.
@@ -174,6 +178,25 @@ final class ImageResource extends 
TiledGridCoverageResource implements StoreReso
         return metadata;
     }
 
+    /**
+     * Declares that this image is the pyramid level of the given base grid.
+     * This method does nothing if this image already has its own "grid to 
<abbr>CRS</abbr>" transform.
+     *
+     * @param  base  grid geometry of the pyramid level at the finest 
resolution.
+     * @throws TransformException if an error occurred while deriving the 
"grid to <abbr>CRS</abbr>" transform.
+     */
+    final void setPyramidLevelOf(final GridGeometry base) throws 
TransformException {
+        if (!gridGeometry.isDefined(GridGeometry.GRID_TO_CRS)) {
+            final GridExtent extent = gridGeometry.getExtent();
+            final GridExtent baseExtent = base.getExtent();
+            final var factors = new double[baseExtent.getDimension()];
+            for (int i = 0; i < factors.length; i++) {
+                factors[i] = Numerics.divide(baseExtent.getSize(i), 
extent.getSize(i));
+            }
+            gridGeometry = new GridGeometry(base, extent, 
MathTransforms.scale(factors));
+        }
+    }
+
     /**
      * Returns the valid extent of grid coordinates together with the 
conversion from those grid
      * coordinates to real world coordinates.
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Pyramid.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Pyramid.java
index 58c4261ca1..b93aafa9e7 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Pyramid.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Pyramid.java
@@ -16,10 +16,18 @@
  */
 package org.apache.sis.storage.geoheif;
 
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalInt;
 import org.opengis.util.GenericName;
-import org.apache.sis.storage.AbstractResource;
-import org.apache.sis.storage.GridCoverageResource;
+import org.opengis.util.NameFactory;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.isobmff.image.ImagePyramid;
+import org.apache.sis.storage.tiling.TiledGridCoverage;
+import org.apache.sis.storage.tiling.TiledGridCoverageResource;
 
 
 /**
@@ -27,26 +35,171 @@ import org.apache.sis.storage.isobmff.image.ImagePyramid;
  *
  * @author Johann Sorel (Geomatys)
  * @author Martin Desruisseaux (Geomatys)
- *
- * @todo Not yet implemented. Not to integrate with {@code TiledGridCoverage}.
- *       It will require completion of the work for making {@code 
TiledGridCoverage}.
- *       an implementation of {@code TileMatrixSet}.
  */
-final class Pyramid extends AbstractResource {
+final class Pyramid extends TiledGridCoverageResource implements 
TiledGridCoverageResource.Pyramid {
     /**
-     * Name of this pyramid.
+     * Name of this pyramid, or {@code null} if none.
      */
     private final GenericName name;
 
+    /**
+     * Tile width in pixels.
+     */
+    private final int tileSizeX;
+
+    /**
+     * Tile height in pixels.
+     */
+    private final int tileSizeY;
+
+    /**
+     * The layers in the order they were declared in the {@code pymd} box.
+     * This order should be from finest resolution (at index 0) to coarsest 
resolution.
+     */
+    private final ImageResource[] levels;
+
     /**
      * Creates a new pyramid.
      *
-     * @param store       the parent of this pyramid.
-     * @param name        the name of this pyramid.
-     * @param components  the child resources.
+     * @param  store    the parent of this pyramid.
+     * @param  name     the name of this pyramid, or {@code null} if none.
+     * @param  pyramid  information about the pyramid.
+     * @param  levels   the child resources from finest resolution to coarsest 
resolution.
+     * @throws TransformException if an error occurred while deriving a "grid 
to <abbr>CRS</abbr>" transform.
      */
-    Pyramid(final GeoHeifStore store, final GenericName name, final 
ImagePyramid pyramid, final GridCoverageResource[] components) {
+    Pyramid(final GeoHeifStore store, final GenericName name, final 
ImagePyramid pyramid, final ImageResource[] levels)
+            throws TransformException
+    {
         super(store);
         this.name = name;
+        tileSizeX = pyramid.tileSizeX;
+        tileSizeY = pyramid.tileSizeY;
+        this.levels = levels;
+        final GridGeometry base = levels[0].getGridGeometry();
+        if (base.isDefined(GridGeometry.GRID_TO_CRS)) {
+            for (int i = 1; i < levels.length; i++) {
+                levels[i].setPyramidLevelOf(base);
+            }
+        }
+    }
+
+    /**
+     * Returns the name factory to use for creating identifiers of tiles and 
tile matrices.
+     */
+    @Override
+    public NameFactory nameFactory() {
+        return levels[0].store.nameFactory;
+    }
+
+    /**
+     * Returns the name of this pyramid.
+     */
+    @Override
+    public Optional<GenericName> getIdentifier() {
+        return Optional.ofNullable(name);
+    }
+
+    /**
+     * Returns the size of tiles in this resource.
+     * The length of the returned array is the number of dimensions,
+     */
+    @Override
+    protected int[] getTileSize() {
+        return new int[] {tileSizeX, tileSizeY};
+    }
+
+    /**
+     * Returns the grid geometry of the level with the finest resolution.
+     *
+     * @return grid geometry at finest resolution.
+     * @throws DataStoreException if an error occurred while fetching the grid 
geometry.
+     */
+    @Override
+    public GridGeometry getGridGeometry() throws DataStoreException {
+        return representative().getGridGeometry();
+    }
+
+    /**
+     * Returns the sample dimensions of this grid coverage.
+     * All levels should have the same sample dimensions.
+     * This method uses the finest level as representative.
+     *
+     * @return sample dimensions of this grid coverage.
+     * @throws DataStoreException if an error occurred while fetching the 
sample dimensions.
+     */
+    @Override
+    public List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
+        return representative().getSampleDimensions();
+    }
+
+    /**
+     * Returns a resource which is representative of all pyramid levels except 
for the resolution.
+     * This method is invoked for fetching metadata such as the Coordinate 
Reference System
+     * when the resolution does not matter. For a <abbr>HEIF</abbr> file, this 
is the image
+     * with the finest resolution.
+     *
+     * @return a resource representative of all levels (ignoring resolution).
+     */
+    @Override
+    public TiledGridCoverageResource representative() {
+        return levels[0];
+    }
+
+    /**
+     * Returns information about the overviews which form the pyramid.
+     */
+    @Override
+    protected List<Pyramid> getPyramids() {
+        return List.of(this);
+    }
+
+    /**
+     * Returns the number of pyramid levels.
+     */
+    @Override
+    public OptionalInt numberOfLevels() {
+        return OptionalInt.of(levels.length);
+    }
+
+    /**
+     * Returns the image at the given pyramid level.
+     * Indices are in the reverse order of the images in the <abbr>HEIF</abbr> 
file,
+     * with 0 for the image at the coarsest resolution (the overview).
+     *
+     * @param  level  image index (level) in the pyramid, with 0 for coarsest 
resolution (the overview).
+     * @return image at the given pyramid level, or {@code null} if the given 
level is out of bounds.
+     */
+    @Override
+    public TiledGridCoverageResource forPyramidLevel(int level) {
+        if (level >= 0) {
+            level = (levels.length - 1) - level;    // Reverse order.
+            if (level >= 0) {
+                return levels[level];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Delegates to the pyramid level for the resolution of the given subset.
+     * This method should never be invoked because {@link #read(GridGeometry, 
int...)}
+     * should select itself the pyramid level on which to delegate the read 
operation.
+     * We nevertheless implement this method for safety.
+     *
+     * @param  subset  desired grid extent, resolution and sample dimensions 
to read.
+     * @return the grid coverage for the specified domain, resolution and 
ranges.
+     * @throws DataStoreException if the coverage cannot be created.
+     */
+    @Override
+    protected TiledGridCoverage read(final Subset subset) throws 
DataStoreException {
+        final double[] request = subset.domain.getResolution(false);
+        final int x = subset.xDimension();
+        final int y = subset.yDimension();
+        int level = levels.length;
+        while (--level >= 1) {
+            final double[] actual = 
levels[level].getGridGeometry().getResolution(false);
+            if (request[x] >= actual[x] && request[y] >= actual[y]) break;
+        }
+        return levels[level].read(subset);
     }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java
index f3dfe6d0ec..a86c6508c3 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java
@@ -32,6 +32,7 @@ import java.util.logging.LogRecord;
 import java.io.IOException;
 import javax.imageio.spi.ImageReaderSpi;
 import org.opengis.util.GenericName;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.DataStoreException;
@@ -462,9 +463,11 @@ final class ResourceBuilder {
      * The actual reading does not happen here.
      *
      * @return the resource.
+     * @throws IOException if an error occurred while reading bytes from the 
input stream.
      * @throws DataStoreException if another error occurred while building the 
image or resource.
+     * @throws TransformException if an error occurred while deriving a "grid 
to <abbr>CRS</abbr>" transform.
      */
-    final Resource[] build() throws DataStoreException, IOException {
+    final Resource[] build() throws DataStoreException, IOException, 
TransformException {
         for (final PrimaryItem primary : primaryItem) {
             List<ItemInfoEntry> info = info(itemInfos.remove(primary.itemID));
             if (info.isEmpty()) {

Reply via email to