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 e9e0b2342b Improvement: `BandAggregateImage` now merges the bands of 
repeated sources no matter their position in the array of sources. Before this 
commit, the bands of repeated sources where merged only for consecutive sources 
(e.g. at index `i` and `i+1`). The merging of repeated sources is necessary for 
`BandAggregateGridResource` implementation, which relies on that. While the 
merging of consecutive sources was sufficient in most cases, it was a risk of 
causing confusing beha [...]
e9e0b2342b is described below

commit e9e0b2342b52686ae433ad0eb0c577b599b5964c
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Apr 11 19:55:06 2023 +0200

    Improvement: `BandAggregateImage` now merges the bands of repeated sources 
no matter their position in the array of sources.
    Before this commit, the bands of repeated sources where merged only for 
consecutive sources (e.g. at index `i` and `i+1`).
    The merging of repeated sources is necessary for 
`BandAggregateGridResource` implementation, which relies on that.
    While the merging of consecutive sources was sufficient in most cases, it 
was a risk of causing confusing behavior
    if not generalized for consistency at all positions in the `sources` array.
---
 .../org/apache/sis/gui/map/ValuesFormatter.java    |   3 +-
 .../coverage/grid/BandAggregateGridCoverage.java   |   2 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   |  37 ++-
 .../org/apache/sis/image/BandAggregateImage.java   |  21 +-
 .../java/org/apache/sis/image/BandSelectImage.java |   5 +-
 .../java/org/apache/sis/image/ImageProcessor.java  |  51 ++--
 .../org/apache/sis/image/MultiSourceLayout.java    |  17 +-
 .../java/org/apache/sis/image/Visualization.java   |   2 +-
 .../sis/internal/coverage/MultiSourceArgument.java | 318 ++++++++++++++-------
 .../sis/internal/coverage/RangeArgument.java       |   7 +-
 .../apache/sis/image/BandAggregateImageTest.java   |   7 +-
 .../org/apache/sis/internal/util/Numerics.java     |  13 +
 .../main/java/org/apache/sis/util/ArraysExt.java   | 136 +++++----
 .../java/org/apache/sis/util/ArraysExtTest.java    |  32 ++-
 .../aggregate/BandAggregateGridResource.java       |   5 +-
 15 files changed, 427 insertions(+), 229 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFormatter.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFormatter.java
index 5f28a52a4f..f2c4cb01ef 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFormatter.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesFormatter.java
@@ -42,6 +42,7 @@ import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.UnitFormat;
 import org.apache.sis.util.Characters;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.util.Numerics;
 
 import static org.apache.sis.internal.gui.LogHandler.LOGGER;
 
