Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public static RenderedImage singleTile(final RenderedImage source, final int til
0, 0,
tileWidth,
tileHeight,
0, 0);
tileX, tileY);
return image.isIdentity() ? image.source : image;
}

Expand Down Expand Up @@ -298,6 +298,14 @@ private Raster offset(final Raster data) {
*/
@Override
public Raster getTile(final int tileX, final int tileY) {
// Ensure reshaped image strictly respect its boundaries and does not access source tiles outside its domain.
if (!(
getMinTileX() <= tileX && tileX < getMinTileX() + getNumXTiles()
&&
getMinTileY() <= tileY && tileY < getMinTileY() + getNumYTiles()
)) {
throw new IllegalArgumentException("Requested tile is outside Image domain");
}
return offset(source.getTile(tileX, tileY));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,28 @@ public void testMultiTiles() {
{1210, 1211, 1212 , 1310, 1311, 1312 , 1410, 1411, 1412}
});
}

/**
* Verify a reshaped image created to expose a single tile from a source tiled image only serves the requested tile.
*/
@Test
public void testExposeSingleTileFromTiledImage() {
var source = new TiledImageMock(DataBuffer.TYPE_USHORT, 1, 0, 0, 4, 4, 2, 2, 0, 0, false);
source.validate();
source.initializeAllTiles(0);

var lastTile = ReshapedImage.singleTile(source, 1, 1);
try {
lastTile.getTile(0, 0);
fail("Tile (0, 0) should not be available");
} catch (IllegalArgumentException e) {
// Expected
}

final var exposedTile = lastTile.getTile(1, 1);
assertValuesEqual(exposedTile, 0, new int[][] {
{ 400, 401 },
{ 410, 411 }
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.NoSuchDataException;
import org.apache.sis.storage.UnsupportedQueryException;
import org.apache.sis.storage.InternalDataStoreException;
import org.apache.sis.storage.Resource;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
Expand Down Expand Up @@ -132,6 +131,12 @@ final class ImageTileMatrix implements TileMatrix {
*/
private RenderedImage image;

/**
* Extent of {@link #image} in the {@link #coverage} {@link TileMatrix#getTilingScheme() tiling scheme}.
* Id {@link #image} is null, this should also be null. If image is not null, this must not be null.
*/
private GridExtent imageTilingExtent;

/**
* The grid coverage processor to use when tiles use a subset of the bands.
*
Expand Down Expand Up @@ -350,13 +355,62 @@ public Stream<Tile> getTiles(GridExtent indiceRanges, final boolean parallel) th
if (indiceRanges == null) {
indiceRanges = tilingScheme.getExtent();
}

final var coverage = coverage();
for (int dim = 0 ; dim < indiceRanges.getDimension(); dim++) {
if (dim != coverage.xDimension && dim != coverage.yDimension && dim > 1) {
return slice(indiceRanges, coverage.xDimension, coverage.yDimension, parallel);
}
}

assert indiceRanges.getDegreesOfFreedom() <= 2 : "This code should only be reached if requested extent is 2D";
try {
return StreamSupport.stream(iterator(indiceRanges).iterator(), parallel);
} catch (ArithmeticException e) {
throw new UnsupportedQueryException(e);
}
}

/**
* Split given extent in a lazy sequence of 2D extents. For each 2D slice,
* we query all tiles contained in user requested area.
*
* @param indiceRanges N-D tile extent to slice and to load tiles for.
* @param xDim X dimension of the coverage, first axis of the 2D part to preserve in slices.
* @param yDim Y dimension of the coverage, second axis of the 2D part to preserve in slices.
* @param parallel True if we want a parallel stream returned, false otherwise.
* @return A lazy sequence of all tiles contained in the given tile range.
*/
private Stream<Tile> slice(GridExtent indiceRanges, int xDim, int yDim, boolean parallel) {
final var slicingStart = indiceRanges.getLow().getCoordinateValues();
final var slicingEnd = indiceRanges.getHigh().getCoordinateValues();
final long xMax = slicingEnd[xDim];
final long yMax = slicingEnd[yDim];
slicingEnd[xDim] = slicingStart[xDim];
slicingEnd[yDim] = slicingStart[yDim];

final var slicingExtent = new GridExtent(null, slicingStart, slicingEnd, true);
// NOTE: not sure here, but depending on stream fork policy,
// allowing parallel extents ould hurt performance,
// because it potentially allows to load tiles from different slices in parallel.
// As this class image caching strategy is based on 2D extents,
// we instead push parallelism down on tile loading level directly (see flatMap block).
return slicingExtent.latticePointStream(false)
.map(slicePoint -> {
final var sliceHigh = Arrays.copyOf(slicePoint, slicePoint.length);
sliceHigh[xDim] = xMax;
sliceHigh[yDim] = yMax;
return new GridExtent(null, slicePoint, sliceHigh, true);
})
.flatMap(slice -> {
try {
return StreamSupport.stream(iterator(slice).iterator(), parallel);
} catch (DataStoreException e) {
throw new BackingStoreException("Cannot load tiles for 2D extent", e);
}
});
}

/**
* Creates an object which can be used for retrieving a single tile or a stream tiles.
*
Expand All @@ -368,25 +422,12 @@ public Stream<Tile> getTiles(GridExtent indiceRanges, final boolean parallel) th
private synchronized IterationDomain<Tile> iterator(final GridExtent indiceRanges) throws DataStoreException {
@SuppressWarnings("LocalVariableHidesMemberVariable")
final TiledGridCoverage coverage = coverage();
boolean retry = false;
do { // This loop will be executed only 1 or 2 times.
if (image != null) {
final long xmin, ymin, xmax, ymax;
xmin = Math.subtractExact(indiceRanges.getLow (coverage.xDimension), imageToTileX);
xmax = Math.subtractExact(indiceRanges.getHigh(coverage.xDimension), imageToTileX);
final long x0 = image.getMinTileX();
if (xmin >= x0 && xmax < x0 + image.getNumXTiles()) {
ymin = Math.subtractExact(indiceRanges.getLow (coverage.yDimension), imageToTileY);
ymax = Math.subtractExact(indiceRanges.getHigh(coverage.yDimension), imageToTileY);
final long y0 = image.getMinTileY();
if (ymin >= y0 && ymax < y0 + image.getNumYTiles()) {
return new Iterator(Math.toIntExact(xmin),
Math.toIntExact(ymin),
Math.toIntExact(xmax),
Math.toIntExact(ymax));
}
}
}
assert Arrays.equals(indiceRanges.getSubspaceDimensions(2), new int[] {coverage.xDimension, coverage.yDimension})
: "Iterator can only return tiles for a 2D slice over coverage XY dimensions.";

// Returns currently cached image if it
final var indiceRangesLow = indiceRanges.getLow().getCoordinateValues();
if (image == null || !imageTilingExtent.contains(indiceRanges)) {
/*
* Gets the bounds of the image to read. If deferred reading is supported,
* we can expand to the bounds of the whole coverage in order to perform a
Expand All @@ -399,7 +440,7 @@ private synchronized IterationDomain<Tile> iterator(final GridExtent indiceRange
for (int i=0; i<dimension; i++) {
final long limit = Math.incrementExact(extent.getHigh(i));
high[i] = Math.min(limit, tileToCell(Math.incrementExact(indiceRanges.getHigh(i)), i));
low [i] = Math.max(extent.getLow(i), tileToCell(indiceRanges.getLow(i), i));
low [i] = Math.max(extent.getLow(i), tileToCell(indiceRangesLow[i], i));
final long span = high[i] - low[i];
if (span < 0 || span > Integer.MAX_VALUE) {
throw new ArithmeticException(resource.errors().getString(Errors.Keys.IntegerOverflow_1, Integer.SIZE));
Expand All @@ -412,11 +453,24 @@ private synchronized IterationDomain<Tile> iterator(final GridExtent indiceRange
high[i] += after;
}
}
image = coverage.render(extent.reshape(low, high, false));


final var imagePixelExtent = extent.reshape(low, high, false);
imageTilingExtent = imagePixelExtent
.translate(Arrays.stream(tileToCell).map(v -> -v).toArray())
.subsample(Arrays.stream(tileSize).mapToLong(v -> v).toArray());
image = coverage.render(imagePixelExtent);
imageToTileX = low[coverage.xDimension];
imageToTileY = low[coverage.yDimension];
} while ((retry = !retry) == true);
throw new InternalDataStoreException(); // Should never happen.
}

return new Iterator(
Math.toIntExact(Math.subtractExact(indiceRangesLow[coverage.xDimension], imageToTileX)),
Math.toIntExact(Math.subtractExact(indiceRangesLow[coverage.yDimension], imageToTileY)),
Math.toIntExact(Math.subtractExact(indiceRanges.getHigh(coverage.xDimension), imageToTileX)),
Math.toIntExact(Math.subtractExact(indiceRanges.getHigh(coverage.yDimension), imageToTileY)),
indiceRangesLow
);
}

/**
Expand Down Expand Up @@ -448,19 +502,42 @@ private final class Iterator extends IterationDomain<Tile> {
*/
private final long offsetX, offsetY;

/**
* Dimension indices for the X and Y axes in the tile matrix coordinate system.
*/
private final int xDim, yDim;

/**
* Base template of tile indices.
* Used when returning {@link Tile#getIndices() tile indices}.
* Tiles will use these coordinates, replacing {link #xDim X} and {link #yDim Y} dimensions.
*/
private final long[] baseIndices;

/**
* Creates a new request for tile iterators.
*
* @param xmin first column index of tiles, inclusive.
* @param xmin first row index of tiles, inclusive.
* @param xmax last column index of tiles, inclusive.
* @param ymax last row index of tiles, inclusive.
* @param xmin first column index of tiles, inclusive.
* @param ymin first row index of tiles, inclusive.
* @param xmax last column index of tiles, inclusive.
* @param ymax last row index of tiles, inclusive.
* @param baseIndices tile coordinate template.
* It can be any valid coordinate of the tiles managed by this iterator.
* It serves as base to build correct tile coordinate for each returned tile.
* Each tile will clone this array and replace its X and Y indices with its own.
* Therefore, it is important that it represent properly the "slice" of extra-dimensions
* this iterator operates on.
*/
Iterator(final int xmin, final int ymin, final int xmax, final int ymax) {
Iterator(final int xmin, final int ymin, final int xmax, final int ymax,
final long[] baseIndices)
{
super(xmin, ymin, xmax, ymax);
tiles = image;
offsetX = imageToTileX;
offsetY = imageToTileY;
xDim = coverage.xDimension;
yDim = coverage.yDimension;
this.baseIndices = baseIndices;
}

/**
Expand All @@ -481,7 +558,10 @@ protected Tile createTile(final int tileX, final int tileY) {

/** Returns the indices of this tile in the {@code TileMatrix}. */
@Override public long[] getIndices() {
return new long[] {offsetX + tileX, offsetY + tileY};
final long[] indices = baseIndices.clone();
indices[xDim] = offsetX + tileX;
indices[yDim] = offsetY + tileY;
return indices;
}

/** Returns information about whether the tile failed to load. */
Expand Down
Loading