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 2023/04/15 15:51:36 UTC

[sis] 02/04: Remove `BandAggregateGridResource` from public API. Instead, a new method is added in `CoverageAggregator`.

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 9c391c16b70d3408d30160b56bbeb44fd1db8f38
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Apr 15 15:28:51 2023 +0200

    Remove `BandAggregateGridResource` from public API.
    Instead, a new method is added in `CoverageAggregator`.
---
 .../sis/coverage/grid/GridCoverageProcessor.java   |   2 +-
 .../apache/sis/internal/storage/folder/Store.java  |   2 +-
 .../sis/storage/aggregate/AggregatedResource.java  |  22 ++-
 .../aggregate/BandAggregateGridResource.java       |  59 +++---
 .../aggregate/ConcatenatedGridResource.java        |  29 ++-
 .../sis/storage/aggregate/CoverageAggregator.java  | 210 ++++++++++++++++-----
 .../apache/sis/storage/aggregate/GridSlice.java    |   8 +-
 .../org/apache/sis/storage/aggregate/Group.java    |   4 +-
 .../sis/storage/aggregate/GroupAggregate.java      |  33 +++-
 .../apache/sis/storage/aggregate/GroupByCRS.java   |   4 +-
 .../sis/storage/aggregate/GroupBySample.java       |   8 +-
 .../sis/storage/aggregate/GroupByTransform.java    |   2 +-
 .../sis/storage/aggregate/MergeStrategy.java       |   7 +-
 .../aggregate/BandAggregateGridResourceTest.java   |  19 +-
 .../storage/aggregate/CoverageAggregatorTest.java  |   6 +-
 15 files changed, 317 insertions(+), 98 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index d04f92c113..d0c1ecb357 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -773,7 +773,7 @@ public class GridCoverageProcessor implements Cloneable {
      *   <li>All coverages shall use the same data type in their rendered image.</li>
      * </ul>
      *
-     * Some of those restrictions may be relaxed in future versions.
+     * Some of those restrictions may be relaxed in future Apache SIS versions.
      *
      * @param  sources  coverages whose bands shall be aggregated, in order. At least one coverage must be provided.
      * @param  bandsPerSource  bands to use for each source coverage, in order. May contain {@code null} elements.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
index 0e1d24c903..8fe4db9fae 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
@@ -414,7 +414,7 @@ class Store extends DataStore implements StoreResource, UnstructuredAggregate, D
         if (structuredView == null) {
             final CoverageAggregator aggregator = new CoverageAggregator(listeners);
             aggregator.addComponents(this);
-            structuredView = aggregator.build();
+            structuredView = aggregator.build(identifier);
         }
         return structuredView;
     }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/AggregatedResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/AggregatedResource.java
index 1683a313c1..515345c79a 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/AggregatedResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/AggregatedResource.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.storage.aggregate;
 
+import org.opengis.util.GenericName;
 import org.apache.sis.storage.Resource;
 
 
