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 <martin.desruisse...@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);