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
commit dbc948d0673f23f25f8f38f49055a2d0d3a45580 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Sep 15 15:18:23 2022 +0200 Change `MergeStrategy` to an ordinary class for allowing custom strategies. Add support for a "select by time, then by geographic area" strategy. --- .../sis/internal/referencing/ExtentSelector.java | 18 +++- .../org/apache/sis/internal/storage/Resources.java | 5 + .../sis/internal/storage/Resources.properties | 1 + .../sis/internal/storage/Resources_fr.properties | 1 + .../aggregate/ConcatenatedGridCoverage.java | 57 +++++++---- .../aggregate/ConcatenatedGridResource.java | 1 + .../storage/aggregate/CoverageAggregator.java | 13 +-- .../internal/storage/aggregate/MergeStrategy.java | 113 ++++++++++++++++++++- 8 files changed, 178 insertions(+), 31 deletions(-) diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java index 2628e51198..19191f1ded 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java @@ -78,7 +78,7 @@ import org.apache.sis.util.resources.Errors; * } * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * * @param <T> the type of object to be selected. * @@ -177,6 +177,22 @@ public final class ExtentSelector<T> { */ private double temporalDistance; + /** + * Creates a selector for the given area and time of interest. + * + * @param aoi the area of interest, or {@code null} if unbounded. + * @param toi the time of interest, or {@code null} or empty if unbounded. + * The first element is start time and the last element is end time. + */ + public ExtentSelector(final GeographicBoundingBox aoi, final Instant[] toi) { + areaOfInterest = aoi; + final int n; + if (toi != null && (n = toi.length) != 0) { + minTOI = toi[0]; + maxTOI = toi[n-1]; + } + } + /** * Creates a selector for the given area of interest. * diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java index 8c88cedc8b..5a03bb4cf4 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java @@ -139,6 +139,11 @@ public final class Resources extends IndexedResourceBundle { */ public static final short CanNotRenderImage_1 = 61; + /** + * Can not select a slice. + */ + public static final short CanNotSelectSlice = 81; + /** * Can not save resources of type ‘{1}’ in a “{0}” store. */ diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties index 8e97fbf6c7..50b02c5467 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties @@ -35,6 +35,7 @@ CanNotReadPixel_3 = Can not read pixel at ({0}, {1}) indices in CanNotReadSlice_1 = Can not read slice at index {0}. CanNotRemoveResource_2 = Can not remove resource \u201c{1}\u201d from aggregate \u201c{0}\u201d. CanNotRenderImage_1 = Can not render an image for the \u201c{0}\u201d coverage. +CanNotSelectSlice = Can not select a slice. CanNotStoreResourceType_2 = Can not save resources of type \u2018{1}\u2019 in a \u201c{0}\u201d store. CanNotWriteResource_1 = Can not write the \u201c{0}\u201d resource. ClosedStorageConnector = This storage connector is closed. diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties index ca2c3cc76a..9cb9b60745 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties @@ -40,6 +40,7 @@ CanNotReadPixel_3 = Ne peut pas lire le pixel aux indices ({0}, CanNotReadSlice_1 = Ne peut pas lire la tranche \u00e0 l\u2019index {0}. CanNotRemoveResource_2 = Ne peut pas supprimer la ressource \u00ab\u202f{1}\u202f\u00bb de l\u2019agr\u00e9gat \u00ab\u202f{0}\u202f\u00bb. CanNotRenderImage_1 = Ne peut pas produire une image pour la couverture de donn\u00e9es \u00ab\u202f{0}\u202f\u00bb. +CanNotSelectSlice = Ne peut pas s\u00e9lectionner une tranche de donn\u00e9es. CanNotWriteResource_1 = Ne peut pas \u00e9crire la ressource \u00ab\u202f{0}\u202f\u00bb. CanNotStoreResourceType_2 = Ne peut pas enregistrer des ressources de type \u2018{1}\u2019 dans un entrep\u00f4t de donn\u00e9es \u00ab\u202f{0}\u202f\u00bb. ClosedStorageConnector = Ce connecteur est ferm\u00e9. diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/ConcatenatedGridCoverage.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/ConcatenatedGridCoverage.java index c47d674021..7e13c5128a 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/ConcatenatedGridCoverage.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/ConcatenatedGridCoverage.java @@ -24,12 +24,14 @@ import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.coverage.SubspaceNotSpecifiedException; +import org.apache.sis.coverage.grid.DisjointExtentException; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.storage.DataStoreException; import org.apache.sis.internal.storage.Resources; // Branch-dependent imports import org.opengis.coverage.CannotEvaluateException; +import org.opengis.referencing.operation.TransformException; /** @@ -87,6 +89,7 @@ final class ConcatenatedGridCoverage extends GridCoverage { /** * Algorithm to apply when more than one grid coverage can be found at the same grid index. + * This is {@code null} if no merge should be attempted. */ private final MergeStrategy strategy; @@ -185,26 +188,42 @@ final class ConcatenatedGridCoverage extends GridCoverage { } else { extent = gridGeometry.getExtent(); } - final int size = upper - lower; - if (size != 1) { - switch (strategy) { - default: { - /* - * Can not 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, size}; - } else { - message = Resources.Keys.NoSliceSpecified_2; - arguments = new Object[] {locator.getDimensionName(extent), size}; - } - throw new SubspaceNotSpecifiedException(Resources.format(message, arguments)); + final int count = upper - lower; + if (count != 1) { + if (count == 0) { + throw new DisjointExtentException(); + } + if (strategy == null) { + /* + * Can not 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)); + } + /* + * Select a slice using the user-specified merge strategy. + * Current implementation does only a selection; a future version may allow real merges. + */ + final GridGeometry[] geometries = new GridGeometry[count]; + try { + for (int i=0; i<count; i++) { + final int j = lower + i; + final GridCoverage slice = slices[j]; + geometries[i] = (slice != null) ? slice.getGridGeometry() : resources[j].getGridGeometry(); } + lower += strategy.apply(new GridGeometry(getGridGeometry(), extent, null), geometries); + } catch (DataStoreException | TransformException e) { + throw new CannotEvaluateException(Resources.format(Resources.Keys.CanNotSelectSlice), e); } } /* diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/ConcatenatedGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/ConcatenatedGridResource.java index 0f0c6c8f4f..5254220c5e 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/ConcatenatedGridResource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/ConcatenatedGridResource.java @@ -110,6 +110,7 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem /** * Algorithm to apply when more than one grid coverage can be found at the same grid index. + * This is {@code null} if no merge should be attempted. */ final MergeStrategy strategy; diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/CoverageAggregator.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/CoverageAggregator.java index 100c410a57..43e08dafa4 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/CoverageAggregator.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/CoverageAggregator.java @@ -37,7 +37,6 @@ import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.util.collection.BackingStoreException; -import org.apache.sis.util.ArgumentChecks; /** @@ -108,6 +107,7 @@ public final class CoverageAggregator extends Group<GroupBySample> { /** * Algorithm to apply when more than one grid coverage can be found at the same grid index. + * This is {@code null} by default. * * @see #getMergeStrategy() */ @@ -122,7 +122,6 @@ public final class CoverageAggregator extends Group<GroupBySample> { public CoverageAggregator(final StoreListeners listeners) { this.listeners = listeners; aggregates = new HashMap<>(); - strategy = MergeStrategy.FAIL; } /** @@ -241,9 +240,11 @@ 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 {@link MergeStrategy#FAIL} by default. + * or {@code null} if no strategy has been specified. In the latter case, + * a {@link SubspaceNotSpecifiedException} will be thrown by {@link GridCoverage#render(GridExtent)} + * 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. + * @return algorithm to apply for merging source coverages at the same grid index, or {@code null} if none. */ public MergeStrategy getMergeStrategy() { return strategy; @@ -263,10 +264,10 @@ public final class CoverageAggregator extends Group<GroupBySample> { * Said otherwise, the merge strategy of a data cube is the strategy which was active * at the time of the most recently added slice. * - * @param strategy new algorithm to apply for merging source coverages at the same grid index. + * @param strategy new algorithm to apply for merging source coverages at the same grid index, + * or {@code null} if none. */ public void setMergeStrategy(final MergeStrategy strategy) { - ArgumentChecks.ensureNonNull("strategy", strategy); this.strategy = strategy; } diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/MergeStrategy.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/MergeStrategy.java index 4578097af5..66aa3303f6 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/MergeStrategy.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/MergeStrategy.java @@ -16,9 +16,14 @@ */ package org.apache.sis.internal.storage.aggregate; +import java.time.Instant; +import java.time.Duration; 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.coverage.SubspaceNotSpecifiedException; +import org.apache.sis.internal.referencing.ExtentSelector; +import org.apache.sis.internal.util.Strings; /** @@ -34,17 +39,115 @@ import org.apache.sis.coverage.SubspaceNotSpecifiedException; * then the aggregated coverages have more than one source coverages capable to provide the requested data. * This enumeration specify how to handle this multiplicity.</div> * + * 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. + * * @author Martin Desruisseaux (Geomatys) * @version 1.3 * @since 1.3 * @module */ -public enum MergeStrategy { +public final class 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 amount 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 merge strategy. This constructor is private for now because + * we have not yet decided a callback API for custom merges. + */ + private MergeStrategy(final Duration timeGranularity) { + this.timeGranularity = timeGranularity; + } + + /** + * Selects a single slice using criteria based first on temporal extent, then on geographic area. + * This strategy applies the following rules, in order: + * + * <ol> + * <li>Slice having largest intersection with the time of interest (TOI) is selected.</li> + * <li>If two or more slices have the same intersection with TOI, + * then the one with less "overtime" (time outside TOI) is selected.</li> + * <li>If two or more slices are considered equal after above criteria, + * then the one best centered on the TOI is selected.</li> + * </ol> + * + * <div class="note"><b>Rational:</b> + * the "smallest time outside" criterion (rule 2) is before "best centered" criterion (rule 3) + * because of the following scenario: if a user specifies a "time of interest" (TOI) of 1 day + * and if there is two slices intersecting the TOI, with one slice being a raster of monthly + * averages the other slice being a raster of daily data, we want the daily data to be selected + * even if by coincidence the monthly averages is better centered.</div> + * + * If the {@code timeGranularity} argument is non-null, then intersections with TOI will be rounded + * to an integer amount of the specified granularity and the last criterion in above list is relaxed. + * This is useful when data are expected at an approximately regular time interval (for example one remote + * sensing image per day) and we want to ignore slight variations in the temporal extent declared for each image. + * + * <p>If there is no time of interest, or the slices do not declare time range, + * or some slices are still at equality after application of above criteria, + * then the selection continues on the basis of geographic criteria:</p> + * + * <ol> + * <li>Largest intersection with the area of interest (AOI) is selected.</li> + * <li>If two or more slices have the same intersection area with AOI, then the one with the less + * "irrelevant" material is selected. "Irrelevant" material are area outside the AOI.</li> + * <li>If two or more slices are considered equal after above criteria, + * the one best centered on the AOI is selected.</li> + * <li>If two or more slices are considered equal after above criteria, + * then the first of those candidates is selected.</li> + * </ol> + * + * If two slices are still considered equal after all above criteria, then an arbitrary one is selected. + * + * @param timeGranularity the temporal granularity of the Time of Interest (TOI), 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 new MergeStrategy(timeGranularity); + } + + /** + * 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. + * @return index of best slice according the heuristic rules of this {@code MergeStrategy}. + */ + 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; + } + for (int i=0; i < candidates.length; i++) { + final GridGeometry candidate = candidates[i]; + 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); + } + return selector.best(); + } + /** - * Do not perform any merge. It will cause a {@link SubspaceNotSpecifiedException} to be thrown by - * {@link GridCoverage#render(GridExtent)} if more than one source coverage is found for a specified slice. + * Returns a string representation of this strategy for debugging purposes. * - * <p>This is the default strategy.</p> + * @return string representation of this strategy. */ - FAIL + @Override + public String toString() { + return Strings.toString(getClass(), "algo", "selectByTimeThenArea", "timeGranularity", timeGranularity); + } }