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 <[email protected]>
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.