@@ -473,7 +474,7 @@ final class ValuesFormatter extends 
ValuesUnderCursor.Formatter {
      *         or does not use a supported bits pattern.
      */
     private static Long toNodataKey(final int band, final float value) {
-        return (((long) MathFunctions.toNanOrdinal(value)) << Integer.SIZE) | 
band;
+        return Numerics.tuple(MathFunctions.toNanOrdinal(value), band);
     }
 
     /**
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
index 113033a9fb..9e671073a9 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
@@ -92,7 +92,7 @@ final class BandAggregateGridCoverage extends GridCoverage {
     BandAggregateGridCoverage(final MultiSourceArgument<GridCoverage> 
aggregate, final ImageProcessor processor) {
         super(aggregate.domain(GridCoverage::getGridGeometry), 
aggregate.ranges());
         this.sources           = aggregate.sources();
-        this.bandsPerSource    = aggregate.bandsPerSource();
+        this.bandsPerSource    = aggregate.bandsPerSource(true);
         this.numBands          = aggregate.numBands();
         this.sourceOfGridToCRS = aggregate.sourceOfGridToCRS();
         this.processor         = processor;
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 f7f9017fc9..b56fd29e0f 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
@@ -714,17 +714,8 @@ public class GridCoverageProcessor implements Cloneable {
      * The {@linkplain GridCoverage#getSampleDimensions() list of sample 
dimensions} of
      * the aggregated coverage will be the concatenation of the lists of all 
sources.
      *
-     * <h4>Restrictions</h4>
-     * All coverages shall have compatible domain, defined as below:
-     *
-     * <ul>
-     *   <li>Same CRS.</li>
-     *   <li>Same <cite>grid to CRS</cite> transform except for translation 
terms.</li>
-     *   <li>Translation terms that differ only by an integer amount of grid 
cells.</li>
-     * </ul>
-     *
-     * The intersection of the domain of all coverages shall be non-empty,
-     * and all coverages shall use the same data type in their rendered image.
+     * <p>This convenience method delegates to {@link 
#aggregateRanges(GridCoverage[], int[][])}.
+     * See that method for more information on restrictions.</p>
      *
      * @param  sources  coverages whose ranges shall be aggregated, in order. 
At least one coverage must be provided.
      * @return the aggregated coverage, or {@code sources[0]} returned 
directly if only one coverage was supplied.
@@ -742,16 +733,23 @@ public class GridCoverageProcessor implements Cloneable {
     /**
      * Aggregates in a single coverage the specified bands of a sequence of 
source coverages, in order.
      * This method performs the same work than {@link 
#aggregateRanges(GridCoverage...)},
-     * but with the possibility to specify the bands to retain in each source 
coverage.
-     * The {@code bandsPerSource} argument specifies the bands to select in 
each source coverage.
-     * That array can be {@code null} for selecting all bands in all source 
coverages,
-     * or may contain {@code null} elements for selecting all bands of the 
corresponding coverage.
-     * An empty array element (i.e. zero band to select) discards the 
corresponding source coverage.
-     * In the latter case, the discarded element in the {@code sources} array 
may be {@code null}.
+     * but with the possibility to specify the sample dimensions to retain in 
each source coverage.
+     * The {@code bandsPerSource} argument specifies the sample dimensions to 
keep, in order.
+     * That array can be {@code null} for selecting all sample dimensions in 
all source coverages,
+     * or may contain {@code null} elements for selecting all sample 
dimensions of the corresponding coverage.
+     * An empty array element (i.e. zero sample dimension to select) discards 
the corresponding source coverage.
+     *
+     * <h4>Restrictions</h4>
+     * <ul>
+     *   <li>All coverage shall use the same CRS.</li>
+     *   <li>All coverage shall use the same <cite>grid to CRS</cite> 
transform except for translation terms.</li>
+     *   <li>Translation terms in <cite>grid to CRS</cite> can differ only by 
an integer amount of grid cells.</li>
+     *   <li>The intersection of the domain of all coverages shall be 
non-empty.</li>
+     *   <li>All coverages shall use the same data type in their rendered 
image.</li>
+     * </ul>
      *
      * @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 be {@code null} or may contain {@code null} 
elements.
+     * @param  bandsPerSource  bands to use for each source coverage, in 
order. May contain {@code null} elements.
      * @return the aggregated coverage, or one of the sources if it can be 
used directly.
      * @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.
@@ -762,7 +760,6 @@ public class GridCoverageProcessor implements Cloneable {
      */
     public GridCoverage aggregateRanges(GridCoverage[] sources, int[][] 
bandsPerSource) {
         final var aggregate = new MultiSourceArgument<>(sources, 
bandsPerSource);
-        aggregate.identityAsNull();
         aggregate.validate(GridCoverage::getSampleDimensions);
         if (aggregate.isIdentity()) {
             return aggregate.sources()[0];
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
index 255176dcc8..9583193eae 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
@@ -116,6 +116,10 @@ class BandAggregateImage extends MultiSourceImage {
             sources[i] = source;
             bandsPerSource[i] = new int[] {band - lower};
         }
+        /*
+         * Tne same image may be repeated many times in the `sources` array, 
each time with only one band specified.
+         * But we rely on `create(…)` post-processing for merging multiple 
references to a single one for each image.
+         */
         if (unwrapper != null) {
             unwrapper.apply(sources, bandsPerSource);
             return null;
@@ -146,14 +150,23 @@ class BandAggregateImage extends MultiSourceImage {
         } else {
             image = new BandAggregateImage(layout, colorizer, allowSharing, 
parallel);
         }
+        RenderedImage result = image;
         if (image.getNumSources() == 1) {
-            RenderedImage source = image.getSource();
+            result = image.getSource();
             if ((forceColors && colorizer != null)) {
-                source = RecoloredImage.applySameColors(source, image);
+                result = RecoloredImage.applySameColors(result, image);
             }
-            return source;
+        } else {
+            result = ImageProcessor.unique(result);
+        }
+        /*
+         * If we need to use `BandSelectImage` for reordering bands, the 
`unwrap` argument
+         * MUST be false for avoiding `StackOverflowError` with never-ending 
recusivity.
+         */
+        if (layout.bandSelect != null) {
+            result = BandSelectImage.create(result, false, layout.bandSelect);
         }
-        return ImageProcessor.unique(image);
+        return result;
     }
 
     /**
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
index 2b126c5d28..a5aeaff2cd 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
@@ -105,10 +105,11 @@ class BandSelectImage extends SourceAlignedImage {
      * Creates a new "band select" operation for the given source.
      *
      * @param  source  the image in which to select bands.
+     * @param  unwrap  whether to allow unwrapping of {@link 
BandAggregateImage} source.
      * @param  bands   the bands to select. Not cloned in order to share 
common arrays when possible.
      *                 If that array instance was user supplied, then it 
should be cloned by caller.
      */
-    static RenderedImage create(RenderedImage source, int... bands) {
+    static RenderedImage create(RenderedImage source, final boolean unwrap, 
int... bands) {
         final int numBands = ImageUtilities.getNumBands(source);
         if (bands.length == numBands && ArraysExt.isRange(0, bands)) {
             return source;
@@ -128,7 +129,7 @@ class BandSelectImage extends SourceAlignedImage {
             bands  = select.getSourceBands(bands);
             source = select.getSource();
         }
-        if (source instanceof BandAggregateImage) {
+        if (unwrap && source instanceof BandAggregateImage) {
             return ((BandAggregateImage) source).subset(bands, cm, null);
         }
         /*
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
index a6149ea3f0..57741fd29c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
@@ -916,7 +916,7 @@ public class ImageProcessor implements Cloneable {
      */
     public RenderedImage selectBands(final RenderedImage source, final int... 
bands) {
         ArgumentChecks.ensureNonNull("source", source);
-        return BandSelectImage.create(source, bands.clone());
+        return BandSelectImage.create(source, true, bands.clone());
     }
 
     /**
@@ -926,26 +926,9 @@ public class ImageProcessor implements Cloneable {
      * contain values from the pixels at the same coordinates in all source 
images.
      * The result image will be bounded by the intersection of all source 
images.
      *
-     * <p>If all source images are {@link WritableRenderedImage} instances,
-     * then the returned image will also be a {@link WritableRenderedImage}.
-     * In such case values written in the returned image will be copied back
-     * to the source images.</p>
-     *
-     * <h4>Restrictions</h4>
-     * All images shall use the same {@linkplain SampleModel#getDataType() 
data type},
-     * and all source images shall intersect each other with a non-empty 
intersection area.
-     * However it is not required that all images have the same bounds or the 
same tiling scheme.
-     *
-     * <h4>Memory saving</h4>
-     * The returned image may opportunistically share the underlying data 
arrays of
-     * some source images. Bands are really copied only when sharing is not 
possible.
-     * The actual strategy may be a mix of both arrays sharing and bands 
copies.
-     *
-     * <h4>Properties used</h4>
-     * This operation uses the following properties in addition to method 
parameters:
-     * <ul>
-     *   <li>{@linkplain #getColorizer() Colorizer}.</li>
-     * </ul>
+     * <p>This convenience method delegates to {@link 
#aggregateBands(RenderedImage[], int[][])}.
+     * See that method for more information on restrictions, writable images, 
memory saving and
+     * properties used.</p>
      *
      * @param  sources  images whose bands shall be aggregated, in order. At 
least one image must be provided.
      * @return the aggregated image, or {@code sources[0]} returned directly 
if only one image was supplied.
@@ -969,17 +952,27 @@ public class ImageProcessor implements Cloneable {
      * An empty array element (i.e. zero band to select) discards the 
corresponding source image.
      * In the latter case, the discarded element in the {@code sources} array 
may be {@code null}.
      *
-     * <p>If all source images are {@link WritableRenderedImage} instances,
+     * <h4>Restrictions</h4>
+     * All images shall use the same {@linkplain SampleModel#getDataType() 
data type},
+     * and all source images shall intersect each other with a non-empty 
intersection area.
+     * However it is not required that all images have the same bounds or the 
same tiling scheme.
+     *
+     * <h4>Writable image</h4>
+     * If all source images are {@link WritableRenderedImage} instances,
      * then the returned image will also be a {@link WritableRenderedImage}.
      * In such case values written in the returned image will be copied back
-     * to the source images.</p>
+     * to the source images.
      *
-     * <h4>Restrictions</h4>
-     * <ul>
-     *   <li>All images shall use the same {@linkplain 
SampleModel#getDataType() data type}.</li>
-     *   <li>All source images shall intersect each other with a non-empty 
intersection area.</li>
-     *   <li>The same band for a given source image cannot be used twice.</li>
-     * </ul>
+     * <h4>Memory saving</h4>
+     * The returned image may opportunistically share the underlying data 
arrays of
+     * some source images. Bands are really copied only when sharing is not 
possible.
+     * The actual strategy may be a mix of both arrays sharing and bands 
copies.
+     *
+     * <h4>Repeated bands</h4>
+     * For any value of <var>i</var>, the array at {@code bandsPerSource[i]} 
shall not contain duplicated values.
+     * This restriction is for capturing common errors, in order to reduce the 
risk of accidental band repetition.
+     * However the same band can be repeated indirectly if the same image is 
repeated at different values of <var>i</var>.
+     * But even when a source band is referenced many times, all occurrences 
still share pixel data copied at most once.
      *
      * <h4>Properties used</h4>
      * This operation uses the following properties in addition to method 
parameters:
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java
index 415915c93e..42c005dcc1 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java
@@ -75,6 +75,11 @@ final class MultiSourceLayout extends ImageLayout {
      */
     private final int[][] bandsPerSource;
 
+    /**
+     * Final band select operation to apply on the aggregated result, or 
{@code null} if none.
+     */
+    final int[] bandSelect;
+
     /**
      * The sample model of the combined image.
      * All {@linkplain BandedSampleModel#getBandOffsets() band offsets} are 
zeros and
@@ -128,12 +133,12 @@ final class MultiSourceLayout extends ImageLayout {
     @Workaround(library="JDK", version="1.8")
     static MultiSourceLayout create(RenderedImage[] sources, int[][] 
bandsPerSource, boolean allowSharing) {
         final var aggregate = new MultiSourceArgument<RenderedImage>(sources, 
bandsPerSource);
-        aggregate.identityAsNull();
         aggregate.unwrap(BandAggregateImage::unwrap);
         aggregate.validate(ImageUtilities::getNumBands);
 
+        int[] bandSelect   = aggregate.mergeDuplicatedSources();
         sources            = aggregate.sources();
-        bandsPerSource     = aggregate.bandsPerSource();
+        bandsPerSource     = aggregate.bandsPerSource(true);
         Rectangle domain   = null;          // Nullity check used for telling 
when the first image is processed.
         int scanlineStride = 0;
         int tileWidth      = 0;
@@ -203,7 +208,7 @@ final class MultiSourceLayout extends ImageLayout {
         final var preferredTileSize = new Dimension((int) cx, (int) cy);
         final boolean exactTileSize = ((cx | cy) >>> Integer.SIZE) == 0;
         allowSharing &= exactTileSize;
-        return new MultiSourceLayout(sources, bandsPerSource, domain, 
preferredTileSize, exactTileSize,
+        return new MultiSourceLayout(sources, bandsPerSource, bandSelect, 
domain, preferredTileSize, exactTileSize,
                 chooseMinTile(tileGridXOffset, domain.x, 
preferredTileSize.width),
                 chooseMinTile(tileGridYOffset, domain.y, 
preferredTileSize.height),
                 commonDataType, aggregate.numBands(), allowSharing ? 
scanlineStride : 0);
@@ -214,13 +219,14 @@ final class MultiSourceLayout extends ImageLayout {
      *
      * @param  sources            images to combine, in order.
      * @param  bandsPerSource     bands to use for each source image, in 
order. May contain {@code null} elements.
+     * @param  bandSelect         final band select operation to apply on the 
aggregated result, or {@code null}.
      * @param  domain             bounds of the image to create.
      * @param  preferredTileSize  the preferred tile size.
      * @param  commonDataType     data type of the combined image.
      * @param  scanlineStride     common scanline stride if data buffers will 
be shared, or 0 if no sharing.
      * @param  numBands           number of bands of the image to create.
      */
-    private MultiSourceLayout(final RenderedImage[] sources, final int[][] 
bandsPerSource,
+    private MultiSourceLayout(final RenderedImage[] sources, final int[][] 
bandsPerSource, final int[] bandSelect,
             final Rectangle domain, final Dimension preferredTileSize, final 
boolean exactTileSize,
             final int minTileX, final int minTileY, final int commonDataType, 
final int numBands,
             final int scanlineStride)
@@ -228,6 +234,7 @@ final class MultiSourceLayout extends ImageLayout {
         super(preferredTileSize, false);
         this.exactTileSize  = exactTileSize;
         this.bandsPerSource = bandsPerSource;
+        this.bandSelect     = bandSelect;
         this.sources        = sources;
         this.domain         = domain;
         this.minTileX       = minTileX;
@@ -242,7 +249,7 @@ final class MultiSourceLayout extends ImageLayout {
             RenderedImage source = sources[i];
             final int[] bands = bandsPerSource[i];
             if (bands != null) {
-                source = BandSelectImage.create(source, bands);
+                source = BandSelectImage.create(source, true, bands);
             }
             filteredSources[i] = source;
         }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
index 1b86bdd292..7305d66d7e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
@@ -264,7 +264,7 @@ final class Visualization extends ResampledImage {
                     break;
                 }
             }
-            source = BandSelectImage.create(source, visibleBand);
+            source = BandSelectImage.create(source, true, visibleBand);
             final SampleDimension visibleSD = (sampleDimensions != null && 
visibleBand < sampleDimensions.length)
                                             ? sampleDimensions[visibleBand] : 
null;
             /*
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java
index 51318aafec..8c53d447a0 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java
@@ -17,9 +17,11 @@
 package org.apache.sis.internal.coverage;
 
 import java.util.List;
+import java.util.BitSet;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.IdentityHashMap;
 import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -27,6 +29,7 @@ import java.util.function.ToIntFunction;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.IllegalGridGeometryException;
+import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
@@ -45,6 +48,9 @@ import org.apache.sis.util.ComparisonMode;
  * This is done by an {@link #unwrap(Consumer)}, which should be invoked in 
order to get a flattened
  * view of nested aggregations.</p>
  *
+ * <p>All methods in this class may return direct references to internal 
arrays.
+ * This is okay if instances of this class are discarded immediately after 
usage.</p>
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.4
  *
@@ -52,55 +58,80 @@ import org.apache.sis.util.ComparisonMode;
  *
  * @since 1.4
  */
+@SuppressWarnings("ReturnOfCollectionOrArrayField")     // See class Javadoc.
 public final class MultiSourceArgument<S> {
     /**
      * The user-specified sources, usually grid coverages or rendered images.
      * This is initially a copy of the array specified at construction time.
      * This array is modified in-place by {@code validate(…)} methods for
      * removing empty sources and flattening nested aggregations.
+     *
+     * @see #sources()
      */
     private S[] sources;
 
     /**
      * Indices of selected bands or sample dimensions for each source.
+     * This array is modified in-place by {@code validate(…)} methods
+     * for removing empty elements and flattening nested aggregations.
+     *
      * The length of this array must be always equal to the {@link #sources} 
array length.
-     * The array is non-null but may contain {@code null} elements for meaning 
"all bands".
-     * This array is modified in-place by {@code validate(…)} methods for 
removing empty
-     * elements and flattening nested aggregations.
+     * The array is non-null but may contain {@code null} elements for meaning 
"all bands"
+     * before validation. After validation, all null elements are replaced by 
sequences.
+     *
+     * @see #bandsPerSource(boolean)
      */
     private int[][] bandsPerSource;
 
     /**
-     * Whether to allow null elements in {@link #bandsPerSource} for meaning 
"all bands".
+     * Number of bands per source. This array is built by {@code validate(…)} 
methods.
      */
-    private boolean identityAsNull;
+    private int[] numBandsPerSource;
 
     /**
-     * A method which may decompose a source in a sequence of deeper sources 
associated with their bands to select.
+     * Whether the bands selection for a given source is an identity operation.
+     * For a source at index <var>i</var>, the bit <var>i</var> is set to 1 if
+     * {@code bandsPerSource[i]} is a sequence selecting all bands in order.
+     *
+     * <p>This field is initially null and assigned on validation.
+     * Consequently this field can also be used for checking whether
+     * one of the {@code validate(…)} methods has been invoked.</p>
+     *
+     * @see #validate(Function)
+     * @see #validate(ToIntFunction)
      */
-    private Consumer<Unwrapper> unwrapper;
+    private BitSet isIdentity;
 
     /**
-     * Union of all selected bands in all specified sources, or {@code null} 
if not applicable.
+     * Number of valid elements in {@link #sources} array after empty elements 
have been removed.
+     * This is initially zero and is set after a {@code validate(…)} method 
has been invoked.
      */
-    private List<SampleDimension> ranges;
+    private int validatedSourceCount;
 
     /**
      * Total number of bands. This is the length of the {@link #ranges} list,
      * except that this information is provided even if {@code ranges} is null.
      */
-    private int numBands;
+    private int totalBandCount;
+
+    /**
+     * Union of all selected bands in all specified sources, or {@code null} 
if not applicable.
+     */
+    private List<SampleDimension> ranges;
 
     /**
      * Index of a source having the same "grid to CRS" transform than the grid 
geometry
      * returned by {@link #domain(Function)}. If there is none, then this 
value is -1.
      */
-    private int sourceOfGridToCRS;
+    private int sourceOfGridToCRS = -1;
 
     /**
-     * Whether one of the {@code validate(…)} methods has been invoked.
+     * A method which may decompose a source in a sequence of deeper sources 
associated with their bands to select.
+     * Shall be set (if desired) before a {@code validate(…)} method is 
invoked.
+     *
+     * @see #unwrap(Consumer)
      */
-    private boolean validated;
+    private Consumer<Unwrapper> unwrapper;
 
     /**
      * Prepares an argument validator for the given sources and bands 
arguments.
@@ -133,17 +164,19 @@ public final class MultiSourceArgument<S> {
         }
         this.sources        = sources.clone();
         this.bandsPerSource = bandsPerSource;
-        sourceOfGridToCRS   = -1;
+        numBandsPerSource   = new int[sources.length];
     }
 
     /**
-     * Requests the use of {@code null} elements for meaning "all bands".
-     * The null elements can appear in the {@link #bandsPerSource()} array,
-     * but the array itself will still never null.
+     * Ensures that a {@code validate(…)} method has been invoked (or not).
+     *
+     * @param  expected  {@code true} if the caller expects validation to be 
done, or
+     *                   {@code false} if the caller expects validation to not 
be done yet.
      */
-    public void identityAsNull() {
-        if (validated) throw new IllegalStateException();
-        identityAsNull = true;
+    private void checkValidationState(final boolean expected) {
+        if ((isIdentity == null) == expected) {
+            throw new IllegalStateException();
+        }
     }
 
     /**
@@ -156,7 +189,7 @@ public final class MultiSourceArgument<S> {
      * @param  filter  the method to invoke for getting the sources of an 
image or coverage.
      */
     public void unwrap(final Consumer<Unwrapper> filter) {
-        if (validated) throw new IllegalStateException();
+        checkValidationState(false);
         unwrapper = filter;
     }
 
@@ -226,9 +259,9 @@ public final class MultiSourceArgument<S> {
                 throw new 
IllegalArgumentException(Errors.format(Errors.Keys.MismatchedArrayLengths));
             }
             if (done) throw new IllegalStateException();
-            sources = ArraysExt.insert(sources, index+1, n-1);
+            sources        = ArraysExt.insert(sources,        index+1, n-1);
             bandsPerSource = ArraysExt.insert(bandsPerSource, index+1, n-1);
-            System.arraycopy(components, 0, sources, index, n);
+            System.arraycopy(components,     0, sources,        index, n);
             System.arraycopy(componentBands, 0, bandsPerSource, index, n);
             done = true;
         }
@@ -241,41 +274,39 @@ public final class MultiSourceArgument<S> {
      * @throws IllegalArgumentException if some band indices are duplicated or 
outside their range of validity.
      */
     public void validate(final ToIntFunction<S> counter) {
+        checkValidationState(false);
         validate(null, Objects.requireNonNull(counter));
     }
 
     /**
      * Clones and validates the arguments given to the constructor.
      * Also computes the union of bands in the sources given at construction 
time.
-     * The union result is stored in {@link #ranges}.
+     * The union result is stored in {@link #ranges()}.
      *
      * @param  getter  method to invoke for getting the list of sample 
dimensions.
      * @throws IllegalArgumentException if some band indices are duplicated or 
outside their range of validity.
      */
     public void validate(final Function<S, List<SampleDimension>> getter) {
+        checkValidationState(false);
         ranges = new ArrayList<>();
         validate(Objects.requireNonNull(getter), null);
     }
 
     /**
-     * Computes the union of bands in the sources given at construction time.
-     * This method also verifies the indices in band arguments.
-     * Sources with no indices are removed from the iterator.
+     * Clones and validates the arguments given to the constructor.
+     * This method ensures that all band indices are in their ranges of 
validity with no duplicated value.
+     * Then this method stores a copy of the band indices, replacing {@code 
null} values by sequences.
+     * If an empty array of bands is specified, then the corresponding source 
is omitted.
      *
-     * <p>Exactly one of {@code getter} or {@code count} arguments shall be 
non-null.</p>
+     * <p>Exactly one of {@code getter} or {@code counter} arguments shall be 
non-null.</p>
      *
      * @param  getter   method to invoke for getting the list of sample 
dimensions.
      * @param  counter  method to invoke for counting the number of bands in a 
source.
      * @throws IllegalArgumentException if some band indices are duplicated or 
outside their range of validity.
      */
     private void validate(final Function<S, List<SampleDimension>> getter, 
final ToIntFunction<S> counter) {
-        final HashMap<Integer,int[]> pool = identityAsNull ? null : new 
HashMap<>();
-        int filteredCount = 0;
-        /*
-         * This loop ensures that all band indices are in their ranges of 
validity
-         * with no duplicated value, then stores a copy of the band indices or 
null.
-         * If an empty array of bands is specified, then the source is omitted.
-         */
+        final HashMap<Integer,int[]> identityPool = new HashMap<>();
+        isIdentity = new BitSet();
 next:   for (int i=0; i<sources.length; i++) {          // `sources.length` 
may change during the loop.
             S source;
             int[] selected;
@@ -301,52 +332,120 @@ next:   for (int i=0; i<sources.length; i++) {          
// `sources.length` may
                 selected = range.getSelectedBands();
                 /*
                  * Verify if the source is a nested aggregation, in order to 
get a flattened view.
-                 * This replacement must be done before the optimization for 
consecutive images.
+                 * This replacement must be done before the check for 
duplicated image references.
+                 * The call to `unwrap` may result in a need to grow 
`numBandsPerSource` array.
                  */
             } while (unwrap(i, source, selected));
             /*
-             * Store now the sample dimensions before the `selected` array get 
modified.
-             * Should be done only after `RangeArgument.validate(…)` has been 
successful.
+             * Now that the arguments have been validated, overwrite the array 
elements.
+             * The new values may be written at an index lower than `i` if 
some empty
+             * sources have been excluded.
              */
+            if (validatedSourceCount >= numBandsPerSource.length) {
+                // Needed if `unwrap(source)` has expanded the sources array.
+                numBandsPerSource = Arrays.copyOf(numBandsPerSource, 
sources.length);
+            }
             if (ranges != null) {
-                for (int b : selected) {
-                    ranges.add(sourceBands.get(b));
+                for (int j : selected) {
+                    ranges.add(sourceBands.get(j));
                 }
             }
-            /*
-             * If the source in current iteration is the same than the 
previous source, merge the bands together.
-             * The `BandAggregateGridResource.read(…)` implementation relies 
on that optimization.
-             */
-            if (filteredCount > 0 && sources[filteredCount-1] == source) {
-                final int[] previous = bandsPerSource[--filteredCount];
-                ArgumentChecks.ensureNonNullElement("bandsPerSource", 
filteredCount,   previous);
-                ArgumentChecks.ensureNonNullElement("bandsPerSource", 
filteredCount+1, selected);
-                numBands -= previous.length;   // Rollback the value added in 
previous iteration.
-
-                final int[] merged = Arrays.copyOf(previous, previous.length + 
selected.length);
-                System.arraycopy(selected, 0, merged, previous.length, 
selected.length);
-                range = RangeArgument.validate(numSourceBands, merged, null);
-                selected = range.getSelectedBands();
-            }
-            /*
-             * Store a copy of the `bandsPerSource` argument given at 
construction time.
-             * Its validation has been done by `RangeArgument.validate(…)` 
above calls.
-             */
             if (range.isIdentity()) {
-                if (pool != null) {
-                    int[] previous = pool.putIfAbsent(numSourceBands, 
selected);
-                    if (previous != null) selected = previous;
-                } else {
-                    selected = null;
+                isIdentity.set(validatedSourceCount);
+                int[] previous = identityPool.putIfAbsent(numSourceBands, 
selected);
+                if (previous != null) selected = previous;
+            }
+            sources          [validatedSourceCount] = source;
+            bandsPerSource   [validatedSourceCount] = selected;
+            numBandsPerSource[validatedSourceCount] = numSourceBands;
+            totalBandCount += range.getNumBands();
+            validatedSourceCount++;
+        }
+    }
+
+    /**
+     * If the same sources are repeated many times, merges them in a single 
reference.
+     * The {@link #sources()} and {@link #bandsPerSource(boolean)} values are 
modified in-place.
+     * The bands associated to each source reference are merged together, but 
not necessarily in the same order.
+     * Caller must perform a "band select" operation using the array returned 
by this method
+     * in order to reconstitute the band order specified by the user.
+     *
+     * <h4>Use cases</h4>
+     * {@code BandAggregateImage.subset(…)} and
+     * {@code BandAggregateGridResource.read(…)}
+     * implementations rely on this optimization.
+     *
+     * @return the bands to specify in a "band select" operation for 
reconstituting the user-specified band order.
+     *         If all band selections are identity operations, then this 
method returns {@code null}.
+     */
+    public int[] mergeDuplicatedSources() {
+        checkValidationState(true);
+        if (isIdentity.cardinality() == validatedSourceCount) {
+            return null;
+        }
+        /*
+         * Merge together the bands of all sources that are repeated.
+         * The band indices are stored in 64 bits tuples as below:
+         *
+         *     (band in source) | (band in target aggregate)
+         */
+        final var mergedBands = new IdentityHashMap<S,long[]>();
+        int targetBand = 0;
+        for (int i=0; i<validatedSourceCount; i++) {
+            final int[] selected = bandsPerSource[i];
+            final long[] tuples = new long[selected.length];
+            for (int j=0; j<selected.length; j++) {
+                tuples[j] = Numerics.tuple(selected[j], targetBand++);
+            }
+            mergedBands.merge(sources[i], tuples, ArraysExt::concatenate);
+        }
+        /*
+         * Iterate again over the sources, rewriting the arrays with 
consolidated bands.
+         * We need to keep trace of how the bands were reordered.
+         */
+        final int[] reordered = new int[totalBandCount];
+        final int count = validatedSourceCount;
+        validatedSourceCount = 0;
+        targetBand = 0;
+        for (int i=0; i<count; i++) {
+            final S      source = sources[i];
+            final long[] tuples = mergedBands.remove(source);
+            if (tuples != null) {
+                boolean noop = isIdentity.get(i);
+                int[] selected = bandsPerSource[i];
+                if (tuples.length > selected.length) {
+                    /*
+                     * Found a case where the same source appears two ore more 
times.
+                     * Sort the bands in increasing order for making easier to 
detect
+                     * duplicated values, and because it increases the chances 
to get
+                     * an identity selection (bands in same order) for that 
source.
+                     */
+                    Arrays.sort(tuples);
+                    selected = new int[tuples.length];
+                    noop = (tuples.length == numBandsPerSource[i]) && 
ArraysExt.isRange(0, selected);
+                }
+                /*
+                 * Rewrite the `selected` array with the potentially merged 
bands.
+                 * If the source was not repeated, `selected` should be 
unchanged.
+                 * But we loop anyway because we also need to write 
`reordered`.
+                 */
+                for (int j=0; j < tuples.length; j++) {
+                    final long t = tuples[j];
+                    final int sourceBand = (int) (t >>> Integer.SIZE);
+                    reordered[(int) t] = sourceBand + targetBand;
+                    selected[j] = sourceBand;
                 }
+                targetBand += tuples.length;
+                bandsPerSource[validatedSourceCount] = selected;
+                isIdentity.set(validatedSourceCount, noop);
+                sources[validatedSourceCount++] = source;
             }
-            bandsPerSource[filteredCount] = selected;
-            sources[filteredCount++] = source;
-            numBands += range.getNumBands();
         }
-        sources = ArraysExt.resize(sources, filteredCount);
-        bandsPerSource = ArraysExt.resize(bandsPerSource, filteredCount);
-        validated = true;
+        final int n = isIdentity.length();
+        if (n > validatedSourceCount) {
+            isIdentity.clear(validatedSourceCount, n);
+        }
+        return reordered;
     }
 
     /**
@@ -355,7 +454,8 @@ next:   for (int i=0; i<sources.length; i++) {          // 
`sources.length` may
      * @return whether {@code sources[0]} could be used directly.
      */
     public boolean isIdentity() {
-        return bandsPerSource.length == 1 && bandsPerSource[0] == null;
+        checkValidationState(true);
+        return validatedSourceCount == 1 && isIdentity.cardinality() == 1;
     }
 
     /**
@@ -364,31 +464,38 @@ next:   for (int i=0; i<sources.length; i++) {          
// `sources.length` may
      *
      * @return all validated sources.
      */
-    @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public S[] sources() {
-        if (validated) return sources;
-        throw new IllegalStateException();
+        checkValidationState(true);
+        return sources = ArraysExt.resize(sources, validatedSourceCount);
     }
 
     /**
-     * Computes the intersection of the grid geometries of all sources.
-     * This method also verifies that all grid geometries are compatible.
-     *
-     * @param  getter  the method to invoke for getting grid geometry from a 
source.
-     * @return intersection of all grid geometries.
-     * @throws IllegalGridGeometryException if a grid geometry is not 
compatible with the others.
+     * Returns the indices of selected bands as (potentially modified)
+     * copies of the arrays argument given to the constructor.
      *
-     * @todo Current implementation requires that all grid geometry are equal. 
We need to relax that.
+     * @param  identityAsNull  whether to use {@code null} elements for 
meaning "all bands".
+     * @return indices of selected sample dimensions for each source.
+     *         Never null but may contain null elements if {@code 
identityAsNull} is {@code true}.
      */
-    public GridGeometry domain(final Function<S, GridGeometry> getter) {
-        GridGeometry intersection = getter.apply(sources[0]);
-        for (int i=1; i < sources.length; i++) {
-            if (!intersection.equals(getter.apply(sources[i]), 
ComparisonMode.IGNORE_METADATA)) {
-                throw new IllegalGridGeometryException("Not yet supported on 
coverages with different grid geometries.");
+    public int[][] bandsPerSource(final boolean identityAsNull) {
+        checkValidationState(true);
+        bandsPerSource = ArraysExt.resize(bandsPerSource, 
validatedSourceCount);
+        if (identityAsNull) {
+            for (int i=0; (i = isIdentity.nextSetBit(i)) >= 0; i++) {
+                bandsPerSource[i] = null;
             }
         }
-        sourceOfGridToCRS = 0;      // TODO: to be computed when different 
grid geometries will be allowed. Prefer widest extent.
-        return intersection;
+        return bandsPerSource;
+    }
+
+    /**
+     * Returns the total number of bands.
+     *
+     * @return total number of bands.
+     */
+    public int numBands() {
+        checkValidationState(true);
+        return totalBandCount;
     }
 
     /**
@@ -397,33 +504,31 @@ next:   for (int i=0; i<sources.length; i++) {          
// `sources.length` may
      *
      * @return all selected sample dimensions.
      */
-    @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public List<SampleDimension> ranges() {
         if (ranges != null) return ranges;
         throw new IllegalStateException();
     }
 
     /**
-     * Returns the total number of bands.
+     * Computes the intersection of the grid geometries of all sources.
+     * This method also verifies that all grid geometries are compatible.
      *
-     * @return total number of bands.
-     */
-    public int numBands() {
-        if (validated) return numBands;
-        throw new IllegalStateException();
-    }
-
-    /**
-     * Returns the indices of selected bands as (potentially modified)
-     * copies of the arrays argument given to the constructor.
+     * @param  getter  the method to invoke for getting grid geometry from a 
source.
+     * @return intersection of all grid geometries.
+     * @throws IllegalGridGeometryException if a grid geometry is not 
compatible with the others.
      *
-     * @return indices of selected sample dimensions for each source.
-     *         Never null but may contain null elements if {@link 
#identityAsNull()} has been invoked.
+     * @todo Current implementation requires that all grid geometry are equal. 
We need to relax that.
      */
-    @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public int[][] bandsPerSource() {
-        if (validated) return bandsPerSource;
-        throw new IllegalStateException();
+    public GridGeometry domain(final Function<S, GridGeometry> getter) {
+        checkValidationState(true);
+        GridGeometry intersection = getter.apply(sources[0]);
+        for (int i=1; i < validatedSourceCount; i++) {
+            if (!intersection.equals(getter.apply(sources[i]), 
ComparisonMode.IGNORE_METADATA)) {
+                throw new IllegalGridGeometryException("Not yet supported on 
coverages with different grid geometries.");
+            }
+        }
+        sourceOfGridToCRS = 0;      // TODO: to be computed when different 
grid geometries will be allowed. Prefer widest extent.
+        return intersection;
     }
 
     /**
@@ -433,6 +538,7 @@ next:   for (int i=0; i<sources.length; i++) {          // 
`sources.length` may
      * @return index of a sources having the same "grid to CRS" than the 
domain, or -1 if none.
      */
     public int sourceOfGridToCRS() {
+        checkValidationState(true);
         return sourceOfGridToCRS;
     }
 }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/RangeArgument.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/RangeArgument.java
index 78103751c2..3815f507ac 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/RangeArgument.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/RangeArgument.java
@@ -27,6 +27,7 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
 import org.apache.sis.internal.coverage.j2d.SampleModelFactory;
 import org.apache.sis.internal.feature.Resources;
+import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
@@ -97,7 +98,7 @@ public final class RangeArgument {
         if (ranges == null || ranges.length == 0) {
             packed = new long[numSampleDimensions];
             for (int i=1; i<numSampleDimensions; i++) {
-                packed[i] = (((long) i) << Integer.SIZE) | i;
+                packed[i] = Numerics.tuple(i, i);
             }
         } else {
             /*
@@ -110,7 +111,7 @@ public final class RangeArgument {
                     throw new 
IllegalArgumentException(resources(listeners).getString(
                             Resources.Keys.InvalidSampleDimensionIndex_2, 
numSampleDimensions - 1, r));
                 }
-                packed[i] = (((long) r) << Integer.SIZE) | i;
+                packed[i] = Numerics.tuple(r, i);
             }
             /*
              * Sort by increasing `range` value, but keep together with index 
in `ranges` where each
@@ -152,7 +153,7 @@ public final class RangeArgument {
             return false;
         }
         for (int i=0; i<packed.length; i++) {
-            if (packed[i] != ((((long) i) << Integer.SIZE) | i)) {
+            if (packed[i] != Numerics.tuple(i, i)) {
                 return false;
             }
         }
diff --git 
a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
 
b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
index c0b7adf5cf..f5ae9bd381 100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
@@ -442,9 +442,9 @@ public final class BandAggregateImageTest extends TestCase {
         result = BandAggregateImage.create(new RenderedImage[] {im1, result}, 
null, null, false, allowSharing, false);
         assertArrayEquals(sourceImages, ((BandAggregateImage) 
result).getSourceArray());
 
-        assertSame(im1, BandSelectImage.create(result, 0, 1, 2));
-        assertSame(im2, BandSelectImage.create(result, 3));
-        assertSame(im3, BandSelectImage.create(result, 4, 5));
+        assertSame(im1, BandSelectImage.create(result, true, 0, 1, 2));
+        assertSame(im2, BandSelectImage.create(result, true, 3));
+        assertSame(im3, BandSelectImage.create(result, true, 4, 5));
     }
 
     /**
@@ -458,6 +458,7 @@ public final class BandAggregateImageTest extends TestCase {
      *   <li><var>X</var> is the <var>x</var> coordinate (column 0-based 
index) of the sample value relative to current tile.</li>
      * </ol>
      */
+    @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
     private void initializeAllTiles(final TiledImageMock... images) {
         sourceImages = images;
         int band = 0;
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java 
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
index 7633317629..6b8a48982b 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
@@ -198,6 +198,19 @@ public final class Numerics extends Static {
         return (bit & ~(Long.SIZE - 1)) == 0 ? (1L << bit) : 0;
     }
 
+    /**
+     * Returns a 64 bits value made of the juxtaposition of the two given 32 
bits values.
+     * The resulting tuple can be decomposed back in the two 32 bits integer 
components with
+     * {@code (int) (tuple >>> Integer.SIZE)} for high part and {@code (int) 
tuple} for the low part.
+     *
+     * @param  hi   the 32 higher bits.
+     * @param  low  the 32 lower bits.
+     * @return the two given 32 bits integers juxtaposed in a 64 bits integer.
+     */
+    public static long tuple(final int hi, final int low) {
+        return (((long) hi) << Integer.SIZE) | Integer.toUnsignedLong(low);
+    }
+
     /**
      * Returns {@code true} if the given number is an integer value.
      * Special cases:
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
index ae973f5f6e..821ad8157f 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
@@ -67,7 +67,7 @@ import java.lang.reflect.Array;
  * objects.
  *
  * @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.4
  *
  * @see Arrays
  *
@@ -1184,6 +1184,91 @@ public final class ArraysExt extends Static {
         return copy;
     }
 
+    /**
+     * Returns the concatenation of all given arrays. This method performs the 
following checks:
+     *
+     * <ul>
+     *   <li>If the {@code arrays} argument is {@code null} or contains only 
{@code null}
+     *       elements, then this method returns {@code null}.</li>
+     *   <li>Otherwise if the {@code arrays} argument contains exactly one 
non-null array with
+     *       a length greater than zero, then that array is returned. It is 
not copied.</li>
+     *   <li>Otherwise a new array with a length equals to the sum of the 
length of every
+     *       non-null arrays is created, and the content of non-null arrays 
are appended
+     *       in the new array in declaration order.</li>
+     * </ul>
+     *
+     * @param  <T>     the type of arrays.
+     * @param  arrays  the arrays to concatenate, or {@code null}.
+     * @return the concatenation of all non-null arrays (may be a direct 
reference to one
+     *         of the given array if it can be returned with no change), or 
{@code null}.
+     *
+     * @see #append(Object[], Object)
+     * @see #unionOfSorted(int[], int[])
+     */
+    @SafeVarargs
+    public static <T> T[] concatenate(final T[]... arrays) {
+        T[] result = null;
+        if (arrays != null) {
+            int length = 0;
+            for (T[] array : arrays) {
+                if (array != null) {
+                    length += array.length;
+                }
+            }
+            int offset = 0;
+            for (T[] array : arrays) {
+                if (array != null) {
+                    if (result == null) {
+                        if (array.length == length) {
+                            return array;
+                        }
+                        result = Arrays.copyOf(array, length);
+                    } else {
+                        System.arraycopy(array, 0, result, offset, 
array.length);
+                    }
+                    offset += array.length;
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns the concatenation of the given arrays.
+     * If any of the supplied arrays is null or empty, then the other array is 
returned directly (not copied).
+     *
+     * @param  a1  the first array to concatenate, or {@code null}.
+     * @param  a2  the second array to concatenate, or {@code null}.
+     * @return the concatenation of given arrays. May be one of the given 
arrays returned without copying.
+     *
+     * @since 1.4
+     */
+    public static long[] concatenate(final long[] a1, final long[] a2) {
+        if (a1 == null || a1.length == 0) return a2;
+        if (a2 == null || a2.length == 0) return a1;
+        final long[] copy = Arrays.copyOf(a1, a1.length + a2.length);
+        System.arraycopy(a2, 0, copy, a1.length, a2.length);
+        return copy;
+    }
+
+    /**
+     * Returns the concatenation of the given arrays.
+     * If any of the supplied arrays is null or empty, then the other array is 
returned directly (not copied).
+     *
+     * @param  a1  the first array to concatenate, or {@code null}.
+     * @param  a2  the second array to concatenate, or {@code null}.
+     * @return the concatenation of given arrays. May be one of the given 
arrays returned without copying.
+     *
+     * @since 1.4
+     */
+    public static int[] concatenate(final int[] a1, final int[] a2) {
+        if (a1 == null || a1.length == 0) return a2;
+        if (a2 == null || a2.length == 0) return a1;
+        final int[] copy = Arrays.copyOf(a1, a1.length + a2.length);
+        System.arraycopy(a2, 0, copy, a1.length, a2.length);
+        return copy;
+    }
+
     /**
      * Removes the duplicated elements in the given array. This method should 
be invoked only for small arrays,
      * typically less than 10 distinct elements. For larger arrays, use {@link 
java.util.LinkedHashSet} instead.
@@ -2180,55 +2265,6 @@ public final class ArraysExt extends Static {
         return false;
     }
 
-    /**
-     * Returns the concatenation of all given arrays. This method performs the 
following checks:
-     *
-     * <ul>
-     *   <li>If the {@code arrays} argument is {@code null} or contains only 
{@code null}
-     *       elements, then this method returns {@code null}.</li>
-     *   <li>Otherwise if the {@code arrays} argument contains exactly one 
non-null array with
-     *       a length greater than zero, then that array is returned. It is 
not copied.</li>
-     *   <li>Otherwise a new array with a length equals to the sum of the 
length of every
-     *       non-null arrays is created, and the content of non-null arrays 
are appended
-     *       in the new array in declaration order.</li>
-     * </ul>
-     *
-     * @param  <T>     the type of arrays.
-     * @param  arrays  the arrays to concatenate, or {@code null}.
-     * @return the concatenation of all non-null arrays (may be a direct 
reference to one
-     *         of the given array if it can be returned with no change), or 
{@code null}.
-     *
-     * @see #append(Object[], Object)
-     * @see #unionOfSorted(int[], int[])
-     */
-    @SafeVarargs
-    public static <T> T[] concatenate(final T[]... arrays) {
-        T[] result = null;
-        if (arrays != null) {
-            int length = 0;
-            for (T[] array : arrays) {
-                if (array != null) {
-                    length += array.length;
-                }
-            }
-            int offset = 0;
-            for (T[] array : arrays) {
-                if (array != null) {
-                    if (result == null) {
-                        if (array.length == length) {
-                            return array;
-                        }
-                        result = Arrays.copyOf(array, length);
-                    } else {
-                        System.arraycopy(array, 0, result, offset, 
array.length);
-                    }
-                    offset += array.length;
-                }
-            }
-        }
-        return result;
-    }
-
     /**
      * Returns the union of two sorted arrays. The input arrays shall be 
sorted in strictly increasing order.
      * The output array is the union of the input arrays without duplicated 
values,
diff --git 
a/core/sis-utility/src/test/java/org/apache/sis/util/ArraysExtTest.java 
b/core/sis-utility/src/test/java/org/apache/sis/util/ArraysExtTest.java
index 420326aa06..c316494fe5 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/util/ArraysExtTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/util/ArraysExtTest.java
@@ -27,10 +27,40 @@ import static org.junit.Assert.*;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @version 1.4
  * @since   0.3
  */
 public final class ArraysExtTest extends TestCase {
+    /**
+     * Tests {@link ArraysExt#concatenate(Object[]...)}.
+     */
+    @Test
+    public void testConcatenate() {
+        final Integer[] a1 = {2, 8, 4, 8};
+        final Integer[] a2 = {1, 2, 8};
+        assertArrayEquals(new Integer[] {2, 8, 4, 8, 1, 2, 8}, 
ArraysExt.concatenate(a1, a2));
+    }
+
+    /**
+     * Tests {@link ArraysExt#concatenate(long[], long[])}.
+     */
+    @Test
+    public void testConcatenateLong() {
+        final long[] a1 = {2, 8, 4, 8};
+        final long[] a2 = {1, 2, 8};
+        assertArrayEquals(new long[] {2, 8, 4, 8, 1, 2, 8}, 
ArraysExt.concatenate(a1, a2));
+    }
+
+    /**
+     * Tests {@link ArraysExt#concatenate(int[], int[])}.
+     */
+    @Test
+    public void testConcatenateInt() {
+        final int[] a1 = {2, 8, 4, 8};
+        final int[] a2 = {1, 2, 8};
+        assertArrayEquals(new int[] {2, 8, 4, 8, 1, 2, 8}, 
ArraysExt.concatenate(a1, a2));
+    }
+
     /**
      * Tests {@link ArraysExt#removeDuplicated(Object[])}.
      */
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 f548c4289d..d32e47d19a 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
@@ -133,8 +133,7 @@ public class BandAggregateGridResource extends 
AbstractGridCoverageResource {
      * <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.
-     * In the latter case, the discarded element in the {@code sources} array 
may be {@code null}.</p>
+     * An empty array element (i.e. zero band to select) discards the 
corresponding resource.</p>
      *
      * <h4>Restrictions</h4>
      * All resources shall have compatible domain, defined as below:
@@ -169,7 +168,7 @@ public class BandAggregateGridResource extends 
AbstractGridCoverageResource {
             this.sources          = aggregate.sources();
             this.gridGeometry     = 
aggregate.domain(BandAggregateGridResource::domain);
             this.sampleDimensions = List.copyOf(aggregate.ranges());
-            this.bandsPerSource   = aggregate.bandsPerSource();
+            this.bandsPerSource   = aggregate.bandsPerSource(false);
             this.processor        = (processor != null) ? processor : new 
GridCoverageProcessor();
         } catch (BackingStoreException e) {
             throw e.unwrapOrRethrow(DataStoreException.class);

Reply via email to