@@ -23,14 +24,23 @@ import org.apache.sis.storage.Resource;
  * The result of an aggregation computed by {@link CoverageAggregator}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.3
  */
 interface AggregatedResource {
+    /**
+     * Sets the identifier of the resource.
+     * This method is invoked by {@link CoverageAggregator#build(GenericName)} for assigning an identifier
+     * on the final result only. No identifier should be assigned on intermediate results (i.e. components).
+     *
+     * @param  identifier  new identifier of the resource.
+     */
+    void setIdentifier(GenericName identifier);
+
     /**
      * Sets the name of the resource.
      * This method is invoked by {@link GroupAggregate#simplify(CoverageAggregator)} when
-     * a aggregate node is excluded and we want to inherit the name of the excluded node.
+     * an aggregate node is excluded and we want to inherit the name of the excluded node.
      * It should happen before the resource is published.
      *
      * @param  name  new name of the resource.
@@ -43,14 +53,16 @@ interface AggregatedResource {
      * Otherwise returns a new resource. This resource is not modified by this method
      * call because this method can be invoked after this resource has been published.
      *
-     * <div class="note"><b>API design note:</b>
-     * we could try to design a common API for {@link org.apache.sis.storage.RasterLoadingStrategy}
+     * <h4>API design note</h4>
+     * We could try to design a common API for {@link org.apache.sis.storage.RasterLoadingStrategy}
      * and {@link MergeStrategy}. But the former changes the state of the resource while the latter
      * returns a new resource. This is because {@code RasterLoadingStrategy} does not change data,
-     * while {@link MergeStrategy} can change the data obtained from the resource.</div>
+     * while {@link MergeStrategy} can change the data obtained from the resource.
      *
      * @param  strategy  the new merge strategy to apply.
      * @return resource using the specified strategy (may be {@code this}).
+     *
+     * @see MergeStrategy#apply(Resource)
      */
     Resource apply(MergeStrategy strategy);
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
index d32e47d19a..5fecf76a4f 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
@@ -33,6 +33,7 @@ import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.AbstractGridCoverageResource;
 import org.apache.sis.storage.RasterLoadingStrategy;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.util.collection.BackingStoreException;
@@ -40,7 +41,7 @@ import org.apache.sis.util.collection.BackingStoreException;
 
 /**
  * A resource whose range is the aggregation of the ranges of a sequence of resources.
- * This class combines homogeneous {@link GridCoverageResource}s by "stacking" their bands.
+ * This class combines homogeneous {@link GridCoverageResource}s by "stacking" their sample dimensions.
  * The grid geometry is typically the same for all resources, but some variations described below are allowed.
  * The number of sample dimensions in the aggregated coverage is the sum of the number of sample dimensions in
  * each individual resource, unless a subset of sample dimensions is specified.
@@ -49,7 +50,7 @@ import org.apache.sis.util.collection.BackingStoreException;
  * <ul>
  *   <li>All resources shall use the same coordinate reference system (CRS).</li>
  *   <li>All resources shall have the same {@linkplain GridCoverageResource#getGridGeometry() domain}, except
- *       for the grid extent and the translation terms which can vary by integer amounts of grid cells.</li>
+ *       for the grid extent and the translation terms which can vary by integer numbers of grid cells.</li>
  *   <li>All grid extents shall intersect and the intersection area shall be non-empty.</li>
  *   <li>If coverage data are stored in {@link java.awt.image.RenderedImage} instances,
  *       then all images shall use the same data type.</li>
@@ -59,17 +60,17 @@ import org.apache.sis.util.collection.BackingStoreException;
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.4
  *
- * @see GridCoverageProcessor#aggregateRanges(GridCoverage[], int[][])
+ * @see CoverageAggregator#addRangeAggregate(GridCoverageResource[], int[][])
  *
  * @since 1.4
  */
-public class BandAggregateGridResource extends AbstractGridCoverageResource {
+final class BandAggregateGridResource extends AbstractGridCoverageResource implements AggregatedResource {
     /**
-     * Identifier of this resource.
+     * Persistent identifier of this resource, or {@code null} if none.
      *
      * @see #getIdentifier()
      */
-    private final GenericName name;
+    private GenericName identifier;
 
     /**
      * The source grid coverage resources.
@@ -113,18 +114,6 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource {
      */
     private final GridCoverageProcessor processor;
 
-    /**
-     * Creates a new aggregation of all sample dimensions of all specified grid coverage resources.
-     * The new resource has no name and no parent, and use a default processor with default color model.
-     *
-     * @param  sources  resources whose bands shall be aggregated, in order. At least one resource must be provided.
-     * @throws DataStoreException if an error occurred while fetching the grid geometry or sample dimensions from a resource.
-     * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
-     */
-    public BandAggregateGridResource(final GridCoverageResource... sources) throws DataStoreException {
-        this(null, null, sources, null, null);
-    }
-
     /**
      * Creates a new range aggregation of grid coverage resources.
      * The {@linkplain #getSampleDimensions() list of sample dimensions} of the aggregated resource
@@ -147,24 +136,22 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource {
      * The intersection of the domain of all resources shall be non-empty,
      * and all resources shall use the same data type in their rendered image.
      *
-     * @param  parent          the parent resource, or {@code null} if none.
-     * @param  name            name of the combined grid coverage resource, or {@code null} if none.
-     * @param  sources         resources whose bands shall be aggregated, in order. At least one resource must be provided.
-     * @param  bandsPerSource  sample dimensions for each source. May be {@code null} or may contain {@code null} elements.
-     * @param  processor       the processor to use for creating grid coverages, or {@code null} for a default processor.
+     * @param  parentListeners  listeners of the parent resource, or {@code null} if none.
+     * @param  sources          resources whose bands shall be aggregated, in order. At least one resource must be provided.
+     * @param  bandsPerSource   sample dimensions for each source. May be {@code null} or may contain {@code null} elements.
+     * @param  processor        the processor to use for creating grid coverages, or {@code null} for a default processor.
      * @throws DataStoreException if an error occurred while fetching the grid geometry or sample dimensions from a resource.
      * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
      * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity.
      */
-    public BandAggregateGridResource(final Resource parent, final GenericName name,
+    BandAggregateGridResource(final StoreListeners parentListeners,
             final GridCoverageResource[] sources, final int[][] bandsPerSource,
             final GridCoverageProcessor processor) throws DataStoreException
     {
-        super(parent);
+        super(parentListeners, false);
         try {
             final var aggregate = new MultiSourceArgument<GridCoverageResource>(sources, bandsPerSource);
             aggregate.validate(BandAggregateGridResource::range);
-            this.name             = name;
             this.sources          = aggregate.sources();
             this.gridGeometry     = aggregate.domain(BandAggregateGridResource::domain);
             this.sampleDimensions = List.copyOf(aggregate.ranges());
@@ -199,16 +186,32 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource {
         }
     }
 
+    /** Not applicable to this implementation. */
+    @Override public Resource apply(MergeStrategy strategy) {return this;}
+
+    /** Not applicable to this implementation. */
+    @Override public void setName(String name) {}
+
+    /**
+     * Sets the identifier of this resource. This is invoked by {@link CoverageAggregator} only
+     * and should not be invoked anymore after this resource has been returned to the user.
+     *
+     * @param  identifier  identifier of the combined grid coverage resource, or {@code null} if none.
+     */
+    @Override
+    public void setIdentifier(final GenericName identifier) {
+        this.identifier = identifier;
+    }
+
     /**
      * Returns the resource identifier if available.
-     * This is the name specified at construction time.
      *
      * @return an identifier for the band aggregation.
      * @throws DataStoreException if the identifier cannot be obtained.
      */
     @Override
     public Optional<GenericName> getIdentifier() throws DataStoreException {
-        return Optional.ofNullable(name);
+        return Optional.ofNullable(identifier);
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
index fb97d7f7c0..d6558b14f7 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Optional;
+import org.opengis.util.GenericName;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.Metadata;
 import org.opengis.referencing.operation.TransformException;
@@ -55,7 +56,14 @@ import org.apache.sis.util.ArraysExt;
  */
 final class ConcatenatedGridResource extends AbstractGridCoverageResource implements AggregatedResource {
     /**
-     * Name of this resource.
+     * The identifier for this aggregate, or {@code null} if none.
+     * This is optionally supplied by users for their own purposes.
+     * There is no default value.
+     */
+    private GenericName identifier;
+
+    /**
+     * Name of this resource to use in metadata.
      */
     private String name;
 
@@ -198,6 +206,7 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem
 
     /**
      * Returns a coverage with the same data than this coverage but a different merge strategy.
+     * This is the implementation of {@link MergeStrategy#apply(Resource)} public method.
      */
     @Override
     public final synchronized Resource apply(final MergeStrategy s) {
@@ -214,6 +223,24 @@ final class ConcatenatedGridResource extends AbstractGridCoverageResource implem
         this.name = name;
     }
 
+    /**
+     * Sets the identifier of this resource.
+     */
+    @Override
+    public void setIdentifier(final GenericName identifier) {
+        this.identifier = identifier;
+    }
+
+
+    /**
+     * Returns the resource persistent identifier as specified by the
+     * user in {@link CoverageAggregator}. There is no default value.
+     */
+    @Override
+    public Optional<GenericName> getIdentifier() {
+        return Optional.ofNullable(identifier);
+    }
+
     /**
      * Creates when first requested the metadata about this resource.
      */
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/CoverageAggregator.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/CoverageAggregator.java
index cdba29f5e0..edd4825342 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/CoverageAggregator.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/CoverageAggregator.java
@@ -27,7 +27,9 @@ import java.util.IdentityHashMap;
 import java.util.Collections;
 import java.util.Optional;
 import java.util.stream.Stream;
+import org.opengis.util.GenericName;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.apache.sis.image.Colorizer;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.DataStore;
@@ -36,6 +38,7 @@ 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.coverage.grid.GridCoverageProcessor;
 import org.apache.sis.coverage.grid.IllegalGridGeometryException;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.util.collection.BackingStoreException;
@@ -43,12 +46,19 @@ import org.apache.sis.util.collection.BackingStoreException;
 
 /**
  * Creates a grid coverage resource from an aggregation of an arbitrary number of other resources.
+ * This class accepts heterogeneous resources (a <cite>data lake</cite>), organizes them in a tree
+ * of resources as described in the next section, then performs different kinds of aggregation:
  *
- * <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>
+ * <ul class="verbose">
+ *   <li><b>Creation of a data cube from a collection of slices:</b>
+ *     If a collection of {@link GridCoverageResource} instances represent the same phenomenon
+ *     (for example Sea Surface Temperature) over the same geographic area but at different dates and times.
+ *     {@code CoverageAggregator} can be used for building a single data cube with a time axis.</li>
+ *   <li><b>Aggregation of bands:</b>
+ *     Resources having different sample dimensions can be combined in a single resource.</li>
+ * </ul>
  *
+ * <h2>Generated resource tree</h2>
  * 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.
@@ -100,7 +110,7 @@ public final class CoverageAggregator extends Group<GroupBySample> {
     private final StoreListeners listeners;
 
     /**
-     * The aggregates which where the sources of components added during a call to {@link #addComponents(Aggregate)}.
+     * The aggregates which were the sources of components added during a call to {@link #addComponents(Aggregate)}.
      * This is used for reusing existing aggregates instead of {@link GroupAggregate} when the content is the same.
      */
     private final Map<Set<Resource>, Queue<Aggregate>> aggregates;
@@ -113,6 +123,23 @@ public final class CoverageAggregator extends Group<GroupBySample> {
      */
     private volatile MergeStrategy strategy;
 
+    /**
+     * The processor to use for creating grid coverages. Created only when first needed.
+     * This is used for specifying the color model when creating band aggregated resources.
+     *
+     * @see #processor()
+     */
+    private GridCoverageProcessor processor;
+
+    /**
+     * Creates an initially empty aggregator with no listeners and a default grid coverage processor.
+     *
+     * @since 1.4
+     */
+    public CoverageAggregator() {
+        this(null);
+    }
+
     /**
      * Creates an initially empty aggregator.
      *
@@ -125,8 +152,9 @@ public final class CoverageAggregator extends Group<GroupBySample> {
     }
 
     /**
-     * Returns a name of the aggregate to be created.
+     * Creates a name for this group for use in metadata (not a persistent identifier).
      * This is used only if this aggregator find resources having different sample dimensions.
+     * In such case, this name will be the default name of the root resource.
      *
      * @param  locale  the locale for the name to return, or {@code null} for the default.
      * @return a name which can be used as aggregation name, or {@code null} if none.
@@ -136,29 +164,6 @@ public final class CoverageAggregator extends Group<GroupBySample> {
         return (listeners != null) ? listeners.getSourceName() : null;
     }
 
-    /**
-     * Adds all grid resources provided by the given stream. This method can be invoked from any thread.
-     * It delegates to {@link #add(GridCoverageResource)} for each element in the stream.
-     *
-     * @param  resources  resources to add.
-     * @throws DataStoreException if a resource cannot be used.
-     *
-     * @see #add(GridCoverageResource)
-     */
-    public void addAll(final Stream<? extends GridCoverageResource> resources) throws DataStoreException {
-        try {
-            resources.forEach((resource) -> {
-                try {
-                    add(resource);
-                } catch (DataStoreException e) {
-                    throw new BackingStoreException(e);
-                }
-            });
-        } catch (BackingStoreException e) {
-            throw e.unwrapOrRethrow(DataStoreException.class);
-        }
-    }
-
     /**
      * Adds the given coverage. This method can be invoked from any thread.
      *
@@ -168,7 +173,7 @@ public final class CoverageAggregator extends Group<GroupBySample> {
      */
     public void add(final GridCoverage coverage) {
         final GroupBySample bySample = GroupBySample.getOrAdd(members, coverage.getSampleDimensions());
-        final GridSlice slice = new GridSlice(coverage);
+        final GridSlice slice = new GridSlice(listeners, coverage);
         final List<GridSlice> slices;
         try {
             slices = slice.getList(bySample.members, strategy).members;
@@ -183,6 +188,7 @@ public final class CoverageAggregator extends Group<GroupBySample> {
     /**
      * Adds the given resource. This method can be invoked from any thread.
      * This method does <em>not</em> recursively decomposes an {@link Aggregate} into its component.
+     * If such decomposition is desired, see {@link #addComponents(Aggregate)} instead.
      *
      * @param  resource  resource to add.
      * @throws DataStoreException if the resource cannot be used.
@@ -226,12 +232,12 @@ public final class CoverageAggregator extends Group<GroupBySample> {
                 hasDuplicated = true;       // Should never happen, but we are paranoiac.
             }
         }
+        /*
+         * Remember the aggregate that we just added. If after the user finished to add all components,
+         * we discover that we still have the exact same set of components than the given aggregate,
+         * then we will use `resource` instead of creating a `GroupAggregate` with the same content.
+         */
         if (!(hasDuplicated || components.isEmpty())) {
-            /*
-             * We should not have 2 aggregates with the same components.
-             * But if it happens anyway, put the aggregates in a queue.
-             * Each aggregate will be used at most once.
-             */
             synchronized (aggregates) {
                 aggregates.computeIfAbsent(components, (k) -> new ArrayDeque<>(1)).add(resource);
             }
@@ -260,6 +266,95 @@ public final class CoverageAggregator extends Group<GroupBySample> {
         return Optional.empty();
     }
 
+    /**
+     * Adds all grid resources provided by the given stream. This method can be invoked from any thread.
+     * It delegates to {@link #add(GridCoverageResource)} for each element in the stream.
+     * {@link Aggregate} instances are added as-is (not decomposed in their components).
+     *
+     * @param  resources  resources to add.
+     * @throws DataStoreException if a resource cannot be used.
+     *
+     * @see #add(GridCoverageResource)
+     */
+    public void addAll(final Stream<? extends GridCoverageResource> resources) throws DataStoreException {
+        try {
+            resources.forEach((resource) -> {
+                try {
+                    add(resource);
+                } catch (DataStoreException e) {
+                    throw new BackingStoreException(e);
+                }
+            });
+        } catch (BackingStoreException e) {
+            throw e.unwrapOrRethrow(DataStoreException.class);
+        }
+    }
+
+    /**
+     * Adds a resource whose range is the aggregation of the ranges of a sequence of resources.
+     * This method combines homogeneous grid coverage resources by "stacking" their sample dimensions (bands).
+     * The grid geometry is typically the same for all resources, but some variations described below are allowed.
+     * The number of sample dimensions in the aggregated coverage is the sum of the number of sample dimensions in
+     * each individual resource, unless a subset of sample dimensions is specified.
+     *
+     * <p>The {@code bandsPerSource} argument specifies the bands to select in each resource.
+     * That array can be {@code null} for selecting all bands in all resources,
+     * or may contain {@code null} elements for selecting all bands of the corresponding resource.
+     * An empty array element (i.e. zero band to select) discards the corresponding resource.</p>
+     *
+     * <h4>Restrictions</h4>
+     * <ul>
+     *   <li>All resources shall use the same coordinate reference system (CRS).</li>
+     *   <li>All resources shall have the same {@linkplain GridCoverageResource#getGridGeometry() domain}, except
+     *       for the grid extent and the translation terms which can vary by integer numbers of grid cells.</li>
+     *   <li>All grid extents shall intersect and the intersection area shall be non-empty.</li>
+     *   <li>If coverage data are stored in {@link java.awt.image.RenderedImage} instances,
+     *       then all images shall use the same data type.</li>
+     * </ul>
+     *
+     * Some of those restrictions may be relaxed in future Apache SIS versions.
+     *
+     * @param  sources         resources whose bands shall be aggregated, in order. At least one resource must be provided.
+     * @param  bandsPerSource  sample dimensions for each source. May be {@code null} or may contain {@code null} elements.
+     * @throws DataStoreException if an error occurred while fetching the grid geometry or sample dimensions from a resource.
+     * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
+     * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity.
+     *
+     * @see #getColorizer()
+     * @see GridCoverageProcessor#aggregateRanges(GridCoverage[], int[][])
+     *
+     * @since 1.4
+     */
+    public void addRangeAggregate(final GridCoverageResource[] sources, final int[][] bandsPerSource) throws DataStoreException {
+        add(new BandAggregateGridResource(listeners, sources, bandsPerSource, processor()));
+    }
+
+    /**
+     * Returns the colorization algorithm to apply on computed images.
+     * This algorithm is used for all resources added by {@link #addRangeAggregate(GridCoverageResource[], int[][])},
+     *
+     * @return colorization algorithm to apply on computed image, or {@code null} for default.
+     *
+     * @since 1.4
+     */
+    public Colorizer getColorizer() {
+        return processor().getColorizer();
+    }
+
+    /**
+     * Sets the colorization algorithm to apply on computed images.
+     * This algorithm applies to all resources added by {@link #addRangeAggregate(GridCoverageResource[], int[][])},
+     * including resources already added before this method is invoked.
+     * If this method is never invoked, the default value is {@code null}.
+     *
+     * @param  colorizer  colorization algorithm to apply on computed image, or {@code null} for default.
+     *
+     * @since 1.4
+     */
+    public void setColorizer(final Colorizer colorizer) {
+        processor().setColorizer(colorizer);
+    }
+
     /**
      * 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)},
@@ -277,7 +372,7 @@ public final class CoverageAggregator extends Group<GroupBySample> {
      * 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.
+     * For avoiding hard-to-predict behavior, this method should 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
@@ -285,7 +380,7 @@ public final class CoverageAggregator extends Group<GroupBySample> {
      * (data cube) than a coverage added after this method call.
      * In such case, the strategy set by this call to {@code setMergeStrategy(…)} prevails.
      * Said otherwise, the merge strategy of a data cube is the strategy which was active
-     * at the time of the most recently added slice.
+     * at the time of the most recently added slice for that data cube.
      *
      * @param  strategy  new algorithm to apply for merging source coverages at the same grid index,
      *                   or {@code null} if none.
@@ -294,20 +389,51 @@ public final class CoverageAggregator extends Group<GroupBySample> {
         this.strategy = strategy;
     }
 
+    /**
+     * Returns the processor to use for creating grid coverages.
+     */
+    private synchronized GridCoverageProcessor processor() {
+        if (processor == null) {
+            processor = new GridCoverageProcessor();
+        }
+        return processor;
+    }
+
     /**
      * 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,
      * or an instance of {@link Aggregate} if some heterogeneity in grid geometries or sample dimensions
-     * prevent the concatenation of all coverages in a single resource.
+     * prevents the concatenation of all coverages in a single resource.
+     *
+     * <p>An identifier can optionally be specified for the resource.
+     * This identifier will be used if this method creates an aggregated or concatenated resource,
+     * but it will be ignored if this method returns directly one of the resource specified to the
+     * {@code add(…)} methods.</p>
      *
-     * <p>This method is not thread safe. If the {@code add(…)} and {@code addAll(…)} methods were invoked
-     * in background threads, then all additions must be finished before this method is invoked.</p>
+     * <h4>Multi-threading</h4>
+     * If the {@code add(…)} and {@code addAll(…)} methods were invoked in background threads,
+     * then all additions must be finished before this method is invoked.
      *
+     * @param  identifier  identifier to assign to the aggregated resource, or {@code null} if none.
      * @return the aggregation or concatenation of all components added to this aggregator.
+     *
+     * @since 1.4
      */
-    public Resource build() {
+    public synchronized Resource build(final GenericName identifier) {
         final GroupAggregate aggregate = prepareAggregate(listeners);
         aggregate.fillWithChildAggregates(this, GroupBySample::createComponents);
-        return aggregate.simplify(this);
+        final Resource result = aggregate.simplify(this);
+        if (result instanceof AggregatedResource) {
+            ((AggregatedResource) result).setIdentifier(identifier);
+        }
+        return result;
+    }
+
+    /**
+     * @deprecated Replaced by {@link #build(GenericName)}.
+     */
+    @Deprecated
+    public Resource build() {
+        return build(null);
     }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GridSlice.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GridSlice.java
index 502cf6878a..09fb3c54e8 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GridSlice.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GridSlice.java
@@ -24,6 +24,7 @@ import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.coverage.grid.GridCoverage;
@@ -72,10 +73,11 @@ final class GridSlice {
     /**
      * Creates a new slice for the specified coverage.
      *
-     * @param  slice  coverage associated to this slice.
+     * @param  parent  listeners of the parent resource, or {@code null} if none.
+     * @param  slice   coverage associated to this slice.
      */
-    GridSlice(final GridCoverage slice) {
-        resource = new MemoryGridResource(null, slice);
+    GridSlice(final StoreListeners parent, final GridCoverage slice) {
+        resource = new MemoryGridResource(parent, slice);
         geometry = slice.getGridGeometry();
         offset   = new long[geometry.getDimension()];
     }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/Group.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/Group.java
index d1ed30eb32..649eec3ae6 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/Group.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/Group.java
@@ -58,11 +58,11 @@ abstract class Group<E> {
     }
 
     /**
-     * Creates a name for this group.
+     * Creates a name for this group for use in metadata (not a persistent identifier).
      * This is used as the resource name if an aggregated resource needs to be created.
      *
      * @param  locale  the locale for the name to return, or {@code null} for the default.
-     * @return a name which can be used as aggregation name.
+     * @return a name which can be used as aggregation name for metadata purposes.
      */
     abstract String createName(Locale locale);
 
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupAggregate.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupAggregate.java
index e46b8b2a52..af653cb63d 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupAggregate.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupAggregate.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.Collection;
 import java.util.Optional;
 import java.util.function.BiConsumer;
+import org.opengis.util.GenericName;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.Metadata;
 import org.opengis.referencing.operation.TransformException;
@@ -47,7 +48,7 @@ import org.apache.sis.geometry.ImmutableEnvelope;
  * it would not be a persistent identifier.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.3
  */
 final class GroupAggregate extends AbstractResource implements Aggregate, AggregatedResource {
@@ -56,6 +57,15 @@ final class GroupAggregate extends AbstractResource implements Aggregate, Aggreg
      */
     private static final int KEEP_ALIVE = 2;
 
+    /**
+     * The identifier for this aggregate, or {@code null} if none.
+     * This is optionally supplied by users for their own purposes.
+     * There is no default value.
+     *
+     * @see #getIdentifier()
+     */
+    private GenericName identifier;
+
     /**
      * Name of this aggregate, or {@code null} if none.
      * This is <strong>not</strong> a persistent identifier.
@@ -66,6 +76,8 @@ final class GroupAggregate extends AbstractResource implements Aggregate, Aggreg
      * The components of this aggregate. Array elements are initially null, but should all become non-null
      * after a {@code fill(…)} method has been invoked. If the length is smaller than {@value #KEEP_ALIVE},
      * then this aggregate is only a temporary object.
+     *
+     * @see #components()
      */
     private final Resource[] components;
 
@@ -151,6 +163,7 @@ final class GroupAggregate extends AbstractResource implements Aggregate, Aggreg
 
     /**
      * Returns an aggregate with the same data than this aggregate but a different merge strategy.
+     * This is the implementation of {@link MergeStrategy#apply(Resource)} public method.
      */
     @Override
     public final synchronized Resource apply(final MergeStrategy strategy) {
@@ -226,6 +239,24 @@ final class GroupAggregate extends AbstractResource implements Aggregate, Aggreg
         return aggregator.existingAggregate(components).orElse(this);
     }
 
+    /**
+     * Sets the identifier of this resource.
+     */
+    @Override
+    public void setIdentifier(final GenericName identifier) {
+        this.identifier = identifier;
+    }
+
+
+    /**
+     * Returns the resource persistent identifier as specified by the
+     * user in {@link CoverageAggregator}. There is no default value.
+     */
+    @Override
+    public Optional<GenericName> getIdentifier() {
+        return Optional.ofNullable(identifier);
+    }
+
     /**
      * Returns the components of this aggregate.
      */
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupByCRS.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupByCRS.java
index 571c85eb54..3099885fa7 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupByCRS.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupByCRS.java
@@ -56,7 +56,9 @@ final class GroupByCRS<E> extends Group<E> {
     }
 
     /**
-     * Returns a name for this group.
+     * Creates a name for this group for use in metadata (not a persistent identifier).
+     * This is used as the resource name if an aggregated resource needs to be created.
+     * The name distinguishes the group by their CRS name.
      */
     @Override
     final String createName(final Locale locale) {
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupBySample.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupBySample.java
index 40c4cb1805..8d97e7f033 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupBySample.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupBySample.java
@@ -31,7 +31,7 @@ import org.apache.sis.coverage.SampleDimension;
  * which in turn contain an arbitrary number of {@link GridSlice} instances.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.3
  */
 final class GroupBySample extends Group<GroupByCRS<GroupByTransform>> {
@@ -50,7 +50,9 @@ final class GroupBySample extends Group<GroupByCRS<GroupByTransform>> {
     }
 
     /**
-     * Returns a name for this group.
+     * Creates a name for this group for use in metadata (not a persistent identifier).
+     * This is used as the resource name if an aggregated resource needs to be created.
+     * Current implementation tries to return a text describing sample dimensions.
      */
     @Override
     final String createName(final Locale locale) {
@@ -84,7 +86,7 @@ final class GroupBySample extends Group<GroupByCRS<GroupByTransform>> {
     }
 
     /**
-     * Creates sub-aggregates for each member of this group and add them to the given aggregate.
+     * Creates sub-aggregates for each member of this group and adds them to the given aggregate.
      *
      * @param  destination  where to add sub-aggregates.
      */
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupByTransform.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupByTransform.java
index d8884efe15..fbf54ec8d0 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupByTransform.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/GroupByTransform.java
@@ -78,7 +78,7 @@ final class GroupByTransform extends Group<GridSlice> {
     }
 
     /**
-     * Returns a name for this group.
+     * Creates a name for this group for use in metadata (not a persistent identifier).
      * This is used as the resource name if an aggregated resource needs to be created.
      * Current implementation assumes that the main reason why many groups may exist is
      * that they differ by their resolution.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/MergeStrategy.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/MergeStrategy.java
index 5cf4702cde..e149b29d59 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/MergeStrategy.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/MergeStrategy.java
@@ -32,14 +32,15 @@ import org.apache.sis.internal.util.Strings;
  * 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
+ * <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.
  * 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.</div>
+ * This enumeration specify 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.
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
index c447fa6aca..a1669ab993 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
@@ -67,6 +67,18 @@ public final class BandAggregateGridResourceTest extends TestCase {
                                   MathTransforms.identity(2), HardCodedCRS.WGS84);
     }
 
+    /**
+     * Creates a new aggregation of all sample dimensions of all specified grid coverage resources.
+     * The new resource has no identifier and no parent, and uses a default processor with default color model.
+     *
+     * @param  sources  resources whose bands shall be aggregated, in order. At least one resource must be provided.
+     * @throws DataStoreException if an error occurred while fetching the grid geometry or sample dimensions from a resource.
+     * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others.
+     */
+    private static BandAggregateGridResource create(final GridCoverageResource... sources) throws DataStoreException {
+        return new BandAggregateGridResource(null, sources, null, null);
+    }
+
     /**
      * Tests aggregation of two resources having one band each.
      * All source coverages share the same grid geometry.
@@ -77,7 +89,7 @@ public final class BandAggregateGridResourceTest extends TestCase {
     public void aggregateBandsFromSingleBandSources() throws DataStoreException {
         final GridCoverageResource first  = singleValuePerBand(17);
         final GridCoverageResource second = singleValuePerBand(23);
-        final var aggregation = new BandAggregateGridResource(first, second);
+        final var aggregation = create(first, second);
 
         assertAllPixelsEqual(aggregation.read(null), 17, 23);
         assertAllPixelsEqual(aggregation.read(null, 0), 17);
@@ -96,7 +108,7 @@ public final class BandAggregateGridResourceTest extends TestCase {
         final GridCoverageResource thirdAndFourthBands = singleValuePerBand(103, 104);
         final GridCoverageResource fifthAndSixthBands  = singleValuePerBand(105, 106);
 
-        var aggregation = new BandAggregateGridResource(firstAndSecondBands, thirdAndFourthBands, fifthAndSixthBands);
+        var aggregation = create(firstAndSecondBands, thirdAndFourthBands, fifthAndSixthBands);
         aggregation.getIdentifier().ifPresent(name -> fail("No name provided at creation, but one was returned: " + name));
         assertAllPixelsEqual(aggregation.read(null), 101, 102, 103, 104, 105, 106);
         assertAllPixelsEqual(aggregation.read(null, 1, 2, 4, 5), 102, 103, 105, 106);
@@ -106,10 +118,11 @@ public final class BandAggregateGridResourceTest extends TestCase {
          * In addition, band order in one of the 3 coverages is modified.
          */
         final LocalName testName = Names.createLocalName(null, null, "test-name");
-        aggregation = new BandAggregateGridResource(null, testName,
+        aggregation = new BandAggregateGridResource(null,
                 new GridCoverageResource[] {firstAndSecondBands, thirdAndFourthBands, fifthAndSixthBands},
                 new int[][] {null, new int[] {1, 0}, new int[] {1}}, null);
 
+        aggregation.setIdentifier(testName);
         assertEquals(testName, aggregation.getIdentifier().orElse(null));
         assertAllPixelsEqual(aggregation.read(null), 101, 102, 104, 103, 106);
         assertAllPixelsEqual(aggregation.read(null, 2, 4), 104, 106);
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/CoverageAggregatorTest.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/CoverageAggregatorTest.java
index 655a649a5c..fe5ca02dae 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/CoverageAggregatorTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/CoverageAggregatorTest.java
@@ -28,7 +28,7 @@ import static org.junit.Assert.*;
  * Tests {@link CoverageAggregator}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.3
  */
 public final class CoverageAggregatorTest extends TestCase {
@@ -39,7 +39,7 @@ public final class CoverageAggregatorTest extends TestCase {
      */
     @Test
     public void testEmpty() throws DataStoreException {
-        final var aggregator = new CoverageAggregator(null);
-        assertTrue(((Aggregate) aggregator.build()).components().isEmpty());
+        final var aggregator = new CoverageAggregator();
+        assertTrue(((Aggregate) aggregator.build(null)).components().isEmpty());
     }
 }