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()) {
