You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/06/08 10:45:19 UTC
[sis] branch GeoTIFF updated: Bug fix. Testing the reading of
random sub-regions now pass.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch GeoTIFF
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/GeoTIFF by this push:
new e1bd0d9 Bug fix. Testing the reading of random sub-regions now pass.
e1bd0d9 is described below
commit e1bd0d9503b8b241663cea34a4517e868992f37e
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Jun 8 12:44:53 2021 +0200
Bug fix. Testing the reading of random sub-regions now pass.
---
.../java/org/apache/sis/image/PlanarImage.java | 8 +-
.../sis/internal/coverage/j2d/TiledImage.java | 37 ++-
.../org/apache/sis/storage/geotiff/DataCube.java | 10 +-
.../org/apache/sis/storage/geotiff/DataSubset.java | 270 ++++++++++++---------
.../apache/sis/storage/geotiff/PixelDecoder.java | 41 ++--
.../sis/test/storage/CoverageReadConsistency.java | 12 +-
6 files changed, 224 insertions(+), 154 deletions(-)
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index 397dec2..0dd5392 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -551,17 +551,17 @@ public abstract class PlanarImage implements RenderedImage {
if (sm.getHeight() < tileHeight) return "tileHeight";
}
long size = getWidth();
- long remainder = size - JDK9.multiplyFull(getNumXTiles(), tileWidth);
+ long remainder = JDK9.multiplyFull(getNumXTiles(), tileWidth) - size;
if (remainder != 0) {
return (remainder >= 0 && remainder < size) ? "width" : "numXTiles";
}
size = getHeight();
- remainder = size - JDK9.multiplyFull(getNumYTiles(), tileHeight);
+ remainder = JDK9.multiplyFull(getNumYTiles(), tileHeight) - size;
if (remainder != 0) {
return (remainder >= 0 && remainder < size) ? "height" : "numYTiles";
}
- if (JDK9.multiplyFull(getMinTileX(), tileWidth) + getTileGridXOffset() != getMinX()) return "tileX";
- if (JDK9.multiplyFull(getMinTileY(), tileHeight) + getTileGridYOffset() != getMinY()) return "tileY";
+ if (JDK9.multiplyFull(getMinTileX(), tileWidth) + getTileGridXOffset() != getMinX()) return "tileX";
+ if (JDK9.multiplyFull(getMinTileY(), tileHeight) + getTileGridYOffset() != getMinY()) return "tileY";
return null;
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java
index 8cce878..abecce8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TiledImage.java
@@ -80,8 +80,7 @@ public class TiledImage extends PlanarImage {
* @param height number of pixels along Y axis in the whole rendered image.
* @param minTileX minimum tile index in the X direction.
* @param minTileY minimum tile index in the Y direction.
- * @param tiles the tiles. Must contains at least one element.
- * This array is not cloned.
+ * @param tiles the tiles. Must contains at least one element. This array is not cloned.
*/
public TiledImage(final Map<String,Object> properties, final ColorModel colorModel,
final int width, final int height, final int minTileX, final int minTileY,
@@ -100,6 +99,40 @@ public class TiledImage extends PlanarImage {
}
/**
+ * Verifies whether image layout information and tile coordinates are consistent.
+ * This method verifies the size and minimum pixel coordinates of all tiles,
+ * in addition to the verifications documented in the super-class.
+ *
+ * @return {@code null} if image layout information are consistent,
+ * or the name of inconsistent attribute if a problem is found.
+ */
+ @Override
+ public String verify() {
+ final int minX = getMinX();
+ final int minY = getMinY();
+ final int numXTiles = getNumXTiles();
+ final int tileWidth = getTileWidth();
+ final int tileHeight = getTileHeight();
+ for (int i=0; i < tiles.length; i++) {
+ final Raster tile = tiles[i];
+ final int tx = i % numXTiles;
+ final int ty = i / numXTiles;
+ if (tile.getWidth() != tileWidth) return property(tx, ty, "width");
+ if (tile.getHeight() != tileHeight) return property(tx, ty, "height");
+ if (tile.getMinX() != tileWidth * tx + minX) return property(tx, ty, "x");
+ if (tile.getMinY() != tileHeight * ty + minY) return property(tx, ty, "y");
+ }
+ return super.verify();
+ }
+
+ /**
+ * Label returned by {@link #verify()} for identifying an error in a specified tile.
+ */
+ private static String property(final int tx, final int ty, final String name) {
+ return "tiles[" + tx + ", " + ty + "]." + name;
+ }
+
+ /**
* Returns the color model, or {@code null} if none.
*/
@Override
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
index a709d6a..649b0ec 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
@@ -62,7 +62,9 @@ abstract class DataCube extends AbstractGridResource implements ResourceOnFileSy
* All tiles loaded by any {@link GridCoverage} created from this resource.
* Keys are the same indices than the ones used by {@link #getTileArrayInfo()}.
* For each value, the {@link WritableRaster#getMinX()} and {@code minY} values
- * shall be the pixel coordinates of the raster upper-left corner relative to the full image.
+ * can be anything, depending which {@link DataSubset} was first to load the tile.
+ *
+ * @see DataSubset#rasters
*/
private final WeakValueHashMap<Integer,WritableRaster> rasters;
@@ -170,7 +172,7 @@ abstract class DataCube extends AbstractGridResource implements ResourceOnFileSy
final GridExtent fullSize = gridGeometry.getExtent();
boolean sharedCache = true;
final int[] subsamplings;
- GridExtent areaOfInterest;
+ final GridExtent areaOfInterest;
if (domain == null) {
domain = gridGeometry;
areaOfInterest = fullSize;
@@ -181,8 +183,8 @@ abstract class DataCube extends AbstractGridResource implements ResourceOnFileSy
* If an area of interest has been specified, we may need to expand it to an integer amount of tiles.
* But we do not need to do that if the image is untiled; it is okay to read only a sub-region of the
* single tile. We disable the "integer amount of tiles" restriction by setting the tile size to 1.
- * Note that we can disable this restriction in only one dimension, typically the X one when the TIFF
- * image use strips instead of tiles.
+ * Note that it is possible to disable this restriction in a single dimension, typically the X one
+ * when the TIFF image use strips instead of tiles.
*/
int tileWidth = sampleModel.getWidth();
int tileHeight = sampleModel.getHeight();
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 250cebf..308869f 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
@@ -56,6 +56,12 @@ import org.opengis.coverage.CannotEvaluateException;
*/
final class DataSubset extends GridCoverage {
/**
+ * Number of dimensions in a rendered image.
+ * Used for identifying codes where a two-dimensional slice is assumed.
+ */
+ static final int BIDIMENSIONAL = 2;
+
+ /**
* The resource which contain this {@code DataSubset}.
* Used for fetching information like the input channel and where to report warnings.
*/
@@ -83,7 +89,7 @@ final class DataSubset extends GridCoverage {
/**
* Number of tiles along each dimension, inside the domain specified by the user.
* The array length is 2 for a standard TIFF file, but this class accepts a higher number of
- * dimensions if 3- or 4-dimensional data are stored in a GeoTIFF file using some convention.
+ * dimensions if 3- or 4-dimensional data are stored in a TIFF file using some convention.
* This is the number of tiles in the domain specified at construction time, not necessarily
* the number of tiles of the image stored in the TIFF file. Tile indices starts at (0, 0, …),
* not at the indices of the corresponding tile in the TIFF file, for avoiding integer overflow.
@@ -103,31 +109,38 @@ final class DataSubset extends GridCoverage {
private final int indexOfFirstTile;
/**
- * Tile coordinates of the tile in the upper-left corner of this {@code DataSubset}.
- * The upper-left corner of the image stored in the TIFF file has coordinates (0,0).
+ * Pixel coordinates of the upper-left corner of the first tile in this {@code DataSubset}.
+ * Those coordinates are relative to the the upper-left corner of the image stored in the
+ * TIFF file, which has coordinates (0,0) by definition. This is the origin of this subset.
*/
- private final int firstTileX, firstTileY;
+ private final long[] origin;
/**
* The area to read in unit of image pixels (before subsampling).
* Lower values are inclusive and upper values are exclusive.
+ * Coordinates are relative to this {@code DataSubset}, i.e. coordinates (0,0) are at the position
+ * identified by the lower coordinates of the {@code areaOfInterest} argument given to constructor.
+ * Note that {@code areaOfInterest} has usually been rounded to integer amount of tile size.
*
- * <p>In the usual case where tiles are read fully, {@code lowerX|Y} are (0,0) and {@code upperX|Y} are
+ * <p>In the usual case where tiles are read fully, {@code lowers} are (0,0) and {@code uppers} are
* multiples of the tile size. But those values may identify a smaller region if this {@link DataSubset}
* contains only one (potentially large) tile along at least one dimension. In such case, those values
* allow to read only a sub-region of tiles.</p>
*/
- private final long lowerX, lowerY, upperX, upperY;
+ private final long[] relativeLower, relativeUpper;
/**
- * The method for transferring pixel values from GeoTIFF file to this {@code Raster}.
+ * The method for transferring pixel values from TIFF file to this {@code Raster}.
* There is different subtypes depending on decompression methods.
*/
private final PixelDecoder decoder;
/**
* Cache of rasters read by this {@code DataSubset}. This cache may be shared with other {@code DataSubset}
- * instances created for the same {@link DataCube} resource.
+ * instances created for the same {@link DataCube} resource. For each value, the raster {@code minX} and
+ * {@code minY} values can be anything, depending which {@code DataSubset} was first to load the tile.
+ *
+ * @see DataCube#rasters
*/
private final Map<Integer,WritableRaster> rasters;
@@ -136,7 +149,7 @@ final class DataSubset extends GridCoverage {
* by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
*
* @param owner the resource which contain this {@code DataSubset}.
- * @param fullSize the full size of the GeoTIFF image.
+ * @param fullSize the full size of the TIFF image.
* @param areaOfInterest the area to read in unit of the full size image (without subsampling).
* @param domain the sub-region extent, CRS and conversion from cell indices to CRS.
* @param ranges sample dimensions for each image band.
@@ -162,8 +175,8 @@ final class DataSubset extends GridCoverage {
tileStrides = new int[dimension];
tileStrides[0] = 1;
for (int i=0; i < dimension - 1; i++) {
- final int numTile = Math.toIntExact(Numerics.ceilDiv(fullSize.getSize(i), decoder.getChunkSize(i)));
- tileStrides[i+1] = Math.multiplyExact(numTile, tileStrides[i]);
+ final int tileCount = Math.toIntExact(Numerics.ceilDiv(fullSize.getSize(i), decoder.getChunkSize(i)));
+ tileStrides[i+1] = Math.multiplyExact(tileCount, tileStrides[i]);
}
/*
* If a sub-domain is requested, modify the tile indices in order to include only
@@ -171,32 +184,25 @@ final class DataSubset extends GridCoverage {
* as the indices of the first tile, we need to have that tile at index 0 in the
* `tileOffsets` and `tileByteCounts` vectors.
*/
- numTiles = new int[dimension];
- final long[] relativeLows = new long[dimension];
- final long[] relativeHighs = new long[dimension];
- final long[] firstTilePos = new long[dimension];
+ numTiles = new int [dimension];
+ relativeLower = new long[dimension]; // Coordinates of first pixel relative to request.
+ relativeUpper = new long[dimension]; // Relative coordinates of last pixel, exclusive.
+ origin = new long[dimension]; // Minimal pixel coordinates of first requested tile.
long indexOfFirstTile = 0;
for (int i=0; i<dimension; i++) {
- final long low = areaOfInterest.getLow(i); // Always ≥ 0.
- final long high = areaOfInterest.getHigh(i); // Inclusive
- final int size = decoder.getChunkSize(i);
- final long tileLow = low / size; // Always ≥ 0.
- final long tileHigh = high / size; // Inclusive.
- final int tileSpan = Math.toIntExact(tileHigh - tileLow); // Number of tiles minus 1.
- numTiles[i] = Math.incrementExact(tileSpan);
- relativeLows [i] = (low % size);
- relativeHighs[i] = (high % size) + (tileSpan * size);
- firstTilePos [i] = tileLow;
- indexOfFirstTile = Math.addExact(Math.multiplyExact(tileLow, tileStrides[i]), indexOfFirstTile);
+ final long low = areaOfInterest.getLow(i); // Always ≥ 0.
+ final long high = areaOfInterest.getHigh(i); // Inclusive
+ final int tileSize = decoder.getChunkSize(i);
+ final long tileLow = low / tileSize; // Always ≥ 0.
+ final long tileHigh = high / tileSize; // Inclusive.
+ final int tileCount = Math.toIntExact(tileHigh - tileLow); // Number of tiles minus 1.
+ numTiles [i] = Math.incrementExact(tileCount);
+ relativeLower[i] = (low % tileSize);
+ relativeUpper[i] = Math.incrementExact((high % tileSize) + JDK9.multiplyFull(tileCount, tileSize));
+ origin [i] = Math.multiplyExact(tileLow, tileSize);
+ indexOfFirstTile = Math.addExact(Math.multiplyExact(tileLow, tileStrides[i]), indexOfFirstTile);
}
this.indexOfFirstTile = Math.toIntExact(indexOfFirstTile);
- lowerX = relativeLows[0];
- lowerY = relativeLows[1];
- upperX = Math.incrementExact(relativeHighs[0]);
- upperY = Math.incrementExact(relativeHighs[1]);
- firstTileX = Math.toIntExact(firstTilePos [0]);
- firstTileY = Math.toIntExact(firstTilePos [1]);
- // Values in higher dimensions are not needed.
}
/**
@@ -205,11 +211,11 @@ final class DataSubset extends GridCoverage {
*/
private static final class Tile implements Comparable<Tile> {
/**
- * Index of the tile in the two first dimensions, relative to the enclosing {@link DataSubset}.
- * Tile (0,0) is the tile in the upper-left corner of this {@link DataSubset}, not necessarily
- * the tile in the upper-left corner of the image in the TIFF file.
+ * Tile Matrix Coordinates (MC) in the two first dimensions, relative to the enclosing {@link DataSubset}.
+ * Tile (0,0) is the tile in the upper-left corner of this {@link DataSubset}, not necessarily the tile in
+ * the upper-left corner of the image in the TIFF file.
*/
- final int xInSubset, yInSubset;
+ final int[] tileInSubset;
/**
* Index of this tile in the query answered by {@link DataSubset#readTiles(int[], int[])}.
@@ -234,15 +240,21 @@ final class DataSubset extends GridCoverage {
final long byteCount;
/**
+ * Pixel coordinates (relative to the {@link DataSubset}) of the upper-left corner of the tile.
+ */
+ final int minX, minY;
+
+ /**
* Stores information about a tile to be loaded.
*/
- Tile(final DataSubset ds, final int[] tc, final int indexInQuery, final int indexInSource) {
- this.xInSubset = tc[0];
- this.yInSubset = tc[1];
+ Tile(final DataSubset ds, final int[] tc, final int indexInQuery, final int indexInSource, final int[] pixelCoord) {
+ this.tileInSubset = tc.clone();
this.indexInQuery = indexInQuery;
this.indexInSource = indexInSource;
this.byteOffset = ds.tileOffsets.longValue(indexInSource);
this.byteCount = ds.tileByteCounts.longValue(indexInSource);
+ this.minX = pixelCoord[0];
+ this.minY = pixelCoord[1];
}
/**
@@ -256,35 +268,37 @@ final class DataSubset extends GridCoverage {
/**
* Returns all tiles in the given ranges of tile indices. Given indices are relative to this {@code DataSubset}!
* (0,0) is the tile in the upper-left corner of this {@code DataSubset} (not necessarily the upper-left corner
- * of the image stored in the TIFF file). However the {@link WritableRaster#getMinX()} and {@code getMinY()}
- * pixel coordinates are relative to the full image; callers will generally need to apply translations on the
- * tiles returned by this method.
+ * of the image stored in the TIFF file). The {@link WritableRaster#getMinX()} and {@code getMinY()} coordinates
+ * of returned raster will start at the given {@code offsetAOI} values.
*
* <p>This method is thread-safe.</p>
*
- * @param lower indices (relative to this {@code DataSubset} of the upper-left tile to read.
- * @param upper indices (relative to this {@code DataSubset} after the bottom-right tile to read.
- * @return tiles decoded from the GeoTIFF file.
+ * @param tileLower indices (relative to this {@code DataSubset}) of the upper-left tile to read.
+ * @param tileUpper indices (relative to this {@code DataSubset}) after the bottom-right tile to read.
+ * @param offsetAOI pixel coordinates to assign to the upper-left corner of the region to read.
+ * @return tiles decoded from the TIFF file.
* @throws IOException if an I/O error occurred.
* @throws DataStoreContentException if the sample model is not supported.
* @throws RuntimeException if the Java2D image can not be created for another reason
* (too many exception types to list them all).
*/
- private WritableRaster[] readTiles(final int[] lower, final int[] upper) throws IOException, DataStoreContentException {
+ private WritableRaster[] readTiles(final int[] tileLower, final int[] tileUpper, final int[] offsetAOI)
+ throws IOException, DataStoreContentException
+ {
/*
- * Convert the `lower` coordinates to index in the `tileOffsets` and `tileByteCounts` vectors.
+ * Convert the `tileLower` coordinates to index in the `tileOffsets` and `tileByteCounts` vectors.
* This loop does argument validation in same time.
*/
int indexInSource = indexOfFirstTile;
int countInQuery = 1;
for (int i=0; i < tileStrides.length; i++) {
- final int lo = lower[i];
- final int up = upper[i];
- final int n = numTiles[i];
- ArgumentChecks.ensureBetween("lower", 0, n-1, lo);
- ArgumentChecks.ensureBetween("upper", lo+1, n, up);
- indexInSource = Math.addExact(indexInSource, Math.multiplyExact(tileStrides[i], lo));
- countInQuery = Math.multiplyExact(countInQuery, up - lo);
+ final int tlo = tileLower[i];
+ final int tup = tileUpper[i];
+ final int n = numTiles [i];
+ ArgumentChecks.ensureBetween("tileLower", 0, n-1, tlo);
+ ArgumentChecks.ensureBetween("tileUpper", tlo+1, n, tup);
+ indexInSource = Math.addExact(indexInSource, Math.multiplyExact(tileStrides[i], tlo));
+ countInQuery = Math.multiplyExact(countInQuery, tup - tlo);
}
/*
* Prepare an array for all tiles to be returned. Tiles that are already in memory will be stored
@@ -293,24 +307,40 @@ final class DataSubset extends GridCoverage {
final WritableRaster[] result = new WritableRaster[countInQuery];
final Tile[] missings = new Tile[countInQuery];
int numMissings = 0;
- final int[] tc = lower.clone();
+ final int[] tc = tileLower.clone(); // Tile matrix coordinates relative to this subset.
+ final int[] pc = offsetAOI.clone(); // Pixel coordinates relative to Area Of Interest.
synchronized (owner.reader.store) {
/*
- * Iterate over all tiles in the region specified to this method. This loop maintains 3 indices:
+ * Iterate over all tiles in the region specified to this method. This loop maintains 4 indices:
*
* - `indexInQuery` is the index in the `rasters` array to be returned.
* - `indexInSource` is the corresponding index in the `tileOffsets` vector.
- * - `tile` contains the tile indices relative to this `DataSubset`.
+ * - `tc[]` contains the tile matrix coordinates relative to this `DataSubset`.
+ * - `pc[]` contains the oixel coordinates relative to the user-specified AOI.
*/
for (int indexInQuery = 0; indexInQuery < countInQuery; indexInQuery++) {
- if ((result[indexInQuery] = rasters.get(indexInSource)) == null) {
- missings[numMissings++] = new Tile(this, tc, indexInQuery, indexInSource);
+ WritableRaster tile = rasters.get(indexInSource);
+ if (tile != null) {
+ // Found a tile. Make sure that the raster starts at the expected coordinates.
+ final int x=pc[0], y=pc[1];
+ if (tile.getMinX() != x || tile.getMinY() != y) {
+ tile = tile.createWritableTranslatedChild(x, y);
+ }
+ result[indexInQuery] = tile;
+ } else {
+ // Tile not yet loaded. Add to a queue of tiles to load later.
+ missings[numMissings++] = new Tile(this, tc, indexInQuery, indexInSource, pc);
}
+ // Increment all indices other than `indexInQuery`.
for (int i=0; i<tc.length; i++) {
indexInSource += tileStrides[i];
- if (++tc[i] < upper[i]) break;
- indexInSource -= (tc[i] - lower[i]) * tileStrides[i]; // Rewind to lower[i].
- tc[i] = lower[i];
+ if (++tc[i] < tileUpper[i]) {
+ pc[i] = Math.addExact(pc[i], decoder.getChunkSize(i));
+ break;
+ }
+ indexInSource -= (tc[i] - tileLower[i]) * tileStrides[i]; // Rewind to index for tileLower[i].
+ tc[i] = tileLower[i];
+ pc[i] = offsetAOI[i];
}
}
Arrays.sort(missings, 0, numMissings);
@@ -320,20 +350,21 @@ final class DataSubset extends GridCoverage {
* TODO: Use `tile.byteCount` for checking if two tiles are consecutive in the TIFF file.
* In such case we should send only one HTTP range request.
*/
- final Point location = new Point();
- for (int i=0; i < numMissings; i++) {
- final Tile tile = missings[i];
- final long minX = JDK9.multiplyFull(tile.xInSubset, decoder.tileWidth);
- final long minY = JDK9.multiplyFull(tile.yInSubset, decoder.tileHeight);
- final long offset = Math.addExact(owner.reader.origin, tile.byteOffset);
- location.x = Math.addExact(firstTileX, tile.xInSubset);
- location.y = Math.addExact(firstTileY, tile.yInSubset);
- final WritableRaster raster = decoder.readSlice(offset, tile.byteCount,
- Math.max(lowerX - minX, 0), Math.min(upperX - minX, decoder.tileWidth),
- Math.max(lowerY - minY, 0), Math.min(upperY - minY, decoder.tileHeight),
- location);
-
- // The `existing` raster should always be null, but we check anyway by paranoia.
+ final Point location = new Point(); // Tile upper-left corner coordinates relative to the request.
+ final long[] lower = new long[BIDIMENSIONAL]; // Coordinates of the first pixel to read relative to the tile.
+ final long[] upper = new long[BIDIMENSIONAL]; // Coordinates after the last pixel to read relative to the tile.
+ for (int i=0; i<numMissings; i++) {
+ final Tile tile = missings[i];
+ final long offset = Math.addExact(owner.reader.origin, tile.byteOffset);
+ for (int d=0; d<BIDIMENSIONAL; d++) {
+ final int tileSize = decoder.getChunkSize(d);
+ final long relativeBase = JDK9.multiplyFull(tile.tileInSubset[d], tileSize);
+ lower[d] = Math.max(relativeLower[d] - relativeBase, 0);
+ upper[d] = Math.min(relativeUpper[d] - relativeBase, tileSize);
+ }
+ location.x = tile.minX;
+ location.y = tile.minY;
+ final WritableRaster raster = decoder.readSlice(offset, tile.byteCount, lower, upper, location);
final WritableRaster existing = rasters.putIfAbsent(tile.indexInSource, raster);
result[tile.indexInQuery] = (existing != null) ? existing : raster;
}
@@ -349,64 +380,65 @@ final class DataSubset extends GridCoverage {
*/
@Override
public RenderedImage render(final GridExtent sliceExtent) {
- final int dimension = numTiles.length;
- final int[] upper = numTiles.clone();
- final int[] lower = new int[dimension];
- final int[] translation = new int[dimension];
+ final int dimension = numTiles.length;
+ final int[] tileUpper = numTiles.clone(); // Indices of last tile to read, exclusive.
+ final int[] tileLower = new int[dimension]; // Indices of first tile to read, inclusive.
+ final int[] offsetAOI = new int[dimension]; // Pixel offset compared to Area Of Interest.
+ final int[] imageSize = new int[dimension];
if (sliceExtent != null) {
- final int[] sd = sliceExtent.getSubspaceDimensions(2);
+ final int[] sd = sliceExtent.getSubspaceDimensions(BIDIMENSIONAL);
if (sd[1] != 1) {
// TODO
- throw new UnsupportedOperationException("Non-horizontal slice not yet implemented.");
+ throw new UnsupportedOperationException("Non-horizontal slices not yet implemented.");
}
for (int i = Math.min(sliceExtent.getDimension(), dimension); --i >= 0;) {
- final int s = decoder.getChunkSize(i);
- final long min = sliceExtent.getLow(i);
- translation[i] = (int) Math.floorMod(min, s); // TODO: remove the cast with JDK9.
- final long lo = Math.max(Math.floorDiv(min, s), 0);
- final long up = Math.min(Numerics.ceilDiv(sliceExtent.getHigh(i), s) + 1, numTiles[i]);
- if (up <= lo) {
- throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalRange_2,
- sliceExtent.getLow(i), sliceExtent.getHigh(i)));
+ final long tilePos = origin[i];
+ final int tileSize = decoder.getChunkSize(i);
+ final long aoiMin = sliceExtent.getLow (i);
+ final long aoiMax = sliceExtent.getHigh (i);
+ final long tLo = Math.max((aoiMin - tilePos) / tileSize, 0); // Tile M.C. relative to this subset.
+ final long tUp = Math.min((aoiMax - tilePos) / tileSize + 1, numTiles[i]);
+ if (tUp <= tLo) {
+ throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalRange_2, aoiMin, aoiMax));
}
- lower[i] = Math.toIntExact(lo);
- upper[i] = Math.toIntExact(up);
+ final long pLo = Math.max(tLo * tileSize, relativeLower[i]); // Relative coordinate of first pixel.
+ final long pUp = Math.min(tUp * tileSize, relativeUpper[i]); // Relative coordinate after last pixel.
+ tileLower[i] = Math.toIntExact(tLo);
+ tileUpper[i] = Math.toIntExact(tUp);
+ imageSize[i] = Math.toIntExact(pUp - pLo);
+ offsetAOI[i] = Math.toIntExact(Math.subtractExact(Math.addExact(pLo, origin[i]), aoiMin));
}
}
+ /*
+ * Get all tiles in the specified region. I/O operations, if needed, happen here.
+ */
try {
- /*
- * Get all tiles in the specified region. I/O operations, if needed, happen here.
- */
- final WritableRaster[] result = readTiles(lower, upper);
- /*
- * The tiles returned by `readTiles(…)` have their coordinates relative to the image upper-left corner.
- * We need to make them relative to the `sliceExtent` requested in this method call.
- */
- final int tx = Math.addExact(translation[0], firstTileX * decoder.tileWidth);
- final int ty = Math.addExact(translation[1], firstTileY * decoder.tileHeight);
- if ((tx | ty) != 0) {
- for (int i=0; i<result.length; i++) {
- final WritableRaster tile = result[i];
- final int x = Math.subtractExact(tile.getMinX(), tx);
- final int y = Math.subtractExact(tile.getMinY(), ty);
- result[i] = tile.createWritableTranslatedChild(x, y);
- }
- }
- /*
- * Put the tiles in the image to be returned.
- */
- final int width = Math.toIntExact(Math.min(JDK9.multiplyFull(upper[0] - lower[0], decoder.tileWidth), upperX - lowerX));
- final int height = Math.toIntExact(Math.min(JDK9.multiplyFull(upper[1] - lower[1], decoder.tileHeight), upperY - lowerY));
- final GridGeometry gg = new ImageRenderer(this, sliceExtent).getImageGeometry(2);
+ final WritableRaster[] result = readTiles(tileLower, tileUpper, offsetAOI);
+ final GridGeometry gg = new ImageRenderer(this, sliceExtent).getImageGeometry(BIDIMENSIONAL);
final ColorModel colorModel;
synchronized (owner.reader.store) {
colorModel = owner.getColorModel();
}
- return new TiledImage(Collections.singletonMap(PlanarImage.GRID_GEOMETRY_KEY, gg),
- colorModel, width, height, lower[0], lower[1], result);
+ final TiledImage image = new TiledImage(Collections.singletonMap(PlanarImage.GRID_GEOMETRY_KEY, gg),
+ colorModel, imageSize[0], imageSize[1], tileLower[0], tileLower[1], result);
+ assert validate(image);
+ return image;
} catch (Exception e) { // Too many exception types for listing them all.
throw new CannotEvaluateException(owner.reader.resources().getString(
Resources.Keys.CanNotRenderImage_1, owner.filename()), e);
}
}
+
+ /**
+ * Verifies if the given image is consistent. This method tolerates smaller width or height compared to
+ * what we would expect from an integer amount of tiles, because this may get smaller height from images
+ * encoded as strips.
+ */
+ private static boolean validate(final TiledImage image) {
+ final String error = image.verify();
+ if (error == null || error.equals("width") || error.equals("height")) {
+ return true;
+ }
+ throw new AssertionError(error);
+ }
}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
index ea4bc0f..3a3def5 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.storage.geotiff;
+import java.util.Arrays;
import java.nio.Buffer;
import java.io.IOException;
import java.awt.Point;
@@ -55,7 +56,7 @@ class PixelDecoder {
/**
* Size of all tiles in the GeoTIFF image (without clipping and subsampling).
*/
- final int tileWidth, tileHeight;
+ private final int tileWidth, tileHeight;
/**
* Subsampling along each dimension. All values shall be greater than zero.
@@ -131,30 +132,26 @@ class PixelDecoder {
* <p>The default implementation in this base class assumes uncompressed data.
* Subclasses must override for handling decompression.</p>
*
- * @param input the channel to read.
* @param offset position in the channel where tile data begins.
* @param byteCount number of bytes for the compressed tile data.
- * @param lowerX <var>x</var> coordinate of the first pixel to read relative to the tile.
- * @param lowerY <var>y</var> coordinate of the first pixel to read relative to the tile.
- * @param upperX <var>x</var> coordinate after the last pixel to read relative to the tile.
- * @param upperY <var>y</var> coordinate after the last pixel to read relative to the tile.
- * @param location coordinates of the tile. Note: values are overwritten by this method.
+ * @param lowerX (<var>x</var>,<var>y</var>) coordinates of the first pixel to read relative to the tile.
+ * @param upperX (<var>x</var>,<var>y</var>) coordinate after the last pixel to read relative to the tile.
+ * @param location pixel coordinates in the upper-left corner of the tile to return.
* @return image decoded from the GeoTIFF file.
* @throws IOException if an I/O error occurred.
* @throws DataStoreContentException if the sample model is not supported.
* @throws RuntimeException if the Java2D image can not be created for another reason
* (too many exception types to list them all).
*/
- WritableRaster readSlice(final long offset, final long byteCount,
- final long lowerX, final long upperX, final long lowerY, final long upperY, final Point location)
- throws IOException, DataStoreContentException
+ WritableRaster readSlice(final long offset, final long byteCount, final long[] lower, final long[] upper,
+ final Point location) throws IOException, DataStoreContentException
{
final int type = model.getDataType();
- final long length = (upperX - lowerX) * (upperY - lowerY) * (DataBuffer.getDataTypeSize(type) / Byte.SIZE);
+ final long length = (upper[0] - lower[0]) * (upper[1] - lower[1]) * (DataBuffer.getDataTypeSize(type) / Byte.SIZE);
if (length > byteCount) {
// We may have less bytes to read if only a subregion is read.
- throw new DataStoreContentException(reader.resources().getString(
- Resources.Keys.UnexpectedTileLength_4, location.x, location.y, length, byteCount));
+ throw new DataStoreContentException(reader.resources().getString(Resources.Keys.UnexpectedTileLength_4,
+ location.x / tileWidth, location.y / tileHeight, length, byteCount));
}
final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type), reader.input, offset);
/*
@@ -168,16 +165,16 @@ class PixelDecoder {
numInterleaved = 1;
}
final long[] size = new long[] {JDK9.multiplyFull(numInterleaved, tileWidth), tileHeight, numBanks};
- final int[] subsample2D = new int[] {subsamplings[0], subsamplings[1], 1};
- final long[] regionLower = new long[] {lowerX, lowerY, 0};
- final long[] regionUpper = new long[] {upperX, upperY, 1};
- final Region region = new Region(size, regionLower, regionUpper, subsample2D);
- final int capacity = Math.toIntExact(region.length);
+ final int[] subsample2D = new int [] {subsamplings[0], subsamplings[1], 1};
+ final long[] regionLower = Arrays.copyOf(lower, DataSubset.BIDIMENSIONAL + 1);
+ final long[] regionUpper = Arrays.copyOf(upper, DataSubset.BIDIMENSIONAL + 1);
final Buffer[] banks = new Buffer[numBanks];
for (int b=0; b<numBanks; b++) {
- regionLower[2] = b;
- regionUpper[2] = b + 1;
- final Buffer buffer = hr.readAsBuffer(region, capacity);
+ regionLower[DataSubset.BIDIMENSIONAL] = b;
+ regionUpper[DataSubset.BIDIMENSIONAL] = b + 1;
+ final Region region = new Region(size, regionLower, regionUpper, subsample2D);
+ final int capacity = Math.toIntExact(region.length);
+ final Buffer buffer = hr.readAsBuffer(region, capacity);
/*
* The buffer returned by `readAsBuffer(…)` may have less data than the buffer capacity
* if the current tile is smaller than the expected tile size (e.g. truncated last tile).
@@ -192,8 +189,6 @@ class PixelDecoder {
}
banks[b] = buffer.limit(capacity);
}
- location.x *= tileWidth;
- location.y *= tileHeight;
final DataBuffer buffer = RasterFactory.wrap(DataType.forDataBufferType(model.getDataType()), banks);
return WritableRaster.createWritableRaster(model, buffer, location);
}
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
index 877ad6d..49b22a1 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
@@ -16,7 +16,9 @@
*/
package org.apache.sis.test.storage;
+import java.util.Arrays;
import java.util.Random;
+import java.awt.Point;
import java.awt.Rectangle;
import org.opengis.referencing.datum.PixelInCell;
import org.apache.sis.coverage.grid.GridCoverage;
@@ -105,7 +107,7 @@ public strictfp class CoverageReadConsistency extends TestCase {
for (int i=0; i<NUM_ITERATIONS; i++) {
for (int d=0; d<dimension; d++) {
final int span = StrictMath.toIntExact(ex.getSize(d));
- final int rs = span-1;//random.nextInt(span); // Span of the sub-region - 1.
+ final int rs = random.nextInt(span); // Span of the sub-region - 1.
low [d] = random.nextInt(span - rs); // Note: (span - rs) > 0.
high[d] = low[d] + rs;
}
@@ -141,7 +143,13 @@ nextSlice: for (;;) {
assertTrue(itc.next());
expected = itr.getPixel(expected);
actual = itc.getPixel(actual);
- assertArrayEquals(expected, actual, STRICT);
+ if (!Arrays.equals(expected, actual)) {
+ final Point pr = itr.getPosition();
+ final Point pc = itc.getPosition();
+ assertArrayEquals("Mismatch at position (" + pr.x + ", " + pr.y + ") in full image " +
+ "and (" + pc.x + ", " + pc.y + ") in tested sub-image",
+ expected, actual, STRICT);
+ }
}
assertFalse(itc.next());
/*