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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 3f88ee02c2 Add an "opaque overlay" merge strategy for `CoverageAggregator`. Detect automatically when the "slices" are actually tiles in a mosaic, in which case the the "opaque overlay" strategy can be automatically selected. Improves `ImageOverlay` implementation for avoiding to copy tiles when possible. 3f88ee02c2 is described below commit 3f88ee02c2e4ba7a4b208ee1dcc2178d50b95bc5 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Jan 15 17:05:42 2025 +0100 Add an "opaque overlay" merge strategy for `CoverageAggregator`. Detect automatically when the "slices" are actually tiles in a mosaic, in which case the the "opaque overlay" strategy can be automatically selected. Improves `ImageOverlay` implementation for avoiding to copy tiles when possible. --- .../apache/sis/coverage/grid/GridDerivation.java | 4 +- .../main/org/apache/sis/image/ComputedImage.java | 2 +- .../main/org/apache/sis/image/ComputedTiles.java | 12 +- .../main/org/apache/sis/image/ImageOverlay.java | 59 +++++- .../main/org/apache/sis/image/ImageProcessor.java | 4 + .../main/org/apache/sis/image/TileCache.java | 5 +- .../aggregate/ConcatenatedGridCoverage.java | 144 +++++++------ .../sis/storage/aggregate/CoverageAggregator.java | 11 +- .../sis/storage/aggregate/DimensionSelector.java | 99 ++++++--- .../apache/sis/storage/aggregate/GridSlice.java | 7 +- .../sis/storage/aggregate/GridSliceLocator.java | 7 +- .../sis/storage/aggregate/GroupAggregate.java | 6 +- .../sis/storage/aggregate/GroupBySample.java | 4 +- .../sis/storage/aggregate/GroupByTransform.java | 62 ++++-- .../sis/storage/aggregate/MergeStrategy.java | 222 +++++++++++++++------ .../org/apache/sis/storage/internal/Resources.java | 4 +- .../sis/storage/internal/Resources.properties | 2 +- .../sis/storage/internal/Resources_fr.properties | 2 +- 18 files changed, 454 insertions(+), 202 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java index fa15a7bcef..44ccc2be1f 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java @@ -1144,7 +1144,7 @@ public class GridDerivation { * The slicing is applied on all dimensions except the specified dimensions to keep. * * <h4>Example</h4> - * given a <var>n</var>-dimensional cube, the following call creates a slice of the two first dimensions + * Given a <var>n</var>-dimensional cube, the following call creates a slice of the two first dimensions * (numbered 0 and 1, typically the dimensions of <var>x</var> and <var>y</var> axes) * located at the center (ratio 0.5) of all other dimensions (typically <var>z</var> and/or <var>t</var> axes): * @@ -1163,7 +1163,7 @@ public class GridDerivation { ArgumentChecks.ensureNonNull("dimensionsToKeep", dimensionsToKeep); subGridSetter = "sliceByRatio"; final GridExtent extent = getBaseExtentExpanded(true); - final GeneralDirectPosition slicePoint = new GeneralDirectPosition(extent.getDimension()); + final var slicePoint = new GeneralDirectPosition(extent.getDimension()); baseExtent = extent.sliceByRatio(slicePoint, sliceRatio, dimensionsToKeep); if (scaledExtent != null) { scaledExtent = scaledExtent.sliceByRatio(slicePoint, sliceRatio, dimensionsToKeep); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java index 8fe2511a93..f8a3bd4f50 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java @@ -803,7 +803,7 @@ public abstract class ComputedImage extends PlanarImage implements Disposable { * tiles from the cache and stops observation of {@link WritableRenderedImage} sources. * This image should not be used anymore after this method call. * - * <p><b>Note:</b> keep in mind that this image may be referenced as a source of other images. + * <p><b>Note:</b> caller should keep in mind that this image may be referenced as a source of other images. * In case of doubt, it may be safer to rely on the garbage collector instead of invoking this method.</p> */ @Override diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedTiles.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedTiles.java index 82778d97e1..b6aee07c9f 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedTiles.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedTiles.java @@ -310,12 +310,14 @@ final class ComputedTiles extends WeakReference<ComputedImage> implements Dispos /** * Invoked when the {@link ComputedImage} has been garbage-collected. This method removes all cached - * tiles that were owned by the image and stops observing all sources. + * tiles that were owned by the image and stops observing all sources. If the same {@link Raster} was + * shared by many images, other images are not impacted. * - * This method should not perform other cleaning work because it is not guaranteed to be invoked if this - * {@code ComputedTiles} is not registered as a {@link TileObserver} and if {@link TileCache#GLOBAL} does - * not contain any tile for the {@link ComputedImage}. The reason is because there would be nothing - * preventing this weak reference to be garbage collected before {@code dispose()} is invoked. + * <p>This method should not perform other cleaning work because it is not guaranteed to be invoked. + * In some case, there is nothing preventing this weak reference to be garbage collected before this + * {@code dispose()} method is invoked. The case is: if {@code ComputedTiles} is not registered as a + * {@link TileObserver} and if {@link TileCache#GLOBAL} does not contain any tile associated to this + * {@link ComputedImage} in its key.</p> * * @see ComputedImage#dispose() */ diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageOverlay.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageOverlay.java index a5f484f3a6..a81e34a308 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageOverlay.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageOverlay.java @@ -41,6 +41,7 @@ import org.apache.sis.math.Statistics; import org.apache.sis.measure.Quantities; import org.apache.sis.feature.internal.Resources; import org.apache.sis.image.privy.ImageUtilities; +import org.apache.sis.image.privy.TilePlaceholder; /** @@ -76,6 +77,11 @@ final class ImageOverlay extends MultiSourceImage { */ private final Area[] contributions; + /** + * Pool of shared rasters for empty tiles. Used when to sources intersect a tile to compute. + */ + private final TilePlaceholder emptyTiles; + /** * Creates a new image overlay or returns one of the given sources if equivalent. * All source images shall have the same pixels coordinate system and the same number of bands. @@ -192,6 +198,7 @@ final class ImageOverlay extends MultiSourceImage { super(sources, bounds, minTile, sampleModel, colorModel, parallel); this.validArea = validArea.isRectangular() ? validArea.getBounds2D() : validArea; this.contributions = contributions; + emptyTiles = TilePlaceholder.empty(sampleModel); } /** @@ -390,22 +397,60 @@ final class ImageOverlay extends MultiSourceImage { */ @Override protected Raster computeTile(final int tileX, final int tileY, WritableRaster target) { - if (target == null) { - target = createTile(tileX, tileY); - } - final Rectangle aoi = target.getBounds(); + final Rectangle aoi = new Rectangle( + ImageUtilities.tileToPixelX(this, tileX), + ImageUtilities.tileToPixelY(this, tileY), + getTileWidth(), + getTileHeight()); + + Raster shared = null; final int n = getNumSources(); for (int i=n; --i >= 0;) { if (contributions[i].intersects(aoi)) { final RenderedImage source = getSource(i); final Rectangle bounds = getBounds(); ImageUtilities.clipBounds(source, bounds); - if (!bounds.isEmpty()) { - copyData(bounds, source, target); + if (bounds.isEmpty()) { + continue; + } + /* + * Found a source image which intersects the tile to write. If this is the first time, + * get the source tile at the same coordinates as the destination tile. If the raster + * covers exactly the same region, maybe we will be able to return it directly without + * copying the pixel values. + */ + if (target == null) { + if (shared == null) { + final int tx = ImageUtilities.pixelToTileX(source, aoi.x); + final int dx = tx - source.getMinTileX(); + if (dx >= 0 && dx < source.getNumXTiles()) { + final int ty = ImageUtilities.pixelToTileY(source, aoi.y); + final int dy = ty - source.getMinTileY(); + if (dy >= 0 && dy < source.getNumYTiles()) { + shared = source.getTile(tx, ty); + if (shared.getMinX() == aoi.x && shared.getMinY() == aoi.y) { + if (sampleModel.equals(shared.getSampleModel())) { + continue; // Accept the tile and skip the copy operation. + } + } + shared = null; + } + } + } + /* + * The source tile cannot be used directly. Its value will be copied in a new tile. + * If `shared` was a candidate for return without copy, it needs to be copied now. + */ + target = WritableRaster.createWritableRaster(sampleModel, aoi.getLocation()); + if (shared != null) { + target.setRect(shared); + shared = null; + } } + copyData(bounds, source, target); } } - return target; + return (target != null) ? target : (shared != null) ? shared : emptyTiles.create(aoi.getLocation()); } /** diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java index d3cef1538e..9cdae190a2 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java @@ -1016,6 +1016,10 @@ public class ImageProcessor implements Cloneable { * that some sources will never be drawn (i.e., are fully hidden behind the first images). * If only one source appears to be effectively used, this method returns that image directly.</p> * + * <h4>Optimization</h4> + * The returned image may share some tiles from any source images (without copy) + * if the tile can be used directly with no change. + * * <h4>Preconditions</h4> * All source images shall have the same number of bands (but not necessarily the same sample model). * All source images should have equivalent color model, otherwise color consistency is not guaranteed. diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/TileCache.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/TileCache.java index 7e4df33a4f..7e26aba855 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/TileCache.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/TileCache.java @@ -31,11 +31,14 @@ import org.apache.sis.pending.jdk.JDK16; * Tiles are kept by strong references until a memory usage limit is reached, in which case * the references of oldest tiles become soft references. * + * <p>The same {@link Raster} may be shared by many images. Removing the tiles of an image + * does not impact other images even if they share the same rasters.</p> + * * <h2>Design note</h2> * The use of a common cache for all images makes easier to set an application-wide limit * (for example 25% of available memory). The use of soft reference does not cause as much * memory retention as it may seem because those references are hold only as long as the - * image exist. When an image is garbage collected, the corresponding soft references are + * image exists. When an image is garbage collected, the corresponding soft references are * {@linkplain Key#dispose() cleaned}. * * @author Martin Desruisseaux (Geomatys) diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedGridCoverage.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedGridCoverage.java index 46d2ccac74..b2cb1e7465 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedGridCoverage.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedGridCoverage.java @@ -17,6 +17,7 @@ package org.apache.sis.storage.aggregate; import java.util.List; +import java.util.Arrays; import java.util.ArrayList; import java.awt.image.RenderedImage; import org.opengis.referencing.operation.TransformException; @@ -29,6 +30,7 @@ import org.apache.sis.coverage.grid.DisjointExtentException; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.internal.Resources; +import org.apache.sis.util.ArraysExt; import org.apache.sis.util.privy.Numerics; import org.apache.sis.util.collection.Cache; import org.apache.sis.util.logging.Logging; @@ -242,7 +244,7 @@ final class ConcatenatedGridCoverage extends GridCoverage { final Object[] c = slices.clone(); for (int i=0; i<c.length; i++) { if (!isDeferred(i)) { - final GridCoverage source = (GridCoverage) c[i]; // Should never fail. + final var source = (GridCoverage) c[i]; // Should never fail. changed |= (c[i] = source.forConvertedValues(converted)) != source; template = source; } else { @@ -268,44 +270,41 @@ final class ConcatenatedGridCoverage extends GridCoverage { * Most recently used slices are cached for future invocations of this method. * * @param extent a subspace of this grid coverage where all dimensions except two have a size of 1 cell. - * @return the grid slice as a rendered image. Image location is relative to {@code sliceExtent}. + * @return the grid slice as a rendered image. Image location is relative to {@code extent}. */ @Override public RenderedImage render(GridExtent extent) { - int lower = startAt, upper = lower + slices.length; + int lower = startAt; + int upper = lower + slices.length; if (extent != null) { upper = locator.getUpper(extent, lower, upper); lower = locator.getLower(extent, lower, upper); } else { extent = gridGeometry.getExtent(); } - final GridGeometry request; // The geographic area and temporal extent requested by user. - final GridGeometry[] candidates; // Grid geometry of all slices that intersect the request. - final int count = upper - lower; - if (count > 1) { + int count = upper - lower; + if (count == 1) { + return slice(extent, lower); + } + /* + * We have a non-trivial number of source coverages to aggregate. + * The `failure` exception will be thrown at the end of this method + * if a merge was attempted but could not find suitable sources. + */ + DisjointExtentException failure = null; + if (count > 0) { if (strategy == null) { - /* - * Cannot infer a slice. If the user specified a single slice but that slice - * maps to more than one coverage, the error message tells that this problem - * can be avoided by specifying a merge strategy. - */ - final short message; - final Object[] arguments; - if (locator.isSlice(extent)) { - message = Resources.Keys.NoSliceMapped_3; - arguments = new Object[] {locator.getDimensionName(extent), lower, count}; - } else { - message = Resources.Keys.NoSliceSpecified_2; - arguments = new Object[] {locator.getDimensionName(extent), count}; - } - throw new SubspaceNotSpecifiedException(Resources.format(message, arguments)); + throw new SubspaceNotSpecifiedException(Resources.format( + Resources.Keys.NoSliceMapped_3, locator.getDimensionName(extent), lower, count)); } /* - * Prepare a list of slice candidates. Later in this method, a single slice will be selected + * Prepare a list of slice candidates. Later in this method, a single slice may be selected * among those candidates using the user-specified merge strategy. Elements in `candidates` - * array will become null if that candidate did not worked and we want to look again among + * array will be removed if that candidate did not worked and we want to look again among * remaining candidates. */ + final GridGeometry request; // The geographic area and temporal extent requested by user. + final GridGeometry[] candidates; // Grid geometry of all slices that intersect the request. try { request = new GridGeometry(getGridGeometry(), extent, null); candidates = new GridGeometry[count]; @@ -318,53 +317,68 @@ final class ConcatenatedGridCoverage extends GridCoverage { } catch (DataStoreException | TransformException e) { throw new CannotEvaluateException(Resources.format(Resources.Keys.CanNotSelectSlice), e); } - } else { - request = null; - candidates = null; - } - /* - * The following loop should be executed exactly once. However, it may happen that the "best" slice - * actually does not intersect the requested extent, for example because the merge strategy looked - * only for temporal intersection and did not saw that the geographic extents do not intersect. - */ - DisjointExtentException failure = null; - if (count > 0) do { - int index = lower; - if (candidates != null) { - final Integer n = strategy.apply(request, candidates); - if (n == null) break; - candidates[n] = null; - index += n; - } - final Object slice = slices[index]; - final GridCoverage coverage; - if (!isDeferred(index)) { - coverage = (GridCoverage) slice; // This cast should never fail. - } else try { - coverage = loader.getOrLoad(index, (GridCoverageResource) slice).forConvertedValues(isConverted); - } catch (DataStoreException e) { - throw new CannotEvaluateException(Resources.format(Resources.Keys.CanNotReadSlice_1, index + startAt), e); - } /* - * At this point, coverage of the "best" slice has been fetched from the cache or read from resource. - * Delegate the rendering to that coverage, after converting the extent from this grid coverage space - * to the slice coordinate space. If the coverage said that the converted extent does not intersect, - * try the "next best" slice until we succeed or until we exhausted the candidate list. + * The following loop should be executed exactly once. However, it may happen that the "best" slice + * actually does not intersect the requested extent, for example because the merge strategy looked + * only for temporal intersection and did not saw that the geographic extent does not intersect. */ - try { - final RenderedImage image = coverage.render(locator.toSliceExtent(extent, index)); - if (failure != null) { - Logging.ignorableException(LOGGER, ConcatenatedGridCoverage.class, "render", failure); + final int[] indexes = ArraysExt.range(lower, lower + count); + final var sources = new RenderedImage[count]; + do { + int accepted = 0; + for (final int i : strategy.filter(request, Arrays.copyOf(candidates, count))) { + try { + sources[accepted] = slice(extent, indexes[i]); + accepted++; // On a separated line for incrementing only on success. + } catch (DisjointExtentException e) { + if (failure == null) failure = e; + else failure.addSuppressed(e); + final int remaining = --count - i; + System.arraycopy(candidates, i+1, candidates, i, remaining); + System.arraycopy(indexes, i+1, indexes, i, remaining); + } } - return image; - } catch (DisjointExtentException e) { - if (failure == null) failure = e; - else failure.addSuppressed(e); - } - } while (candidates != null); + if (accepted > 0) { + if (failure != null) { + Logging.ignorableException(LOGGER, ConcatenatedGridCoverage.class, "render", failure); + } + return strategy.aggregate(ArraysExt.resize(sources, accepted)); + } + } while (count > 0); + } + /* + * No coverage found in the specified area of interest. + */ if (failure == null) { failure = new DisjointExtentException(gridGeometry.getExtent(), extent, locator.searchDimension); } throw failure; } + + /** + * Processes to the rendering of a single slice. + * + * @param extent a subspace of this grid coverage where all dimensions except two have a size of 1 cell. + * @param index index of the slice to render. + * @return the grid slice as a rendered image. Image location is relative to {@code extent}. + * @throws CannotEvaluateException if the slice cannot be rendered. + */ + private RenderedImage slice(final GridExtent extent, final int index) { + final Object slice = slices[index]; + final GridCoverage coverage; + if (!isDeferred(index)) { + coverage = (GridCoverage) slice; // This cast should never fail. + } else try { + coverage = loader.getOrLoad(index, (GridCoverageResource) slice).forConvertedValues(isConverted); + } catch (DataStoreException e) { + throw new CannotEvaluateException(Resources.format(Resources.Keys.CanNotReadSlice_1, index + startAt), e); + } + /* + * At this point, coverage of the "best" slice has been fetched from the cache or read from resource. + * Delegate the rendering to that coverage, after converting the extent from this grid coverage space + * to the slice coordinate space. If the coverage said that the converted extent does not intersect, + * try the "next best" slice until we succeed or until we exhausted the candidate list. + */ + return coverage.render(locator.toSliceExtent(extent, index)); + } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java index 6b7f14c5fc..850e08729c 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java @@ -282,7 +282,7 @@ public final class CoverageAggregator extends Group<GroupBySample> { */ public void add(final GridCoverageResource resource) throws DataStoreException { final GroupBySample bySample = GroupBySample.getOrAdd(members, resource.getSampleDimensions()); - final GridSlice slice = new GridSlice(resource); + final var slice = new GridSlice(resource); final List<GridSlice> slices; try { slices = slice.getList(bySample.members, strategy).members; @@ -336,7 +336,7 @@ public final class CoverageAggregator extends Group<GroupBySample> { final var names = new DimensionNameType[] { GridExtent.typeFromAxis(crs.getCoordinateSystem().getAxis(0)).orElse(null) }; - final GridExtent extent = new GridExtent(names, indices, indices, true); + final var extent = new GridExtent(names, indices, indices, true); final MathTransform gridToCRS = MathTransforms.linear(span, Math.fma(index, -span, lower)); add(resource, new GridGeometry(extent, PixelInCell.CELL_CORNER, gridToCRS, crs)); } @@ -362,12 +362,12 @@ public final class CoverageAggregator extends Group<GroupBySample> { * but a future version may use the state of this `CoverageAggregator`, for example making a better * effort to align the resources on the same "gridToCRS" transform. */ - final DefaultTemporalCRS crs = DefaultTemporalCRS.castOrCopy(CommonCRS.Temporal.TRUNCATED_JULIAN.crs()); + final var crs = DefaultTemporalCRS.castOrCopy(CommonCRS.Temporal.TRUNCATED_JULIAN.crs()); double scale = crs.toValue(span); double offset = crs.toValue(lower); long index = Numerics.roundAndClamp(offset / scale); // See comment in above method. offset = crs.toValue(lower.minus(span.multipliedBy(index))); - final GridExtent extent = new GridExtent(DimensionNameType.TIME, index, index, true); + final var extent = new GridExtent(DimensionNameType.TIME, index, index, true); final MathTransform gridToCRS = MathTransforms.linear(scale, offset); add(resource, new GridGeometry(extent, PixelInCell.CELL_CORNER, gridToCRS, crs)); } @@ -546,7 +546,8 @@ public final class CoverageAggregator extends Group<GroupBySample> { * Returns the algorithm to apply when more than one grid coverage can be found at the same grid index. * This is the most recent value set by a call to {@link #setMergeStrategy(MergeStrategy)}, * or {@code null} if no strategy has been specified. In the latter case, - * a {@link SubspaceNotSpecifiedException} will be thrown by {@link GridCoverage#render(GridExtent)} + * {@link SubspaceNotSpecifiedException} will be thrown in situations of ambiguity. + * An ambiguity happens at {@link GridCoverage#render(GridExtent)} invocation time * if more than one source coverage (slice) is found for a specified grid index. * * @return algorithm to apply for merging source coverages at the same grid index, or {@code null} if none. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionSelector.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionSelector.java index 7da41c60d9..2f45d076ca 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionSelector.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionSelector.java @@ -23,6 +23,7 @@ import org.apache.sis.util.privy.Strings; /** * A helper class for choosing the dimension on which to perform aggregation. + * An instance is created for each dimension of the grid geometry of a group. * * @author Martin Desruisseaux (Geomatys) */ @@ -39,6 +40,12 @@ final class DimensionSelector implements Comparable<DimensionSelector> { */ private final long[] positions; + /** + * The largest extent size found among all slices. + * Together with {@link #sumOfSize}, it provides a way to check is the size is constant. + */ + private long maxSize; + /** * Sum of grid extent size of each slice. * This is updated for each new slice added to this selector. @@ -46,45 +53,75 @@ final class DimensionSelector implements Comparable<DimensionSelector> { private BigInteger sumOfSize; /** - * Increment in unit of the extent size. This calculation is based on mean values only. - * It is computed after the {@link #positions} array has been completed with data from all slices. + * {@code true} if the increment between each slice is constant and equals to the extent size. + * In such case, the slices are actually tiles of constant size in a regular tile matrix. + * This is used for setting the value of {@link GroupByTransform#isMosaic}. + * + * <h4>Validity</h4> + * This value is valid only after {@link #finish()} has been invoked, which is itself invoked + * only after the {@link #positions} array has been completed with data from all slices. */ - private double relativeIncrement; + boolean isMosaic; /** - * Difference between minimal and maximal increment. - * This is computed after the {@link #positions} array has been completed with data from all slices. + * {@code true} if all {@link #positions} values are the same. + * For example, for a list of slices in the same geographic area but at different days <var>t</var>, + * this flag will typically be {@code true} for the horizontal dimensions and {@code false} for the + * temporal dimension. + * + * <h4>Validity</h4> + * This value is valid only after {@link #finish()} has been invoked, which is itself invoked + * only after the {@link #positions} array has been completed with data from all slices. */ - private long incrementRange; + boolean isConstantPosition; /** - * {@code true} if all {@link #positions} values are the same. - * This field is valid only after {@link #finish()} call. + * Average position increment in unit of the extent size. + * Small values mean that the position barely changes compared to the slice size. + * This is used for {@linkplain #compareTo choosing a preferred aggregation axis}. + * + * <h4>Validity</h4> + * This value is valid only after {@link #finish()} has been invoked, which is itself invoked + * only after the {@link #positions} array has been completed with data from all slices. */ - boolean isConstantPosition; + private double relativeIncrement; + + /** + * Difference between minimal and maximal increment. + * Small values suggest that the increment is more stable compared to large values. + * This is used for {@linkplain #compareTo choosing a preferred aggregation axis}. + * + * <h4>Validity</h4> + * This value is valid only after {@link #finish()} has been invoked, which is itself invoked + * only after the {@link #positions} array has been completed with data from all slices. + */ + private long incrementRange; /** * Prepares a new selector for a single dimension. * - * @param dim the dimension examined by this selector. - * @param n number of slices. + * @param dimension the dimension examined by this selector. + * @param sliceCount number of slices. */ - DimensionSelector(final int dim, final int n) { - dimension = dim; - positions = new long[n]; - sumOfSize = BigInteger.ZERO; + DimensionSelector(final int dimension, final int sliceCount) { + this.dimension = dimension; + this.positions = new long[sliceCount]; + this.sumOfSize = BigInteger.ZERO; } /** - * Sets the extent of a single slice. + * Sets the position and size of a single slice. The given position can be the low, mid or high grid coordinate, + * or anything else, as long as the choice is kept consistent across calls to this method on the same instance. + * The positions can be in any order, not necessarily increasing with the slice index. * - * @param i index of the slice. - * @param pos position of the slice. Could be low, mid or high index, as long as the choice is kept consistent. - * @param size size of the extent, in number of cells. + * @param sliceIndex index of the slice. + * @param position position of the slice from an arbitrary measurement process. + * @param extentSize size of the extent, in number of cells. */ - final void setSliceExtent(final int i, final long pos, final long size) { - positions[i] = pos; - sumOfSize = sumOfSize.add(BigInteger.valueOf(size)); + final void setSliceExtent(final int sliceIndex, final long position, final long extentSize) { + positions[sliceIndex] = position; + maxSize = Math.max(maxSize, extentSize); + sumOfSize = sumOfSize.add(BigInteger.valueOf(extentSize)); } /** @@ -107,14 +144,15 @@ final class DimensionSelector implements Comparable<DimensionSelector> { previous = p; } } - isConstantPosition = (maxInc == 0); + isMosaic = isConstantPosition = (maxInc == 0); if (minInc <= maxInc) { relativeIncrement = sumOfInc.doubleValue() / sumOfSize.doubleValue(); incrementRange = maxInc - minInc; // Cannot overflow because minInc >= 0. - /* - * TODO: we may have a mosaic if `incrementRange == 0 && maxInc == size`. - * Or maybe we could accept `maxInc <= minSize`. - */ + isMosaic = (incrementRange == 0) && (isConstantPosition || maxInc == maxSize); + } + if (isMosaic) { + // Verify that all tiles have the same size. + isMosaic = sumOfSize.equals(BigInteger.valueOf(maxSize).multiply(BigInteger.valueOf(positions.length))); } } @@ -142,6 +180,11 @@ final class DimensionSelector implements Comparable<DimensionSelector> { */ @Override public String toString() { - return Strings.toString(getClass(), "dimension", dimension, "relativeIncrement", relativeIncrement); + return Strings.toString(getClass(), + "dimension", dimension, + "isMosaic", isMosaic, + "isConstantPosition", isConstantPosition, + "relativeIncrement", relativeIncrement, + "incrementRange", incrementRange); } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSlice.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSlice.java index ed37ed429c..ecb640f714 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSlice.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSlice.java @@ -101,7 +101,7 @@ final class GridSlice { return c; } } - final GroupByTransform c = new GroupByTransform(geometry, gridToCRS, strategy); + final var c = new GroupByTransform(geometry, gridToCRS, strategy); transforms.add(c); return c; } @@ -121,10 +121,11 @@ final class GridSlice { * This is invoked by {@link GroupByTransform#findConcatenatedDimensions()} for choosing * a dimension to concatenate. */ - final void getGridExtent(final int i, final DimensionSelector[] writeTo) { + final void getGridExtent(final int sliceIndex, final DimensionSelector[] writeTo) { final GridExtent extent = getGridExtent(); for (int dim = writeTo.length; --dim >= 0;) { - writeTo[dim].setSliceExtent(i, Math.subtractExact(extent.getMedian(dim), offset[dim]), extent.getSize(dim)); + long position = Math.subtractExact(extent.getMedian(dim), offset[dim]); + writeTo[dim].setSliceExtent(sliceIndex, position, extent.getSize(dim)); } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSliceLocator.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSliceLocator.java index 9c0ede5387..10e224194a 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSliceLocator.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSliceLocator.java @@ -16,7 +16,6 @@ */ package org.apache.sis.storage.aggregate; -import java.util.Map; import java.util.List; import java.util.Arrays; import java.util.HashMap; @@ -86,7 +85,7 @@ final class GridSliceLocator { sliceLows = new long[resources.length]; sliceHighs = new long[resources.length]; offsets = new long[resources.length][]; - final Map<GridSlice,long[]> shared = new HashMap<>(); + final var shared = new HashMap<GridSlice,long[]>(); for (int i=0; i<resources.length; i++) { final GridSlice slice = slices.get(i); final GridExtent extent = slice.getGridExtent(); @@ -111,7 +110,7 @@ final class GridSliceLocator { final <E> GridGeometry union(final GridGeometry base, final List<E> slices, final Function<E,GridExtent> getter) { GridExtent extent = base.getExtent(); final int dimension = extent.getDimension(); - final DimensionNameType[] axes = new DimensionNameType[dimension]; + final var axes = new DimensionNameType[dimension]; final long[] low = new long[dimension]; final long[] high = new long[dimension]; for (int i=0; i<dimension; i++) { @@ -202,7 +201,7 @@ final class GridSliceLocator { } /** - * Return the name of the extent axis in the search dimension. + * Returns the name of the extent axis in the search dimension. * * @param extent the extent from which to get an axis label. * @return label for the search axis. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupAggregate.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupAggregate.java index d452d74ef6..0b07621285 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupAggregate.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupAggregate.java @@ -170,7 +170,7 @@ final class GroupAggregate extends AbstractResource implements Aggregate, Aggreg for (int i=0; i < copy.length; i++) { final Resource c = copy[i]; if (c instanceof AggregatedResource) { - final AggregatedResource component = (AggregatedResource) c; + final var component = (AggregatedResource) c; changed |= ((copy[i] = component.apply(strategy)) != component); } } @@ -291,7 +291,7 @@ final class GroupAggregate extends AbstractResource implements Aggregate, Aggreg static ImmutableEnvelope unionOfComponents(final Resource[] components) throws DataStoreException, TransformException { - final Envelope[] envelopes = new Envelope[components.length]; + final var envelopes = new Envelope[components.length]; for (int i=0; i < components.length; i++) { final Resource r = components[i]; if (r instanceof AbstractResource) { @@ -319,7 +319,7 @@ final class GroupAggregate extends AbstractResource implements Aggregate, Aggreg */ @Override protected Metadata createMetadata() throws DataStoreException { - final MetadataBuilder builder = new MetadataBuilder(); + final var builder = new MetadataBuilder(); builder.addTitle(name); builder.addExtent(envelope, listeners); if (sampleDimensions != null) { diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupBySample.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupBySample.java index f1d9256347..71cd2a0251 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupBySample.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupBySample.java @@ -54,7 +54,7 @@ final class GroupBySample extends Group<GroupByCRS<GroupByTransform>> { */ @Override final String createName(final Locale locale) { - final StringJoiner name = new StringJoiner(", "); + final var name = new StringJoiner(", "); for (final SampleDimension range : ranges) { name.add(range.getName().toInternationalString().toString(locale)); } @@ -77,7 +77,7 @@ final class GroupBySample extends Group<GroupByCRS<GroupByTransform>> { return c; } } - final GroupBySample c = new GroupBySample(ranges); + final var c = new GroupBySample(ranges); groups.add(c); return c; } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupByTransform.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupByTransform.java index abb9ee6a55..5c0d11ff42 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupByTransform.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GroupByTransform.java @@ -62,6 +62,14 @@ final class GroupByTransform extends Group<GridSlice> { */ MergeStrategy strategy; + /** + * Whether the members of this group are the tiles of a mosaic. + * This is {@code true} if all dimensions have tiles of the same size with an increment equal to that size. + * + * @see DimensionSelector#isMosaic + */ + private boolean isMosaic; + /** * Creates a new group of objects associated to the given transform. * @@ -73,6 +81,7 @@ final class GroupByTransform extends Group<GridSlice> { this.geometry = geometry; this.gridToCRS = gridToCRS; this.strategy = strategy; + this.isMosaic = true; } /** @@ -112,28 +121,36 @@ final class GroupByTransform extends Group<GridSlice> { /** * Returns grid dimensions to aggregate, in order of recommendation. * Aggregations should use the first dimension in the returned list. + * This method opportunistically updates {@link #isMosaic}. * * @todo A future version should add {@code findMosaicDimensions()}, which should be tested first. */ private int[] findConcatenatedDimensions() { final DimensionSelector[] selects; synchronized (members) { // Should no longer be needed at this step, but we are paranoiac. - int i = members.size(); + int sliceIndex = members.size(); selects = new DimensionSelector[geometry.getDimension()]; - for (int dim = selects.length; --dim >= 0;) { - selects[dim] = new DimensionSelector(dim, i); + for (int dimension = selects.length; --dimension >= 0;) { + selects[dimension] = new DimensionSelector(dimension, sliceIndex); } - while (--i >= 0) { - members.get(i).getGridExtent(i, selects); + while (--sliceIndex >= 0) { + members.get(sliceIndex).getGridExtent(sliceIndex, selects); } } + /* + * The above block collected information about all slices in this group. + * The following code computes the increment along each grid dimension, + * then finds which axis is the one on which the members are slices. + */ Arrays.stream(selects).parallel().forEach(DimensionSelector::finish); Arrays.sort(selects); // Contains usually less than 5 elements. - final int[] dimensions = new int[selects.length]; + final var dimensions = new int[selects.length]; int count = 0; - for (int i=selects.length; --i >= 0;) { - if (selects[i].isConstantPosition) break; - dimensions[count++] = selects[i].dimension; + for (int dimension = selects.length; --dimension >= 0;) { + final DimensionSelector select = selects[dimension]; + if (select.isConstantPosition) break; + dimensions[count++] = select.dimension; + isMosaic &= select.isMosaic; } return ArraysExt.resize(dimensions, count); } @@ -147,19 +164,32 @@ final class GroupByTransform extends Group<GridSlice> { * @return the concatenated resource. */ final Resource createResource(final StoreListeners parentListeners, final List<SampleDimension> ranges) { - final int n = members.size(); - if (n == 1) { + final int count = members.size(); + if (count == 1) { return members.get(0).resource; } - final var slices = new GridCoverageResource[n]; + final var slices = new GridCoverageResource[count]; final String name = getName(parentListeners); final int[] dimensions = findConcatenatedDimensions(); - if (dimensions.length == 0) { - for (int i=0; i<n; i++) slices[i] = members.get(i).resource; + if (isMosaic) { + if (strategy == null) { + /* + * We can safely default to the "overlay" merge strategy. + * There is no ambiguity, because no tile should overlap. + * The "overlay" operation should be able to share tile + * references without copying pixel values in the common + * case where all tiles use the same `SampleModel`. + */ + strategy = MergeStrategy.opaqueOverlay(null); + } + } else if (dimensions.length == 0) { + // Unable to group the slices in a multi-dimensional cube. + for (int i=0; i<count; i++) slices[i] = members.get(i).resource; return new GroupAggregate(parentListeners, name, slices, ranges); } - final GridSliceLocator locator = new GridSliceLocator(members, dimensions[0], slices); - final GridGeometry domain = locator.union(geometry, members, GridSlice::getGridExtent); + // The following constructor fills itself the `slices` array content. + final var locator = new GridSliceLocator(members, dimensions[0], slices); + final GridGeometry domain = locator.union(geometry, members, GridSlice::getGridExtent); return new ConcatenatedGridResource(name, parentListeners, domain, ranges, slices, locator, strategy); } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/MergeStrategy.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/MergeStrategy.java index 1b4fa71d94..4c57d9d0f9 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/MergeStrategy.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/MergeStrategy.java @@ -16,6 +16,8 @@ */ package org.apache.sis.storage.aggregate; +import java.awt.Rectangle; +import java.awt.image.RenderedImage; import java.time.Instant; import java.time.Duration; import org.apache.sis.storage.Resource; @@ -24,6 +26,8 @@ import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.referencing.privy.ExtentSelector; +import org.apache.sis.image.ImageProcessor; +import org.apache.sis.util.ArraysExt; import org.apache.sis.util.privy.Strings; @@ -31,47 +35,87 @@ import org.apache.sis.util.privy.Strings; * Algorithm to apply when more than one grid coverage can be found at the same grid index. * A merge may happen if an aggregated coverage is created with {@link CoverageAggregator}, * and the extent of some source coverages are overlapping in the dimension to aggregate. + * {@code MergeStrategy} is ignored if only one coverage is contained in a requested extent. * * <h2>Example</h2> * A collection of {@link GridCoverage} instances may represent the same phenomenon - * (for example Sea Surface Temperature) over the same geographic area but at different dates and times. - * {@link CoverageAggregator} can be used for building a single data cube with a time axis. + * (for example, air temperature) over the same geographic area but at different days. + * In such case, {@link CoverageAggregator} can build a three-dimensional data cube + * where each source coverage is located at a different position on the time axis. * But if two coverages have overlapping time ranges, and if a user request data in the overlapping region, - * then the aggregated coverages have more than one source coverages capable to provide the requested data. - * This enumeration specify how to handle this multiplicity. + * then there is an ambiguity about which data to return. + * This {@code MergeStrategy} specifies how to handle this multiplicity. * * <h2>Default behavior</h2> * If no merge strategy is specified, then the default behavior is to throw - * {@link SubspaceNotSpecifiedException} when the {@link GridCoverage#render(GridExtent)} method - * is invoked and more than one source coverage (slice) is found for a specified grid index. + * {@link SubspaceNotSpecifiedException} in situations of ambiguity. + * An ambiguity happens at {@link GridCoverage#render(GridExtent)} invocation time + * if more than one source coverage (slice) is found for a specified grid index. * * @author Martin Desruisseaux (Geomatys) - * @version 1.3 - * @since 1.3 + * @version 1.5 + * + * @see CoverageAggregator#setMergeStrategy(MergeStrategy) + * + * @since 1.3 */ -public final class MergeStrategy { +public abstract class MergeStrategy { /** - * Selects a single slice using criteria based first on temporal extent, then on geographic area. - * This default instance do not use any duration. + * Creates a new merge strategy. * - * @see #selectByTimeThenArea(Duration) + * @since 1.5 */ - private static final MergeStrategy SELECT_BY_TIME = new MergeStrategy(null); + protected MergeStrategy() { + } /** - * Temporal granularity of the time of interest, or {@code null} if none. - * If non-null, intersections with TOI will be rounded to an integer number of this granularity. - * This is useful if data are expected at an approximately regular interval - * and we want to ignore slight variations in the temporal extent declared for each image. + * Builds an {@linkplain org.apache.sis.image.ImageProcessor#overlay image overlay} of all sources. + * The source images added first have precedence (foreground). Images added last are in background. + * All bands are referenced or copied verbatim, without special treatment for the alpha channel. + * In other words, this merge strategy does not handle transparency in overlapping regions. + * + * @param areaOfInterest range of pixel coordinates, or {@code null} for the union of all images. + * @return a merge strategy for building an overlay of all source images. + * + * @since 1.5 */ - private final Duration timeGranularity; + public static MergeStrategy opaqueOverlay(final Rectangle areaOfInterest) { + Overlay strategy = Overlay.DEFAULT; + if (areaOfInterest != null) { + strategy = new Overlay(strategy.processor, new Rectangle(areaOfInterest)); + } + return strategy; + } /** - * Creates a new merge strategy. This constructor is private for now because - * we have not yet decided a callback API for custom merges. + * The implementation returned by {@link #opaqueOverlay(Rectangle)}. */ - private MergeStrategy(final Duration timeGranularity) { - this.timeGranularity = timeGranularity; + private static final class Overlay extends MergeStrategy { + /** The default instance with no particular area of interest specified. */ + static final Overlay DEFAULT = new Overlay(new ImageProcessor(), null); + + /** The image processor with the configuration to use. */ + final ImageProcessor processor; + + /** The area of interest, or {@code null} if none. */ + private final Rectangle areaOfInterest; + + /** Creates a new strategy for an image in the given area. */ + Overlay(final ImageProcessor processor, final Rectangle areaOfInterest) { + this.processor = processor; + this.areaOfInterest = areaOfInterest; + } + + /** Aggregates the given sources. */ + @Override protected RenderedImage aggregate(RenderedImage[] sources) { + return processor.overlay(sources, areaOfInterest); + } + + /** Returns a string representation of this strategy for debugging purposes. */ + @Override public String toString() { + return Strings.toString(MergeStrategy.class, null, + "opaqueOverlay", "areaOfInterest", areaOfInterest); + } } /** @@ -118,49 +162,93 @@ public final class MergeStrategy { * Current implementation does not check the vertical dimension. * This check may be added in a future version. * - * @param timeGranularity the temporal granularity of the Time of Interest (TOI), or {@code null} if none. + * @param timeGranularity the temporal granularity of the Time of Interest (<abbr>TOI</abbr>), or {@code null} if none. * @return a merge strategy for selecting a slice based on temporal criteria first. */ public static MergeStrategy selectByTimeThenArea(final Duration timeGranularity) { - return (timeGranularity != null) ? new MergeStrategy(timeGranularity) : SELECT_BY_TIME; + return (timeGranularity != null) ? new FilterByTime(timeGranularity) : FilterByTime.DEFAULT; } /** - * Applies the merge using the strategy represented by this instance. - * Current implementation does only a slice selection. - * A future version may allow real merge operations. - * - * @param request the geographic area and temporal extent requested by user. - * @param candidates grid geometry of all slices that intersect the request. Null elements are ignored. - * @return index of best slice according the heuristic rules of this {@code MergeStrategy}. + * The implementation returned by {@link #selectByTimeThenArea(Duration)}. */ - final Integer apply(final GridGeometry request, final GridGeometry[] candidates) { - final ExtentSelector<Integer> selector = new ExtentSelector<>( - request.getGeographicExtent().orElse(null), - request.getTemporalExtent()); - - if (timeGranularity != null) { - selector.setTimeGranularity(timeGranularity); - selector.alternateOrdering = true; + private static final class FilterByTime extends MergeStrategy { + /** + * The default instance with no time granularity. + * Temporal positions are compared at their full precision. + */ + static final FilterByTime DEFAULT = new FilterByTime(null); + + /** + * Temporal granularity of the time of interest, or {@code null} if none. + * If non-null, intersections with TOI will be rounded to an integer number of this granularity. + * This is useful if data are expected at an approximately regular interval + * and we want to ignore slight variations in the temporal extent declared for each image. + */ + private final Duration timeGranularity; + + /** + * Creates a new strategy for the given time granularity. + */ + FilterByTime(final Duration timeGranularity) { + this.timeGranularity = timeGranularity; } - for (int i=0; i < candidates.length; i++) { - final GridGeometry candidate = candidates[i]; - if (candidate != null) { - final Instant[] t = candidate.getTemporalExtent(); - final int n = t.length; - selector.evaluate(candidate.getGeographicExtent().orElse(null), - (n == 0) ? null : t[0], - (n == 0) ? null : t[n-1], i); + + /** + * Selects a single coverage using the strategy represented by this instance. + * May return an empty array if there is no source that can be used. + * + * @param request the geographic area and temporal extent requested by user. + * @param candidates grid geometry of all slices that intersect the request. Null elements are ignored. + * @return index of best slice according the heuristic rules of this {@code MergeStrategy}, or empty. + */ + @Override + protected int[] filter(final GridGeometry request, final GridGeometry[] candidates) { + final var selector = new ExtentSelector<Integer>( + request.getGeographicExtent().orElse(null), + request.getTemporalExtent()); + + if (timeGranularity != null) { + selector.setTimeGranularity(timeGranularity); + selector.alternateOrdering = true; + } + for (int i=0; i < candidates.length; i++) { + final GridGeometry candidate = candidates[i]; + if (candidate != null) { + final Instant[] t = candidate.getTemporalExtent(); + final int n = t.length; + selector.evaluate(candidate.getGeographicExtent().orElse(null), + (n == 0) ? null : t[0], + (n == 0) ? null : t[n-1], i); + } } + final Integer best = selector.best(); + return (best != null) ? new int[] {best} : ArraysExt.EMPTY_INT; + } + + /** + * Returns the single image selected by the filter. + * The array length should always be exactly one. + */ + @Override + protected RenderedImage aggregate(RenderedImage[] sources) { + return sources[0]; + } + + /** + * Returns a string representation of this strategy for debugging purposes. + */ + @Override + public String toString() { + return Strings.toString(MergeStrategy.class, null, + "selectByTimeThenArea", "timeGranularity", timeGranularity); } - return selector.best(); } /** - * Returns a resource with same data as specified resource but using this merge strategy. + * Returns a resource with the same data as the specified resource, but using this merge strategy. * If the given resource is an instance created by {@link CoverageAggregator} and uses a different strategy, - * then a new resource using this merge strategy is returned. Otherwise the given resource is returned as-is. - * The returned resource will share the same resources and caches than the given resource. + * then a new resource using this merge strategy is returned. Otherwise, the given resource is returned as-is. * * @param resource the resource for which to update the merge strategy, or {@code null}. * @return resource with updated merge strategy, or {@code null} if the given resource was null. @@ -173,12 +261,34 @@ public final class MergeStrategy { } /** - * Returns a string representation of this strategy for debugging purposes. + * Returns the indexes of the coverages to use in the aggregation. + * The {@code candidates} array contains the grid geometries of all coverages that intersect the request. + * This method can decide to accept none of those candidates (by returning an empty array), or to select + * exactly one (for example, based on {@linkplain #selectByTimeThenArea a temporal criterion}), + * or on the contrary to select all of them, or any intermediate choice. * - * @return string representation of this strategy. + * <p>The default implementation selects all candidates (i.e., filter nothing).</p> + * + * @param request the geographic area and temporal extent requested by user. + * @param candidates grid geometry of all slices that intersect the request. + * @return indexes of the slices to use according the heuristic rules of this {@code MergeStrategy}. + * + * @since 1.5 */ - @Override - public String toString() { - return Strings.toString(getClass(), "algo", "selectByTimeThenArea", "timeGranularity", timeGranularity); + protected int[] filter(GridGeometry request, GridGeometry[] candidates) { + return ArraysExt.range(0, candidates.length); } + + /** + * Aggregates images that have been accepted by the filter. The length of the {@code sources} array + * is equal or smaller than the length of the index array returned by {@link #filter filter(…)}. + * The array may be shorter if some images were outside the request, but the array always contains + * at least one element. + * + * @param sources the images accepted by the filter. + * @return the result of the aggregation. + * + * @since 1.5 + */ + protected abstract RenderedImage aggregate(RenderedImage[] sources); } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java index 0209faa010..36f32a4f02 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java @@ -328,8 +328,8 @@ public class Resources extends IndexedResourceBundle { public static final short NoCommonFeatureType = 75; /** - * Index {1} in dimension “{0}” maps to {2} slices. This error can be avoided by specifying a - * merge strategy. + * Cell coordinate {1} in dimension “{0}” maps to {2} slices or tiles. A smaller extent or a + * merge strategy should be specified. */ public static final short NoSliceMapped_3 = 79; diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties index 7e876bb0e4..79c2562fb5 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties @@ -73,7 +73,7 @@ MetadataLocation = Relative path to metadata. MissingResourceIdentifier_1 = Resource \u201c{0}\u201d does not have an identifier. MissingSchemeInURI_1 = Missing scheme in \u201c{0}\u201d URI. NoCommonFeatureType = No feature type is common to all the features to aggregate. -NoSliceMapped_3 = Index {1} in dimension \u201c{0}\u201d maps to {2} slices. This error can be avoided by specifying a merge strategy. +NoSliceMapped_3 = Cell coordinate {1} in dimension \u201c{0}\u201d maps to {2} slices or tiles. A smaller extent or a merge strategy should be specified. NoSliceSpecified_2 = Extent in dimension \u201c{0}\u201d should be a slice, but {1} cells were specified. NoSuchResourceDirectory_1 = No directory of resources found at \u201c{0}\u201d. NoSuchResourceInAggregate_2 = Resource \u201c{1}\u201d is not part of aggregate \u201c{0}\u201d. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties index b628d0fc9d..3088bb43ac 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties @@ -78,7 +78,7 @@ MetadataLocation = Chemin relatif des m\u00e9ta-donn\u00e9es. MissingResourceIdentifier_1 = La ressource \u00ab\u202f{0}\u202f\u00bb n\u2019a pas d\u2019identifiant. MissingSchemeInURI_1 = Il manque le sch\u00e9ma dans l\u2019URI \u00ab\u202f{0}\u202f\u00bb. NoCommonFeatureType = Il n\u2019y a pas de type commun \u00e0 toutes les entit\u00e9s \u00e0 agr\u00e9ger. -NoSliceMapped_3 = L\u2019index {1} dans la dimension \u00ab\u202f{0}\u202f\u00bb correspond \u00e0 {2} tranches. Cette erreur peut \u00eatre \u00e9vit\u00e9e en sp\u00e9cifiant une strat\u00e9gie de fusion. +NoSliceMapped_3 = La coordonn\u00e9e de cellule {1} dans la dimension \u00ab\u202f{0}\u202f\u00bb correspond \u00e0 {2} tranches ou tuiles. Une \u00e9tendue plus petite, ou une strat\u00e9gie de fusion, devrait \u00eatre sp\u00e9cifi\u00e9e. NoSliceSpecified_2 = La plage dans la dimension \u00ab\u202f{0}\u202f\u00bb devrait \u00eatre une tranche, mais {1} cellules ont \u00e9t\u00e9 sp\u00e9cifi\u00e9es. NoSuchResourceDirectory_1 = Aucun r\u00e9pertoire de ressources n\u2019a \u00e9t\u00e9 trouv\u00e9 \u00e0 l\u2019emplacement \u00ab\u202f{0}\u202f\u00bb. NoSuchResourceInAggregate_2 = La ressource \u00ab\u202f{1}\u202f\u00bb n\u2019est pas une partie de l\u2019agr\u00e9gat \u00ab\u202f{0}\u202f\u00bb.