You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/09/14 17:25:32 UTC

[sis] branch geoapi-4.0 updated: Provide a `MergeStrategy` option and provide more information in the error message if `ConcConcatenatedGridCoverage.render(GridExtent)` can not infer a slice.

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 c69c10ddfc Provide a `MergeStrategy` option and provide more information in the error message if `ConcConcatenatedGridCoverage.render(GridExtent)` can not infer a slice.
c69c10ddfc is described below

commit c69c10ddfc32d25a54ce330edf0070f716cbdf67
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Sep 14 19:10:50 2022 +0200

    Provide a `MergeStrategy` option and provide more information in the error message if `ConcConcatenatedGridCoverage.render(GridExtent)` can not infer a slice.
---
 .../org/apache/sis/internal/storage/Resources.java | 10 ++++
 .../sis/internal/storage/Resources.properties      |  2 +
 .../sis/internal/storage/Resources_fr.properties   |  2 +
 .../aggregate/ConcatenatedGridCoverage.java        | 34 ++++++++++++-
 .../aggregate/ConcatenatedGridResource.java        |  9 +++-
 .../storage/aggregate/CoverageAggregator.java      | 59 +++++++++++++++++++++-
 .../sis/internal/storage/aggregate/GridSlice.java  |  8 +--
 .../storage/aggregate/GridSliceLocator.java        | 24 +++++++++
 .../storage/aggregate/GroupByTransform.java        | 14 ++++-
 .../internal/storage/aggregate/MergeStrategy.java  | 50 ++++++++++++++++++
 10 files changed, 203 insertions(+), 9 deletions(-)

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 5934ebb244..448986964e 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
@@ -326,6 +326,16 @@ public final class Resources extends IndexedResourceBundle {
          */
         public static final short NoCommonFeatureType = 75;
 
+        /**
+         * Index {1} in dimension “{0}” maps to {2} slices.
+         */
+        public static final short NoSliceMapped_3 = 79;
+
+        /**
+         * Extent in dimension “{0}” should be a slice, but {1} cells were specified.
+         */
+        public static final short NoSliceSpecified_2 = 80;
+
         /**
          * No directory of resources found at “{0}”.
          */
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 c561b2bb2f..8e97fbf6c7 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
@@ -72,6 +72,8 @@ MarkNotSupported_1                = Marks are not supported on \u201c{0}\u201d s
 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.
+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.
 NotAWritableFeatureSet_1          = Resource \u201c{0}\u201d is not a writable feature set.
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 6c8d3708e5..ca2c3cc76a 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
@@ -77,6 +77,8 @@ MarkNotSupported_1                = Les marques ne sont pas support\u00e9es sur
 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.
+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.
 NotAWritableFeatureSet_1          = La ressource \u00ab\u202f{0}\u202f\u00bb n\u2019est pas un ensemble d\u2019entit\u00e9s accessibles en \u00e9criture.
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 1a5a8d280f..c47d674021 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
@@ -85,6 +85,11 @@ final class ConcatenatedGridCoverage extends GridCoverage {
      */
     private final int startAt;
 
+    /**
+     * Algorithm to apply when more than one grid coverage can be found at the same grid index.
+     */
+    private final MergeStrategy strategy;
+
     /**
      * Creates a new aggregated coverage.
      *
@@ -108,6 +113,7 @@ final class ConcatenatedGridCoverage extends GridCoverage {
         this.ranges      = ranges;
         this.isConverted = source.isConverted;
         this.locator     = source.locator;
+        this.strategy    = source.strategy;
     }
 
     /**
@@ -124,6 +130,7 @@ final class ConcatenatedGridCoverage extends GridCoverage {
         this.request     = source.request;
         this.ranges      = source.ranges;
         this.locator     = source.locator;
+        this.strategy    = source.strategy;
         this.isConverted = converted;
     }
 
@@ -178,9 +185,32 @@ final class ConcatenatedGridCoverage extends GridCoverage {
         } else {
             extent = gridGeometry.getExtent();
         }
-        if (upper - lower != 1) {
-            throw new SubspaceNotSpecifiedException();
+        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));
+                }
+            }
         }
+        /*
+         * Argument have been validated and slice has been located.
+         * If the coverage has not already been loaded, load it now.
+         */
         GridCoverage slice = slices[lower];
         if (slice == null) try {
             slice = resources[lower].read(request, ranges).forConvertedValues(isConverted);
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 0b5f48fa67..0f0c6c8f4f 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
@@ -108,6 +108,11 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem
      */
     final GridSliceLocator locator;
 
+    /**
+     * Algorithm to apply when more than one grid coverage can be found at the same grid index.
+     */
+    final MergeStrategy strategy;
+
     /**
      * The envelope of this aggregate, or {@code null} if not yet computed.
      * May also be {@code null} if no slice declare an envelope, or if the union can not be computed.
@@ -143,7 +148,8 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem
                              final GridGeometry           domain,
                              final List<SampleDimension>  ranges,
                              final GridCoverageResource[] slices,
-                             final GridSliceLocator       locator)
+                             final GridSliceLocator       locator,
+                             final MergeStrategy          strategy)
     {
         super(listeners, false);
         this.name             = name;
@@ -151,6 +157,7 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem
         this.sampleDimensions = ranges;
         this.slices           = slices;
         this.locator          = locator;
+        this.strategy         = strategy;
         this.deferredLoading  = new long[Numerics.ceilDiv(slices.length, Long.SIZE)];
         for (final SampleDimension sd : ranges) {
             if (sd.forConvertedValues(true) != sd) {
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 b16486ddbe..91d4292bbc 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
@@ -35,12 +35,31 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 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;
 
 
 /**
  * Creates a grid coverage resource from an aggregation of an arbitrary amount of other resources.
  *
+ * <div class="note"><b>Example:</b>
+ * 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.</div>
+ *
+ * All source coverages should share the same CRS and have the same ranges (sample dimensions).
+ * If this is not the case, then the source coverages will be grouped in different aggregates
+ * with an uniform CRS and set of ranges in each sub-aggregates.
+ *
+ * <h2>Multi-threading and concurrency</h2>
+ * All {@code add(…)} methods can be invoked concurrently from arbitrary threads.
+ * It is okay to load {@link GridCoverageResource} instances in parallel threads
+ * and add those resources to {@code CoverageAggregator} without synchronization.
+ * However the final {@link #build()} method is <em>not</em> thread-safe;
+ * that method shall be invoked from a single thread after all sources have been added
+ * and no more addition are in progress.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.3
  * @since   1.3
@@ -58,6 +77,13 @@ public final class CoverageAggregator extends Group<GroupBySample> {
      */
     private final Map<Set<Resource>, Queue<Aggregate>> aggregates;
 
+    /**
+     * Algorithm to apply when more than one grid coverage can be found at the same grid index.
+     *
+     * @see #getMergeStrategy()
+     */
+    private volatile MergeStrategy strategy;
+
     /**
      * Creates an initially empty aggregator.
      *
@@ -67,6 +93,7 @@ public final class CoverageAggregator extends Group<GroupBySample> {
     public CoverageAggregator(final StoreListeners listeners) {
         this.listeners = listeners;
         aggregates = new HashMap<>();
+        strategy = MergeStrategy.FAIL;
     }
 
     /**
@@ -114,7 +141,7 @@ public final class CoverageAggregator extends Group<GroupBySample> {
         final GridSlice slice = new GridSlice(resource);
         final List<GridSlice> slices;
         try {
-            slices = slice.getList(bySample.members).members;
+            slices = slice.getList(bySample.members, strategy).members;
         } catch (NoninvertibleTransformException e) {
             throw new DataStoreContentException(e);
         }
@@ -182,6 +209,36 @@ public final class CoverageAggregator extends Group<GroupBySample> {
         return Optional.empty();
     }
 
+    /**
+     * 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.
+     *
+     * @return algorithm to apply for merging source coverages at the same grid index.
+     */
+    public MergeStrategy getMergeStrategy() {
+        return strategy;
+    }
+
+    /**
+     * Sets the algorithm to apply when more than one grid coverage can be found at the same grid index.
+     * The new strategy applies to the <em>next</em> coverages to be added;
+     * previously added coverage may or may not be impacted by this change (see below).
+     * Consequently this method should usually be invoked before to add the first coverage.
+     *
+     * <h4>Effect on previously added coverages</h4>
+     * The merge strategy of previously added coverages is not modified by this method call,
+     * except for coverages that become part of the same aggregated {@link GridCoverageResource}
+     * than a coverage added after this method call.
+     * In such case, the strategy set by the most recent call to {@code setMergeStrategy(…)} prevails.
+     *
+     * @param strategy new algorithm to apply for merging source coverages at the same grid index.
+     */
+    public void setMergeStrategy(final MergeStrategy strategy) {
+        ArgumentChecks.ensureNonNull("strategy", strategy);
+        this.strategy = strategy;
+    }
+
     /**
      * Builds a resource which is the aggregation or concatenation of all components added to this aggregator.
      * The returned resource will be an instance of {@link GridCoverageResource} if possible,
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GridSlice.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GridSlice.java
index 0c3e90dc4c..e3664625a6 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GridSlice.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GridSlice.java
@@ -129,11 +129,12 @@ final class GridSlice {
      * The CRS comparisons ignore metadata and transform comparisons ignore integer translations.
      * This method takes a synchronization lock on the given list.
      *
-     * @param  groups  the list where to search for a group.
+     * @param  groups    the list where to search for a group.
+     * @param  strategy  algorithm to apply when more than one grid coverage can be found at the same grid index.
      * @return group of objects associated to the given transform (never null).
      * @throws NoninvertibleTransformException if the transform is not invertible.
      */
-    final GroupByTransform getList(final List<GroupByCRS<GroupByTransform>> groups)
+    final GroupByTransform getList(final List<GroupByCRS<GroupByTransform>> groups, final MergeStrategy strategy)
             throws NoninvertibleTransformException
     {
         final MathTransform gridToCRS = geometry.getGridToCRS(PixelInCell.CELL_CORNER);
@@ -144,10 +145,11 @@ final class GridSlice {
                 final Matrix groupToSlice = c.linearTransform(crsToGrid);
                 if (groupToSlice != null && isIntegerTranslation(groupToSlice)) {
                     setOffset(MatrixSIS.castOrCopy(groupToSlice));
+                    c.strategy = strategy;
                     return c;
                 }
             }
-            final GroupByTransform c = new GroupByTransform(geometry, gridToCRS);
+            final GroupByTransform c = new GroupByTransform(geometry, gridToCRS, strategy);
             transforms.add(c);
             return c;
         }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GridSliceLocator.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GridSliceLocator.java
index 1cc91e89f2..35d061ca53 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GridSliceLocator.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GridSliceLocator.java
@@ -21,11 +21,13 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.function.Function;
+import org.opengis.util.InternationalString;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.util.iso.Types;
 
 
 /**
@@ -190,4 +192,26 @@ final class GridSliceLocator {
         }
         return toIndex;
     }
+
+    /**
+     * Returns {@code true} if the grid extent in the search dimension is a slice of size 1.
+     *
+     * @param  sliceExtent  the extent to search.
+     * @return whether the extent is a slice in the search dimension.
+     */
+    final boolean isSlice(final GridExtent sliceExtent) {
+        return sliceExtent.getLow(searchDimension) == sliceExtent.getHigh(searchDimension);
+    }
+
+    /**
+     * Return 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.
+     */
+    final String getDimensionName(final GridExtent extent) {
+        return extent.getAxisType(searchDimension)
+                .map(Types::getCodeTitle).map(InternationalString::toString)
+                .orElseGet(() -> String.valueOf(searchDimension));
+    }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GroupByTransform.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GroupByTransform.java
index 83dd19ccf5..a62efcedc7 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GroupByTransform.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/GroupByTransform.java
@@ -57,15 +57,24 @@ final class GroupByTransform extends Group<GridSlice> {
      */
     private final MathTransform gridToCRS;
 
+    /**
+     * Algorithm to apply when more than one grid coverage can be found at the same grid index.
+     * This is set at construction time and usually keep the same value after that point,
+     * unless {@link CoverageAggregator#setMergeStrategy(MergeStrategy)} is invoked again.
+     */
+    MergeStrategy strategy;
+
     /**
      * Creates a new group of objects associated to the given transform.
      *
      * @param  geometry   geometry of the grid coverage or resource.
      * @param  gridToCRS  value or {@code geometry.getGridToCRS(PixelInCell.CELL_CORNER)}.
+     * @param  strategy   algorithm to apply when more than one grid coverage can be found at the same grid index.
      */
-    GroupByTransform(final GridGeometry geometry, final MathTransform gridToCRS) {
+    GroupByTransform(final GridGeometry geometry, final MathTransform gridToCRS, final MergeStrategy strategy) {
         this.geometry  = geometry;
         this.gridToCRS = gridToCRS;
+        this.strategy  = strategy;
     }
 
     /**
@@ -148,6 +157,7 @@ final class GroupByTransform extends Group<GridSlice> {
         final GridCoverageResource[] slices  = new GridCoverageResource[n];
         final GridSliceLocator       locator = new GridSliceLocator(members, dimensions[0], slices);
         final GridGeometry           domain  = locator.union(geometry, members, GridSlice::getGridExtent);
-        return new ConcatenatedGridResource(getName(parentListeners), parentListeners, domain, ranges, slices, locator);
+        return new ConcatenatedGridResource(getName(parentListeners), parentListeners,
+                                            domain, ranges, slices, locator, 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
new file mode 100644
index 0000000000..4578097af5
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/aggregate/MergeStrategy.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.storage.aggregate;
+
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.SubspaceNotSpecifiedException;
+
+
+/**
+ * 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.
+ *
+ * <div class="note"><b>Example:</b>
+ * 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.
+ * But if two coverages have overlapping time ranges, and if an 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.</div>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+public enum MergeStrategy {
+    /**
+     * 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.
+     *
+     * <p>This is the default strategy.</p>
+     */
+    FAIL
+}