| /* |
| * 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.storage.base; |
| |
| import java.util.Map; |
| import java.util.Locale; |
| import java.io.IOException; |
| import java.awt.Point; |
| import java.awt.image.DataBuffer; |
| import java.awt.image.ColorModel; |
| import java.awt.image.SampleModel; |
| import java.awt.image.MultiPixelPackedSampleModel; |
| import java.awt.image.RenderedImage; |
| import java.awt.image.Raster; |
| import static java.lang.Math.addExact; |
| import static java.lang.Math.subtractExact; |
| import static java.lang.Math.multiplyExact; |
| import static java.lang.Math.multiplyFull; |
| import static java.lang.Math.incrementExact; |
| import static java.lang.Math.decrementExact; |
| import static java.lang.Math.toIntExact; |
| import static java.lang.Math.floorDiv; |
| import org.opengis.util.GenericName; |
| import org.apache.sis.coverage.grid.GridCoverage; |
| import org.apache.sis.coverage.grid.GridExtent; |
| import org.apache.sis.coverage.grid.DisjointExtentException; |
| import org.apache.sis.coverage.privy.DeferredProperty; |
| import org.apache.sis.coverage.privy.TiledImage; |
| import org.apache.sis.storage.DataStoreException; |
| import org.apache.sis.storage.tiling.TileMatrixSet; |
| import org.apache.sis.storage.internal.Resources; |
| import org.apache.sis.util.collection.WeakValueHashMap; |
| import org.apache.sis.util.resources.Errors; |
| import static org.apache.sis.pending.jdk.JDK18.ceilDiv; |
| |
| // Specific to the main and geoapi-3.1 branches: |
| import org.opengis.geometry.MismatchedDimensionException; |
| |
| // Specific to the geoapi-3.1 and geoapi-4.0 branches: |
| import org.opengis.coverage.CannotEvaluateException; |
| |
| |
| /** |
| * Base class of grid coverage read from a resource where data are stored in tiles. |
| * This grid coverage may represent only a subset of the coverage resource. |
| * Tiles are read from the storage only when first needed. |
| * |
| * <h2>Cell Coordinates</h2> |
| * When there is no subsampling, this coverage uses the same cell coordinates as the originating resource. |
| * When there is a subsampling, cell coordinates in this coverage are divided by the subsampling factors. |
| * Conversions are done by {@link #toFullResolution(long, int)}. |
| * |
| * <h2>Tile coordinate matrix</h2> |
| * In each {@code TiledGridCoverage}, indices of tiles starts at (0, 0, …). |
| * This class does not use the same tile indices as the coverage resource |
| * in order to avoid integer overflow. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| public abstract class TiledGridCoverage extends GridCoverage { |
| /** |
| * Number of dimensions in a rendered image. |
| * Used for identifying codes where a two-dimensional slice is assumed. |
| */ |
| protected static final int BIDIMENSIONAL = 2; |
| |
| /** |
| * The dimensions of <var>x</var> and <var>y</var> axes. |
| * Static constants for now, may become configurable fields in the future. |
| */ |
| protected static final int X_DIMENSION = 0, Y_DIMENSION = 1; |
| |
| /** |
| * The area to read in unit of the full coverage (without subsampling). |
| * This is the intersection between user-specified domain and the source |
| * {@link TiledGridResource} domain, expanded to an integer number of tiles. |
| */ |
| private final GridExtent readExtent; |
| |
| /** |
| * Whether to force the {@link #readExtent} tile intersection to the {@link #tileSize}. |
| * This is relevant only for the last column of tile matrix, because those tiles may be truncated |
| * if the image size is not a multiple of tile size. It is usually necessary to read those tiles |
| * fully anyway because otherwise, the pixels read from the storage would not be aligned with the |
| * pixels stored in the {@link Raster}. However, there is a few exceptions where the read extent |
| * should not be forced to the tile size: |
| * |
| * <ul> |
| * <li>If the image is untiled, then the {@link org.apache.sis.storage.base.TiledGridResource.Subset} |
| * constructor assumes that only the requested region of the tile will be read.</li> |
| * <li>If the tile is truncated on the storage as well |
| * (note: this is rare. GeoTIFF for example always stores whole tiles).</li> |
| * </ul> |
| * |
| * <p>In current version this is a flag for the <var>x</var> dimension only. In a future version |
| * it could be flags for other dimensions as well (using bitmask) if it appears to be useful.</p> |
| */ |
| private final boolean forceTileSize; |
| |
| /** |
| * Size of all tiles in the domain of this {@code TiledGridCoverage}, without clipping and subsampling. |
| * All coverages created from the same {@link TiledGridResource} have the same tile size values. |
| * The length of this array is the number of dimensions in the source {@link GridExtent}. |
| * This is often {@value #BIDIMENSIONAL} but can also be more. |
| */ |
| private final int[] tileSize; |
| |
| /** |
| * Values by which to multiply each tile coordinates for obtaining the index in the tile vector. |
| * The length of this array is the same as {@link #tileSize}. All coverages created from the same |
| * {@link TiledGridResource} have the same stride values. |
| */ |
| private final int[] tileStrides; |
| |
| /** |
| * Index of the first {@code TiledGridCoverage} tile in a row-major array of tiles. |
| * This is the value to add to the index computed with {@link #tileStrides} before to access vector elements. |
| */ |
| private final int indexOfFirstTile; |
| |
| /** |
| * The Tile Matrix Coordinates (TMC) of the first tile. |
| * This is the value to subtract from tile indices computed from pixel coordinates. |
| * |
| * @see #indexOfFirstTile |
| */ |
| private final long[] tmcOfFirstTile; |
| |
| /** |
| * Conversion from pixel coordinates in this (potentially subsampled) coverage |
| * to pixel coordinates in the resource coverage at full resolution. |
| * The conversion from (<var>x</var>, <var>y</var>) to (<var>x′</var>, <var>y′</var>) is as below, |
| * where <var>s</var> are subsampling factors and <var>t</var> are subsampling offsets: |
| * |
| * <ul> |
| * <li><var>x′</var> = s₀⋅<var>x</var> + t₀</li> |
| * <li><var>y′</var> = s₁⋅<var>y</var> + t₁</li> |
| * </ul> |
| * |
| * This transform maps {@linkplain org.apache.sis.coverage.grid.PixelInCell#CELL_CORNER pixel corners}. |
| * |
| * @see #getSubsampling(int) |
| * @see #toFullResolution(long, int) |
| */ |
| private final int[] subsampling, subsamplingOffsets; |
| |
| /** |
| * Indices of {@link TiledGridResource} bands which have been retained for |
| * inclusion in this {@code TiledGridCoverage}, in strictly increasing order. |
| * An "included" band is stored in memory but not necessarily visible to the user, |
| * because the {@link SampleModel} can be configured for ignoring some bands. |
| * This array is {@code null} if all bands shall be included. |
| * |
| * <p>If the user specified bands out of order, the change of band order is taken in account |
| * by the sample {@link #model}. This {@code includedBands} array does not apply any change |
| * of order for making sequential readings easier.</p> |
| */ |
| protected final int[] includedBands; |
| |
| /** |
| * Cache of rasters read by this {@code TiledGridCoverage}. This cache may be shared with other coverages |
| * created for the same {@link TiledGridResource} resource. For each value, the raster {@code minX} and |
| * {@code minY} values can be anything, depending which {@code TiledGridCoverage} was first to load the tile. |
| * |
| * @see TiledGridResource#rasters |
| * @see AOI#getCachedTile() |
| * @see #createCacheKey(int) |
| */ |
| private final WeakValueHashMap<TiledGridResource.CacheKey, Raster> rasters; |
| |
| /** |
| * The sample model for all rasters. The width and height of this sample model are the two first elements |
| * of {@link #tileSize} divided by subsampling and clipped to the domain. If user requested to read only |
| * a subset of the bands, then this sample model is already the subset. |
| */ |
| protected final SampleModel model; |
| |
| /** |
| * The Java2D color model for images rendered from this coverage. |
| */ |
| protected final ColorModel colors; |
| |
| /** |
| * The value to use for filling empty spaces in rasters, or {@code null} if zero. |
| */ |
| 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. |
| * @throws ArithmeticException if the number of tiles overflows 32 bits integer arithmetic. |
| */ |
| protected TiledGridCoverage(final TiledGridResource.Subset subset) { |
| 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; |
| includedBands = subset.includedBands; |
| rasters = subset.cache; |
| tileSize = subset.tileSize; |
| tmcOfFirstTile = new long[dimension]; |
| tileStrides = new int [dimension]; |
| final int[] subSize = new int [dimension]; |
| int tileStride = 1; |
| long indexOfFirstTile = 0; |
| for (int i=0; i<dimension; i++) { |
| final int ts = tileSize[i]; |
| tmcOfFirstTile[i] = floorDiv(readExtent.getLow(i), ts); |
| tileStrides[i] = tileStride; |
| subSize[i] = (int) Math.min(((ts-1) / subsampling[i]) + 1, extent.getSize(i)); |
| indexOfFirstTile = addExact(indexOfFirstTile, multiplyExact(tmcOfFirstTile[i], tileStride)); |
| int tileCount = toIntExact(ceilDiv(subset.sourceExtent.getSize(i), ts)); |
| tileStride = multiplyExact(tileCount, tileStride); |
| } |
| this.indexOfFirstTile = toIntExact(indexOfFirstTile); |
| /* |
| * At this point, `tileStride` is the total number of tiles in source. |
| * This value is not stored but its computation is still useful because |
| * we want `ArithmeticException` to be thrown if the value is too high. |
| */ |
| SampleModel model = subset.modelForBandSubset; |
| if (model.getWidth() != subSize[X_DIMENSION] || model.getHeight() != subSize[Y_DIMENSION]) { |
| model = model.createCompatibleSampleModel(subSize[X_DIMENSION], subSize[Y_DIMENSION]); |
| } |
| this.model = model; |
| this.colors = subset.colorsForBandSubset; |
| this.fillValue = subset.fillValue; |
| forceTileSize = subSize[X_DIMENSION] * subsampling[X_DIMENSION] == tileSize[X_DIMENSION]; |
| } |
| |
| /** |
| * Returns a unique name that identifies this coverage. |
| * The name shall be unique in the {@link TileMatrixSet}. |
| * |
| * @return an human-readable identification of this coverage. |
| */ |
| protected abstract GenericName getIdentifier(); |
| |
| /** |
| * Returns the locale for error messages, or {@code null} for the default. |
| * |
| * @return the locale for warning or error messages, or {@code null} if unspecified. |
| */ |
| protected Locale getLocale() { |
| return null; |
| } |
| |
| /** |
| * Returns the size of all tiles in the domain of this {@code TiledGridCoverage}, without clipping and subsampling. |
| * |
| * @param dimension dimension for which to get tile size. |
| * @return tile size in the given dimension, without clipping and subsampling. |
| */ |
| protected final int getTileSize(final int dimension) { |
| return tileSize[dimension]; |
| } |
| |
| /** |
| * Returns the subsampling in the given dimension. |
| * |
| * @param dimension dimension for which to get subsampling. |
| * @return subsampling as a value ≥ 1. |
| */ |
| protected final int getSubsampling(final int dimension) { |
| return subsampling[dimension]; |
| } |
| |
| /** |
| * Converts a cell coordinate from this {@code TiledGridCoverage} coordinate space to full resolution. |
| * This method removes the subsampling effect. Note that since this {@code TiledGridCoverage} uses the |
| * same coordinate space as {@link TiledGridResource}, the converted coordinates should be valid in |
| * the full resource as well. |
| * |
| * @param coordinate coordinate in this {@code TiledGridCoverage} domain. |
| * @param dimension dimension of the coordinate. |
| * @return coordinate in this {@code TiledGridCoverage} as if no subsampling was applied. |
| * @throws ArithmeticException if the coordinate cannot be represented as a long integer. |
| */ |
| private long toFullResolution(final long coordinate, final int dimension) { |
| return addExact(multiplyExact(coordinate, subsampling[dimension]), subsamplingOffsets[dimension]); |
| } |
| |
| /** |
| * Converts a cell coordinate from {@link TiledGridResource} space to {@code TiledGridCoverage} coordinate. |
| * This is the converse of {@link #toFullResolution(long, int)}. Note that there is a possible accuracy lost. |
| * |
| * @param coordinate coordinate in the {@code TiledGridResource} domain. |
| * @param dimension dimension of the coordinate. |
| * @return coordinates in this subsampled {@code TiledGridCoverage} domain. |
| * @throws ArithmeticException if the coordinate cannot be represented as a long integer. |
| */ |
| private long toSubsampledPixel(final long coordinate, final int dimension) { |
| return floorDiv(subtractExact(coordinate, subsamplingOffsets[dimension]), subsampling[dimension]); |
| } |
| |
| /** |
| * Converts a cell coordinate from this {@code TiledGridCoverage} coordinate space |
| * to the Tile Matrix Coordinate (TMC) of the tile which contains that cell. |
| * The TMC is relative to the full {@link TiledGridResource}, |
| * i.e. without subtraction of {@link #tmcOfFirstTile}. |
| * |
| * @param coordinate coordinates in this {@code TiledGridCoverage} domain. |
| * @param dimension dimension of the coordinate. |
| * @return Tile Matrix Coordinate (TMC) of the tile which contains the specified cell. |
| * @throws ArithmeticException if the coordinate cannot be represented as an integer. |
| */ |
| private long toTileMatrixCoordinate(final long coordinate, final int dimension) { |
| return floorDiv(toFullResolution(coordinate, dimension), tileSize[dimension]); |
| } |
| |
| /** |
| * Returns the number of pixels in a single bank element. This is usually 1, except for |
| * {@link MultiPixelPackedSampleModel} which packs many pixels in a single bank element. |
| * This value is a power of 2 according {@code MultiPixelPackedSampleModel} specification. |
| * |
| * <h4>Design note</h4> |
| * This is "pixels per element", not "samples per element". It makes a difference in the |
| * {@link java.awt.image.SinglePixelPackedSampleModel} case, for which this method returns 1 |
| * (by contrast a "samples per element" would give a value greater than 1). |
| * But this value can nevertheless be understood as a "samples per element" value |
| * where only one band is considered at a time. |
| * |
| * @return number of pixels in a single bank element. Usually 1. |
| * |
| * @see SampleModel#getSampleSize(int) |
| * @see MultiPixelPackedSampleModel#getPixelBitStride() |
| */ |
| protected final int getPixelsPerElement() { |
| return getPixelsPerElement(model); |
| } |
| |
| /** |
| * Implementation of {@link #getPixelsPerElement()}. |
| * |
| * @param model the sample model from which to infer the number of pixels per bank element. |
| * @return number of pixels in a single bank element. Usually 1. |
| */ |
| static int getPixelsPerElement(final SampleModel model) { |
| if (model instanceof MultiPixelPackedSampleModel) { |
| /* |
| * The following code performs the same computation as `MultiPixelPackedSampleModel` |
| * constructor when computing its package-private field `pixelsPerDataElement`. |
| * That constructor ensured that `sampleSize` is a divisor of `typeSize`. |
| */ |
| final int typeSize = DataBuffer.getDataTypeSize(model.getDataType()); |
| final int sampleSize = ((MultiPixelPackedSampleModel) model).getPixelBitStride(); |
| final int pixelsPerElement = typeSize / sampleSize; |
| if (pixelsPerElement > 0) { // Paranoiac check. |
| return pixelsPerElement; |
| } |
| } |
| return 1; |
| } |
| |
| /** |
| * Returns a two-dimensional slice of grid data as a rendered image. |
| * |
| * @param sliceExtent a subspace of this grid coverage, or {@code null} for the whole image. |
| * @return the grid slice as a rendered image. Image location is relative to {@code sliceExtent}. |
| */ |
| @Override |
| public RenderedImage render(GridExtent sliceExtent) { |
| final GridExtent available = gridGeometry.getExtent(); |
| final int dimension = available.getDimension(); |
| if (sliceExtent == null) { |
| sliceExtent = available; |
| } else { |
| final int sd = sliceExtent.getDimension(); |
| if (sd != dimension) { |
| throw new MismatchedDimensionException(Errors.format( |
| Errors.Keys.MismatchedDimension_3, "sliceExtent", dimension, sd)); |
| } |
| } |
| final int[] selectedDimensions = sliceExtent.getSubspaceDimensions(BIDIMENSIONAL); |
| if (selectedDimensions[1] != 1) { |
| // TODO |
| throw new UnsupportedOperationException("Non-horizontal slices not yet implemented."); |
| } |
| 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. |
| final int[] offsetAOI = new int[dimension]; // Pixel offset compared to Area Of Interest. |
| final int[] imageSize = new int[dimension]; // Subsampled image size. |
| for (int i=0; i<dimension; i++) { |
| final long min = available .getLow (i); // Lowest valid coordinate in subsampled image. |
| final long max = available .getHigh(i); // Highest valid coordinate, inclusive. |
| final long aoiMin = sliceExtent.getLow (i); // Requested coordinate in subsampled image. |
| final long aoiMax = sliceExtent.getHigh(i); |
| final long tileUp = incrementExact(toTileMatrixCoordinate(Math.min(aoiMax, max), i)); |
| final long tileLo = toTileMatrixCoordinate(Math.max(aoiMin, min), i); |
| if (tileUp <= tileLo) { |
| final String message = Errors.forLocale(getLocale()) |
| .getString(Errors.Keys.IllegalRange_2, aoiMin, aoiMax); |
| if (aoiMin > aoiMax) { |
| throw new IllegalArgumentException(message); |
| } else { |
| throw new DisjointExtentException(message); |
| } |
| } |
| // Lower and upper coordinates in subsampled image, rounded to integer number of tiles and clipped to available data. |
| final long lower = /* inclusive */Math.max(toSubsampledPixel(/* inclusive */multiplyExact(tileLo, tileSize[i]), i), min); |
| final long upper = incrementExact(Math.min(toSubsampledPixel(decrementExact(multiplyExact(tileUp, tileSize[i])), i), max)); |
| imageSize[i] = toIntExact(subtractExact(upper, lower)); |
| offsetAOI[i] = toIntExact(subtractExact(lower, aoiMin)); |
| tileLower[i] = toIntExact(subtractExact(tileLo, tmcOfFirstTile[i])); |
| tileUpper[i] = toIntExact(subtractExact(tileUp, tmcOfFirstTile[i])); |
| } |
| /* |
| * 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(gridGeometry, selectedDimensions); |
| 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, getIdentifier().toFullyQualifiedName()), e); |
| } |
| return image; |
| } |
| |
| /** |
| * The Area Of Interest specified by user in a call to {@link #render(GridExtent)}. |
| * This class is also an iterator over tiles in the region of interest. |
| */ |
| protected final class AOI { |
| /** |
| * Total number of tiles in the AOI, from {@link #tileLower} inclusive to {@link #tileUpper} exclusive. |
| * This is the length of the array to be returned by {@link #readTiles(AOI)}. |
| */ |
| public final int tileCountInQuery; |
| |
| /** |
| * Indices (relative to enclosing {@code TiledGridCoverage}) of the upper-left tile to read. |
| * Tile indices starts at (0, 0, …), not at the indices of the corresponding tile in resource, |
| * for avoiding integer overflow. |
| */ |
| private final int[] tileLower; |
| |
| /** |
| * Indices (relative to enclosing {@code TiledGridCoverage}) after the bottom-right tile to read. |
| */ |
| private final int[] tileUpper; |
| |
| /** |
| * Pixel coordinates to assign to the upper-left corner of the region to render, with subsampling applied. |
| * This is the difference between the region requested by user and the region which will be rendered. |
| */ |
| private final int[] offsetAOI; |
| |
| /** |
| * Tile Matrix Coordinates (TMC) of current iterator position relative to enclosing {@code TiledGridCoverage}. |
| * Initial position is a clone of {@link #tileLower}. This array is modified by calls to {@link #next()}. |
| */ |
| private final int[] tmcInSubset; |
| |
| /** |
| * Pixel coordinates of current iterator position relative to the Area Of Interest specified by user. |
| * Those coordinates are in units of the full resolution image. |
| * Initial position is {@link #offsetAOI} multiplied by {@link #subsampling}. |
| * This array is modified by calls to {@link #next()}. |
| */ |
| private final long[] tileOffsetFull; |
| |
| /** |
| * Current iterator position as an index in the array of tiles to be returned by {@link #readTiles(AOI)}. |
| * The initial position is zero. This field is incremented by calls to {@link #next()}. |
| */ |
| private int indexInResultArray; |
| |
| /** |
| * Current iterator position as an index in the vector of tiles in the {@link TiledGridResource}. |
| * Tiles are assumed stored in a row-major fashion. This field is incremented by calls to {@link #next()}. |
| */ |
| private int indexInTileVector; |
| |
| /** |
| * Creates a new Area Of Interest for the given tile indices. |
| * |
| * @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. |
| * @param offsetAOI pixel coordinates to assign to the upper-left corner of the subsampled region to render. |
| * @param dimension number of dimension of the {@code TiledGridCoverage} grid extent. |
| */ |
| AOI(final int[] tileLower, final int[] tileUpper, final int[] offsetAOI, final int dimension) { |
| this.tileLower = tileLower; |
| this.tileUpper = tileUpper; |
| this.offsetAOI = offsetAOI; |
| tileOffsetFull = new long[offsetAOI.length]; |
| /* |
| * Initialize variables to values for the first tile to read. The loop does arguments validation and |
| * converts the `tileLower` coordinates to index in the `tileOffsets` and `tileByteCounts` vectors. |
| */ |
| indexInTileVector = indexOfFirstTile; |
| int tileCountInQuery = 1; |
| for (int i=0; i<dimension; i++) { |
| final int lower = tileLower[i]; |
| final int count = subtractExact(tileUpper[i], lower); |
| indexInTileVector = addExact(indexInTileVector, multiplyExact(tileStrides[i], lower)); |
| tileCountInQuery = multiplyExact(tileCountInQuery, count); |
| tileOffsetFull[i] = multiplyFull(offsetAOI[i], subsampling[i]); |
| /* |
| * Following is the pixel coordinate after the last pixel in current dimension. |
| * This is not stored; the intent is to get a potential `ArithmeticException` |
| * now instead of in a call to `next()` during iteration. A negative value |
| * would mean that the AOI does not intersect the region requested by user. |
| */ |
| final int max = addExact(offsetAOI[i], multiplyExact(tileSize[i], count)); |
| assert max > Math.max(offsetAOI[i], 0) : max; |
| } |
| this.tileCountInQuery = tileCountInQuery; |
| this.tmcInSubset = tileLower.clone(); |
| } |
| |
| /** |
| * Returns a new {@code AOI} instance over a sub-region of this Area Of Interest. |
| * 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() { |
| return TiledGridCoverage.this; |
| } |
| |
| /** |
| * Returns the current iterator position as an index in the array of tiles to be returned |
| * by {@link #readTiles(AOI)}. The initial position is zero. |
| * The position is incremented by 1 in each call to {@link #next()}. |
| * |
| * @return current iterator position in result array. |
| */ |
| public final int getIndexInResultArray() { |
| return indexInResultArray; |
| } |
| |
| /** |
| * Returns the cached tile for current iterator position. |
| * |
| * @return cached tile at current iterator position, or {@code null} if none. |
| * |
| * @see Snapshot#cache(Raster) |
| */ |
| public Raster getCachedTile() { |
| final Raster tile = rasters.get(createCacheKey(indexInTileVector)); |
| if (tile != null) { |
| /* |
| * Found a tile, but the sample model may be different because band order may be different. |
| * In both cases, we need to make sure that the raster starts at the expected coordinates. |
| */ |
| final int x = getTileOrigin(X_DIMENSION); |
| final int y = getTileOrigin(Y_DIMENSION); |
| if (model.equals(tile.getSampleModel())) { |
| if (tile.getMinX() == x && tile.getMinY() == y) { |
| return tile; |
| } |
| return tile.createTranslatedChild(x, y); |
| } |
| /* |
| * If the sample model is not the same (e.g. different bands), it must at least have the same size. |
| * Having a sample model of different size would probably be a bug, but we check anyway for safety. |
| * Note that the tile size is not necessarily equals to the sample model size. |
| */ |
| final SampleModel sm = tile.getSampleModel(); |
| if (sm.getWidth() == model.getWidth() && sm.getHeight() == model.getHeight()) { |
| final int width = tile.getWidth(); // May be smaller than sample model width. |
| final int height = tile.getHeight(); // Idem. |
| /* |
| * It is okay to have a different number of bands if the sample model is |
| * a view created by `SampleModel.createSubsetSampleModel(int[] bands)`. |
| * Bands can also be in a different order and still share the same buffer. |
| */ |
| Raster r = Raster.createRaster(model, tile.getDataBuffer(), new Point(x, y)); |
| if (r.getWidth() != width || r.getHeight() != height) { |
| r = r.createChild(x, y, width, height, x, y, null); |
| } |
| return r; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the origin to assign to the tile at current iterator position. |
| * Note that the subsampling should be a divisor of tile size, |
| * otherwise a drift in pixel coordinates will appear. |
| * There are two exceptions to this rule: |
| * |
| * <ul> |
| * <li>If image is untiled (i.e. there is only one tile), |
| * we allow to read a sub-region of the unique tile.</li> |
| * <li>If subsampling is larger than tile size.</li> |
| * </ul> |
| */ |
| final int getTileOrigin(final int dimension) { |
| /* |
| * We really need `ceilDiv(…)` below, not `floorDiv(…)`. It makes no difference in the usual |
| * case where the subsampling is a divisor of the tile size (the numerator is initialized to |
| * a multiple of the denominator, then incremented by another multiple of the denominator). |
| * It makes no difference in the untiled case neither because the numerator is not incremented. |
| * But if we are in the case where subsampling is larger than tile size, then we want rounding |
| * to the next tile. The tile seems "too far", but it will either be discarded at a later step |
| * (because of empty intersection with AOI) or compensated by the offset caused by subsampling. |
| * At first the index values seem inconsistent, but after we discard the tiles where |
| * `getRegionInsideTile(…)` returns `false` they become consistent. |
| */ |
| return toIntExact(ceilDiv(tileOffsetFull[dimension], subsampling[dimension])); |
| } |
| |
| /** |
| * Moves the iterator position to next tile. This method should be invoked in a loop as below: |
| * |
| * {@snippet lang="java" : |
| * do { |
| * // Process current tile. |
| * } while (domain.next()); |
| * } |
| * |
| * @return {@code true} on success, or {@code false} if the iteration is finished. |
| */ |
| public boolean next() { |
| if (++indexInResultArray >= tileCountInQuery) { |
| return false; |
| } |
| /* |
| * Iterates over all tiles in the region specified to this method by maintaining 4 indices: |
| * |
| * - `indexInResultArray` is the index in the `rasters` array to be returned. |
| * - `indexInTileVector` is the corresponding index in the `tileOffsets` vector. |
| * - `tmcInSubset[]` contains the Tile Matrix Coordinates (TMC) relative to this `TiledGridCoverage`. |
| * - `tileOffsetFull[]` contains the pixel coordinates relative to the user-specified AOI. |
| * |
| * We do not check for integer overflow in this method because if an overflow is possible, |
| * then `ArithmeticException` should have occurred in `TiledGridCoverage` constructor. |
| */ |
| for (int i=0; i<tmcInSubset.length; i++) { |
| indexInTileVector += tileStrides[i]; |
| if (++tmcInSubset[i] < tileUpper[i]) { |
| tileOffsetFull[i] += tileSize[i]; |
| break; |
| } |
| // Rewind to index for tileLower[i]. |
| indexInTileVector -= (tmcInSubset[i] - tileLower[i]) * tileStrides[i]; |
| tmcInSubset [i] = tileLower[i]; |
| tileOffsetFull[i] = multiplyFull(offsetAOI[i], subsampling[i]); |
| } |
| return true; |
| } |
| } |
| |
| /** |
| * Snapshot of a {@link AOI} iterator position. Those snapshots can be created during an iteration |
| * for processing a tile later. For example, a {@link #readTiles(AOI)} method implementation may want |
| * to create a list of all tiles to load before to start the actual reading process in order to read |
| * the tiles in some optimal order, or for combining multiple read operations in a single operation. |
| */ |
| protected static class Snapshot { |
| /** |
| * The source coverage. |
| */ |
| private final TiledGridCoverage coverage; |
| |
| /** |
| * Tile Matrix Coordinates (TMC) relative to the enclosing {@link TiledGridCoverage}. |
| * Tile (0,0) is the tile in the upper-left corner of this {@link TiledGridCoverage}, |
| * not necessarily the tile in the upper-left corner of the image in the resource. |
| */ |
| private final int[] tmcInSubset; |
| |
| /** |
| * Index of this tile in the array of tiles returned by {@link #readTiles(AOI)}. |
| */ |
| public final int indexInResultArray; |
| |
| /** |
| * Index of this tile in the {@link TiledGridResource}. In a GeoTIFF image, this is |
| * the index of the tile in the {@code tileOffsets} and {@code tileByteCounts} vectors. |
| * This index is also used as key in the {@link TiledGridCoverage#rasters} map. |
| */ |
| public final int indexInTileVector; |
| |
| /** |
| * Pixel coordinates of the upper-left corner of the tile. |
| */ |
| public final int originX, originY; |
| |
| /** |
| * Stores information about a tile to be loaded. |
| * |
| * @param iterator the iterator for which to create a snapshot of its current position. |
| */ |
| public Snapshot(final AOI iterator) { |
| coverage = iterator.getCoverage(); |
| tmcInSubset = iterator.tmcInSubset.clone(); |
| indexInResultArray = iterator.indexInResultArray; |
| indexInTileVector = iterator.indexInTileVector; |
| originX = iterator.getTileOrigin(X_DIMENSION); |
| originY = iterator.getTileOrigin(Y_DIMENSION); |
| } |
| |
| /** |
| * Returns the coordinate of the pixel to read <em>inside</em> the tile, ignoring subsampling. |
| * The tile upper-left corner is assumed (0,0). Consequently, the lower coordinates are usually |
| * (0,0) and the upper coordinates are usually the tile size, but those value may be different |
| * if the enclosing {@link TiledGridCoverage} contains only one (potentially big) tile. |
| * In that case, the reading process is more like untiled image reading. |
| * |
| * <p>The {@link TiledGridCoverage} subsampling is provided for convenience, |
| * but is constant for all tiles regardless the subregion to read. |
| * The same values can be obtained by {@link #getSubsampling(int)}.</p> |
| * |
| * @param lower a pre-allocated array where to store relative coordinates of the first pixel. |
| * @param upper a pre-allocated array where to store relative coordinates after the last pixel. |
| * @param subsampling a pre-allocated array where to store subsampling. |
| * @param dimension number of elements to write in the {@code lower} and {@code upper} arrays. |
| * @return {@code true} on success, or {@code false} if the tile is empty. |
| */ |
| public boolean getRegionInsideTile(final long[] lower, final long[] upper, final int[] subsampling, int dimension) { |
| System.arraycopy(coverage.subsampling, 0, subsampling, 0, dimension); |
| while (--dimension >= 0) { |
| final int tileSize = coverage.tileSize[dimension]; |
| final long tileIndex = addExact(coverage.tmcOfFirstTile[dimension], tmcInSubset[dimension]); |
| final long tileBase = multiplyExact(tileIndex, tileSize); |
| /* |
| * The `offset` value is usually zero or negative because the tile to read should be inside the AOI, |
| * e.g. at the right of the AOI left border. It may be positive if the `TiledGridCoverage` contains |
| * only one (potentially big) tile, so the tile reading process become a reading of untiled data. |
| */ |
| long offset = subtractExact(coverage.readExtent.getLow(dimension), tileBase); |
| long limit = Math.min(addExact(offset, coverage.readExtent.getSize(dimension)), tileSize); |
| if (offset < 0) { |
| /* |
| * Example: for `tileSize` = 10 pixels and `subsampling` = 3, |
| * the pixels to read are represented by black small squares below: |
| * |
| * -10 0 10 20 30 |
| * ┼──────────╫──────────┼──────────┼──────────╫ |
| * │▪▫▫▪▫▫▪▫▫▪║▫▫▪▫▫▪▫▫▪▫│▫▪▫▫▪▫▫▪▫▫│▪▫▫▪▫▫▪▫▫▪║ |
| * ┼──────────╫──────────┼──────────┼──────────╫ |
| * |
| * If reading the second tile, then `tileBase` = 10 and `offset` = -10. |
| * The first pixel to read in the second tile has a subsampling offset. |
| * We usually try to avoid this situation because it causes a variable |
| * number of white squares in tiles (4,3,3,4 in the above example), |
| * except when there is only 1 tile to read in which case offset is tolerated. |
| */ |
| final int s = coverage.subsampling[dimension]; |
| offset %= s; |
| if (offset != 0) { |
| offset += s; |
| } |
| } |
| if (offset >= limit) { // Test for intersection before we adjust the limit. |
| return false; |
| } |
| if (dimension == X_DIMENSION && coverage.forceTileSize) { |
| limit = tileSize; |
| } |
| lower[dimension] = offset; |
| upper[dimension] = limit; |
| } |
| return true; |
| } |
| |
| /** |
| * Stores the given raster in the cache. If another raster existed previously in the cache, |
| * the old raster will be reused if it has the same size and model, or discarded otherwise. |
| * The latter case may happen if {@link AOI#getCachedTile()} determined that a cached raster |
| * exists but cannot be reused. |
| * |
| * @param tile the raster to cache. |
| * @return the cached raster. Should be the given {@code raster} instance, |
| * but this method check for concurrent caching as a paranoiac check. |
| * |
| * @see AOI#getCachedTile() |
| */ |
| public Raster cache(final Raster tile) { |
| final TiledGridResource.CacheKey key = coverage.createCacheKey(indexInTileVector); |
| Raster existing = coverage.rasters.put(key, tile); |
| /* |
| * If a tile already exists, verify if its layout is compatible with the given tile. |
| * If yes, we assume that the two tiles have the same content. We do this check as a |
| * safety but it should not happen if the caller synchronized the tile read actions. |
| */ |
| if (existing != null |
| && existing.getSampleModel().equals(tile.getSampleModel()) |
| && existing.getWidth() == tile.getWidth() |
| && existing.getHeight() == tile.getHeight()) |
| { |
| // Restore the existing tile in the cache, with its original position. |
| if (coverage.rasters.replace(key, tile, existing)) { |
| final int x = tile.getMinX(); |
| final int y = tile.getMinY(); |
| if (existing.getMinX() != x || existing.getMinY() != y) { |
| existing = existing.createTranslatedChild(x, y); |
| } |
| return existing; |
| } |
| } |
| return tile; |
| } |
| } |
| |
| /** |
| * Creates the key to use for caching the tile at given index. |
| */ |
| private TiledGridResource.CacheKey createCacheKey(final int indexInTileVector) { |
| return new TiledGridResource.CacheKey(indexInTileVector, includedBands, subsampling, subsamplingOffsets); |
| } |
| |
| /** |
| * Returns all tiles in the given area of interest. Tile indices are relative to this {@code TiledGridCoverage}: |
| * (0,0) is the tile in the upper-left corner of this {@code TiledGridCoverage} (not necessarily the upper-left |
| * corner of the image in the {@link TiledGridResource}). |
| * |
| * 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. 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}. |
| * @throws IOException if an I/O error occurred. |
| * @throws DataStoreException if a logical error occurred. |
| * @throws RuntimeException if the Java2D image cannot be created for another reason |
| * (too many exception types to list them all). |
| */ |
| protected abstract Raster[] readTiles(AOI iterator) throws IOException, DataStoreException; |
| } |