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);
+    }
 }

Reply via email to