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/10 17:16:24 UTC

[sis] branch geoapi-4.0 updated (ed74a09dc9 -> e172934869)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


    from ed74a09dc9 When doing an aggregation of "band select", verify if the operations cancel each other.
     new 5f36de44f4 "Band select" on a band aggregation should be able to return the original component. Aggregation of aggregations should use a flattened list or source images.
     new e172934869 Spelling fixes in documentation.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../apache/sis/gui/coverage/CoverageCanvas.java    |   2 +-
 .../sis/coverage/grid/DimensionalityReduction.java |   4 +-
 .../org/apache/sis/image/BandAggregateImage.java   |  92 ++++++++--
 .../java/org/apache/sis/image/BandSelectImage.java |  47 +++--
 .../main/java/org/apache/sis/image/Colorizer.java  |   2 +-
 .../java/org/apache/sis/image/ComputedImage.java   |   2 +-
 .../java/org/apache/sis/image/ImageProcessor.java  |   6 +-
 .../org/apache/sis/image/MultiSourceImage.java     |   4 +-
 .../org/apache/sis/image/MultiSourceLayout.java    |   6 +-
 .../java/org/apache/sis/image/Visualization.java   |   4 +-
 .../sis/internal/coverage/MultiSourceArgument.java | 190 ++++++++++++++-------
 .../sis/internal/coverage/RangeArgument.java       |   2 +-
 .../internal/coverage/j2d/ColorModelFactory.java   |   6 +-
 .../sis/internal/coverage/j2d/ColorsForRange.java  |   2 +-
 .../apache/sis/image/BandAggregateImageTest.java   |  54 ++++--
 .../org/apache/sis/image/BandSelectImageTest.java  |  14 ++
 .../operation/transform/PassThroughTransform.java  |   2 +-
 .../operation/transform/TransformSeparator.java    |   2 +-
 .../main/java/org/apache/sis/util/ArraysExt.java   |   2 +-
 .../aggregate/BandAggregateGridResource.java       |   6 +-
 20 files changed, 325 insertions(+), 124 deletions(-)


[sis] 02/02: Spelling fixes in documentation.

Posted by de...@apache.org.
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 e172934869f89fb1d2fafa96bf0c0e75c921a98c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Apr 10 18:36:16 2023 +0200

    Spelling fixes in documentation.
---
 .../src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java   | 2 +-
 .../java/org/apache/sis/coverage/grid/DimensionalityReduction.java  | 4 ++--
 .../src/main/java/org/apache/sis/image/BandAggregateImage.java      | 4 ++--
 .../src/main/java/org/apache/sis/image/BandSelectImage.java         | 2 +-
 core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java  | 2 +-
 .../src/main/java/org/apache/sis/image/ComputedImage.java           | 2 +-
 .../src/main/java/org/apache/sis/image/ImageProcessor.java          | 4 ++--
 .../src/main/java/org/apache/sis/image/MultiSourceLayout.java       | 2 +-
 .../src/main/java/org/apache/sis/image/Visualization.java           | 4 ++--
 .../java/org/apache/sis/internal/coverage/MultiSourceArgument.java  | 4 ++--
 .../main/java/org/apache/sis/internal/coverage/RangeArgument.java   | 2 +-
 .../org/apache/sis/internal/coverage/j2d/ColorModelFactory.java     | 6 +++---
 .../java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java   | 2 +-
 .../src/test/java/org/apache/sis/image/BandAggregateImageTest.java  | 6 +++---
 .../sis/referencing/operation/transform/PassThroughTransform.java   | 2 +-
 .../sis/referencing/operation/transform/TransformSeparator.java     | 2 +-
 .../org/apache/sis/storage/aggregate/BandAggregateGridResource.java | 6 +++---
 17 files changed, 28 insertions(+), 28 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index 5cfe75de75..893b228927 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -413,7 +413,7 @@ public class CoverageCanvas extends MapCanvasAWT {
      * Sets the colorization algorithm to apply on rendered images.
      * Should be an algorithm based on coverage categories.
      *
-     * <p>{@code CoverageCanvas} can not detect when the given colorizer changes its internal state.
+     * <p>{@code CoverageCanvas} cannot detect when the given colorizer changes its internal state.
      * The {@link #stylingChanged()} method should be invoked explicitly when such change occurs.</p>
      *
      * @param colorizer colorization algorithm to apply on computed image, or {@code null} for default.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java
index a6dc48d28c..517edb4390 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionalityReduction.java
@@ -225,7 +225,7 @@ public class DimensionalityReduction implements UnaryOperator<GridCoverage>, Ser
     /**
      * Returns all CRS components for the dimensions where the bit is set.
      * There is one CRS for each range of consecutive dimension indices.
-     * If at least one CRS can not be fetched, then this method returns {@code null}.
+     * If at least one CRS cannot be fetched, then this method returns {@code null}.
      *
      * @param  crs   the CRS for which to get components.
      * @param  axes  dimensions (or axis indices) of the components to get.
@@ -614,7 +614,7 @@ public class DimensionalityReduction implements UnaryOperator<GridCoverage>, Ser
     }
 
     /**
-     * Returns {@code true} if the {@code actual} CRS is equals, ignore metadata, to the one in {@code expected}.
+     * Returns {@code true} if the {@code actual} CRS is equal, ignore metadata, to the one in {@code expected}.
      * If any CRS is null, this method conservatively returns {@code true}.
      * This is used for assertions only.
      */
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 f1d06ca4b4..255176dcc8 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
@@ -58,7 +58,7 @@ class BandAggregateImage extends MultiSourceImage {
      */
 
     /**
-     * Returns potentially deeper sources than the user-supplied image.
+     * Returns potentially deeper sources than the user supplied image.
      * This method unwraps {@link BandSelectImage} for making possible to detect that two
      * consecutive images are actually the same image, with only different bands selected.
      *
@@ -194,7 +194,7 @@ class BandAggregateImage extends MultiSourceImage {
             }
         }
         /*
-         * Fallback when the data arrays can not be shared.
+         * Fallback when the data arrays cannot be shared.
          * This code copies all sample values in new arrays.
          */
         if (tile == null) {
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 7dfd7c1279..2b126c5d28 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
@@ -106,7 +106,7 @@ class BandSelectImage extends SourceAlignedImage {
      *
      * @param  source  the image in which to select bands.
      * @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.
+     *                 If that array instance was user supplied, then it should be cloned by caller.
      */
     static RenderedImage create(RenderedImage source, int... bands) {
         final int numBands = ImageUtilities.getNumBands(source);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
index e1f1d7a937..d7ee7fb9c7 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
@@ -329,7 +329,7 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode
 
     /**
      * Returns a new colorizer which will apply the specified alternative
-     * if this colorizer can not infer a color model.
+     * if this colorizer cannot infer a color model.
      *
      * @param  alternative  the alternative strategy for creating a color model.
      * @return a new colorizer which will attempt to apply {@code this} first,
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
index 792bb06d08..fa024899ef 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
@@ -263,7 +263,7 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
     }
 
     /**
-     * Ensures that a user-supplied color model is compatible with the sample model.
+     * Ensures that a user supplied color model is compatible with the sample model.
      * This is a helper method for argument validation in sub-classes constructors.
      *
      * @param  colors  the color model to validate. Can be {@code 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 ad9b1fee78..a6149ea3f0 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
@@ -395,7 +395,7 @@ public class ImageProcessor implements Cloneable {
      * Consequently the colorizer is not invoked for that operation.</p>
      *
      * <p>But by contrast, the color model of an {@link #aggregateBands(RenderedImage...) aggregateBands(…)}
-     * operation can not be determined in such straightforward way.
+     * operation cannot be determined in such straightforward way.
      * If three or four bands are aggregated, should they be interpreted as an (A)RGB image?
      * The {@link Colorizer} allows to specify the desired behavior.</p>
      *
@@ -1325,7 +1325,7 @@ public class ImageProcessor implements Cloneable {
      *
      * <h4>How to specify colors</h4>
      * The image colors can be controlled by the {@link Colorizer} set on this image processor.
-     * It is possible to {@linkplain Colorizer#forInstance(ColorModel) specify explicitely} the
+     * It is possible to {@linkplain Colorizer#forInstance(ColorModel) specify explicitly} the
      * {@link ColorModel} to use, but this approach is unsafe because it depends on the pixel values
      * <em>after</em> their conversion to the visualization image, which is implementation dependent.
      * A safer approach is to define colors relative to pixel values <em>before</em> their conversions.
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 7ce6ba198b..415915c93e 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
@@ -359,7 +359,7 @@ final class MultiSourceLayout extends ImageLayout {
      * then the returned color model will reuse the colors of that visible band.
      * Otherwise a grayscale color model is built with a value range inferred from the data-type.
      *
-     * @param  colorizer  user-supplied provider of color model, or {@code null} if none.
+     * @param  colorizer  user supplied provider of color model, or {@code null} if none.
      */
     final ColorModel createColorModel(final Colorizer colorizer) {
         ColorModel colors = null;
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 c7c44fa529..1b86bdd292 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
@@ -279,7 +279,7 @@ final class Visualization extends ResampledImage {
             }
             /*
              * Sample values will be unconditionally converted to integers in the [0 … 255] range.
-             * The sample model is a mandatory argument before we invoke user-supplied colorizer,
+             * The sample model is a mandatory argument before we invoke user supplied colorizer,
              * which must be done before to build the color model.
              */
             sampleModel = layout.createBandedSampleModel(ColorModelBuilder.TYPE_COMPACT, NUM_BANDS, source, bounds, 0);
@@ -320,7 +320,7 @@ final class Visualization extends ResampledImage {
                     builder.rescaleMainRange(sourceCM);
                 } else {
                     /*
-                     * At this point there is no more user-supplied colors (through `Colorizer`) that we can use.
+                     * At this point there is no more user supplied colors (through `Colorizer`) that we can use.
                      * If we have not been able to use the SampleDimension, try to use the ColorModel or SampleModel.
                      * There is no call to `rescaleMainRange(…)` because the following code already uses the range
                      * specified by the ColorModel, if available.
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 6685ed04a8..51318aafec 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
@@ -178,7 +178,7 @@ public final class MultiSourceArgument<S> {
     }
 
     /**
-     * Replace a user-supplied source by a deeper source with the bands to select.
+     * Replace a user supplied source by a deeper source with the bands to select.
      * This is used for getting a flattened view of nested aggregations.
      */
     public final class Unwrapper {
@@ -204,7 +204,7 @@ public final class MultiSourceArgument<S> {
         private boolean done;
 
         /**
-         * Creates a new instance to be submitted to user-supplied {@link #unwrapper}.
+         * Creates a new instance to be submitted to user supplied {@link #unwrapper}.
          */
         private Unwrapper(final int index, final S source, final int[] bands) {
             this.index  = index;
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 e2ddedbda0..78103751c2 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
@@ -366,7 +366,7 @@ public final class RangeArgument {
 
     /**
      * Returns a color model for the bands specified by the user.
-     * This method may return {@code null} if the color model can not be created.
+     * This method may return {@code null} if the color model cannot be created.
      *
      * @param  colors  the original color model with all bands. Can be {@code null}.
      * @return the color model for a subset of bands, or null if the given color model was null.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
index 631d6663d2..108bada84f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
@@ -597,7 +597,7 @@ public final class ColorModelFactory {
     /**
      * Creates a RGB color model for the given sample model.
      * The sample model shall use integer type and have 3 or 4 bands.
-     * This method may return {@code null} if the color model can not be created.
+     * This method may return {@code null} if the color model cannot be created.
      *
      * @param  model  the sample model for which to create a color model.
      * @return the color model, or null if a precondition does not hold.
@@ -666,7 +666,7 @@ public final class ColorModelFactory {
      *   <li>Input color model is recognized, but we cannot infer a proper color interpretation for given number of bands.</li>
      * </ul>
      *
-     * This method may return {@code null} if the color model can not be created.
+     * This method may return {@code null} if the color model cannot be created.
      *
      * <p><em>Note about {@link PackedColorModel} and {@link DirectColorModel}</em>:
      * those color models not managed for now, because they are really designed for
@@ -700,7 +700,7 @@ public final class ColorModelFactory {
      * Returns a color model with with the same colors but a different number of bands.
      * Current implementation supports {@link ComponentColorModel} with only one band,
      * {@link MultiBandsIndexColorModel} and {@link ScaledColorModel}.
-     * This method may return {@code null} if the color model can not be created.
+     * This method may return {@code null} if the color model cannot be created.
      *
      * @param  cm           the color model, or {@code null}.
      * @param  numBands     new number of bands.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
index d40c20196b..39c8d91d47 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
@@ -137,7 +137,7 @@ final class ColorsForRange implements Comparable<ColorsForRange> {
      * <p>If no colors were explicitly defined but a fallback exists, then this method considers
      * this range as defined for allowing {@link ColorModelBuilder} to inherit those colors with
      * the range of values specified by {@link #originalSampleRange}. We conceptually accept any
-     * {@link #inheritedColors} even if {@link #toARGB(int)} can not handle all of them.</p>
+     * {@link #inheritedColors} even if {@link #toARGB(int)} cannot handle all of them.</p>
      */
     final boolean isUndefined() {
         return colors == null && inheritedColors == null;
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 144510cee8..c0b7adf5cf 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
@@ -250,7 +250,7 @@ public final class BandAggregateImageTest extends TestCase {
             expected[119] = 250;
             assertSame(target, result.getTile(tileX, tileY));
             assertArrayEquals(expected, result.getData().getPixels(minX, minY, width, height, (int[]) null));
-            return;             // Can not continue the tests because the source images have been modified.
+            return;             // Cannot continue the tests because the source images have been modified.
         }
         /*
          * Repeat the test with a custom band selection.
@@ -302,7 +302,7 @@ public final class BandAggregateImageTest extends TestCase {
 
     /**
      * Tests the aggregation of three tiled images having different tile matrices.
-     * A copy of sample values can not be avoided in this case.
+     * A copy of sample values cannot be avoided in this case.
      */
     @Test
     @DependsOnMethod("testSimilarlyTiledImages")
@@ -351,7 +351,7 @@ public final class BandAggregateImageTest extends TestCase {
 
     /**
      * Tests the aggregation of three tiled images having different extents and different tile matrices.
-     * A copy of sample values can not be avoided in this case, except on the second image.
+     * A copy of sample values cannot be avoided in this case, except on the second image.
      */
     @Test
     @DependsOnMethod("testImagesUsingSameExtentButDifferentTileSizes")
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
index 1ff924203f..aea2fa1455 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
@@ -270,7 +270,7 @@ public class PassThroughTransform extends AbstractMathTransform implements Seria
             if (subLower == 0 && subUpper == subDim) {
                 step = subTransform;
             } else {
-                // Restriction below apply only if the `subTransform` can not be used as a whole.
+                // Restriction below apply only if the `subTransform` cannot be used as a whole.
                 ArgumentChecks.ensureDimensionsMatch("subTransform", subDim, subDim, subTransform);
                 sep.addSourceDimensionRange(subLower, subUpper);
                 sep.addTargetDimensionRange(subLower, subUpper);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
index 67616b04fc..fe0495f17f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/TransformSeparator.java
@@ -599,7 +599,7 @@ public class TransformSeparator {
              * If all source dimensions not in the sub-transform are consecutive numbers, we can use our passthrough
              * transform implementation. The "consecutive numbers" requirement (expressed in the `if` statement below)
              * is a consequence of a limitation in our current implementation: our current passthrough transform does
-             * not accept arbitrary indices for modified coordinates. We can not delegate to the static factory method
+             * not accept arbitrary indices for modified coordinates. We cannot delegate to the static factory method
              * `MathTransforms.passThrough(int[] modifiedCoordinates, ...)` because that method itself relies on this
              * `TransformSeparator` for separating the transform components at non-consecutive indices.
              */
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 52e0f8b141..f548c4289d 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
@@ -205,7 +205,7 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource {
      * This is the name specified at construction time.
      *
      * @return an identifier for the band aggregation.
-     * @throws DataStoreException if the identifier can not be obtained.
+     * @throws DataStoreException if the identifier cannot be obtained.
      */
     @Override
     public Optional<GenericName> getIdentifier() throws DataStoreException {
@@ -217,7 +217,7 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource {
      * This grid geometry is inferred from the grid geometries of resources specified at construction time.
      *
      * @return extent of grid coordinates together with their mapping to "real world" coordinates.
-     * @throws DataStoreException if the grid geometry can not be obtained.
+     * @throws DataStoreException if the grid geometry cannot be obtained.
      */
     @Override
     public GridGeometry getGridGeometry() throws DataStoreException {
@@ -229,7 +229,7 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource {
      * This is the union or a subset of the union of the ranges of all resources specified at construction time.
      *
      * @return ranges of sample values together with their mapping to "real values".
-     * @throws DataStoreException if the sample dimensions can not be obtained.
+     * @throws DataStoreException if the sample dimensions cannot be obtained.
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")


[sis] 01/02: "Band select" on a band aggregation should be able to return the original component. Aggregation of aggregations should use a flattened list or source images.

Posted by de...@apache.org.
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 5f36de44f40febbc4c8f4b08a92427f63d49fc9a
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Apr 10 18:33:18 2023 +0200

    "Band select" on a band aggregation should be able to return the original component.
    Aggregation of aggregations should use a flattened list or source images.
---
 .../org/apache/sis/image/BandAggregateImage.java   |  90 ++++++++--
 .../java/org/apache/sis/image/BandSelectImage.java |  47 +++--
 .../java/org/apache/sis/image/ImageProcessor.java  |   2 +-
 .../org/apache/sis/image/MultiSourceImage.java     |   4 +-
 .../org/apache/sis/image/MultiSourceLayout.java    |   4 +-
 .../sis/internal/coverage/MultiSourceArgument.java | 190 ++++++++++++++-------
 .../apache/sis/image/BandAggregateImageTest.java   |  48 +++++-
 .../org/apache/sis/image/BandSelectImageTest.java  |  14 ++
 .../main/java/org/apache/sis/util/ArraysExt.java   |   2 +-
 9 files changed, 301 insertions(+), 100 deletions(-)

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 2e672a1bb1..f1d06ca4b4 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
@@ -25,6 +25,7 @@ import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.MultiSourceArgument;
 
 
 /**
@@ -51,20 +52,92 @@ class BandAggregateImage extends MultiSourceImage {
      */
     private final boolean allowSharing;
 
+    /*
+     * The method declaration order below is a little bit unusual,
+     * but it follows an execution order.
+     */
+
+    /**
+     * Returns potentially deeper sources than the user-supplied image.
+     * This method unwraps {@link BandSelectImage} for making possible to detect that two
+     * consecutive images are actually the same image, with only different bands selected.
+     *
+     * @param  unwrapper  a handler where to supply the result of an aggregate decomposition.
+     */
+    static void unwrap(final MultiSourceArgument<RenderedImage>.Unwrapper unwrapper) {
+        RenderedImage source = unwrapper.source;
+        int[] bands = unwrapper.bands;
+        while (source instanceof ImageAdapter) {
+            source = ((ImageAdapter) source).source;
+        }
+        if (source instanceof BandSelectImage) {
+            final var select = (BandSelectImage) source;
+            bands  = select.getSourceBands(bands);
+            source = select.getSource();
+        }
+        if (source instanceof BandAggregateImage) {
+            ((BandAggregateImage) source).subset(bands, null, unwrapper);
+        } else if (source != unwrapper.source) {
+            unwrapper.apply(new RenderedImage[] {source}, new int[][] {bands});
+        }
+    }
+
+    /**
+     * Decomposes this aggregate for the specified subset of bands.
+     * The result can be used either for creating a new aggregate,
+     * or consumed by {@code unwrapper} for flattening an aggregation.
+     *
+     * <p>This is a kind of constructor, but for an image derived from this instance.
+     * The returned image may be one of the source images for simplifying the result.</p>
+     *
+     * @param  bands      the bands to keep.
+     * @param  colors     the colors to apply, or {@code null} if unspecified.
+     * @param  unwrapper  where to provide decomposition result, or {@code null} for creating the image immediately.
+     * @return an image with a subset of the bands of this image, or {@code null} if {@code unwrapper} was non-null.
+     */
+    final RenderedImage subset(final int[] bands, final ColorModel colors,
+            final MultiSourceArgument<RenderedImage>.Unwrapper unwrapper)
+    {
+        final RenderedImage[] sources = new RenderedImage[bands.length];
+        final int[][] bandsPerSource = new int[bands.length][];
+        int lower=0, upper=0, sourceIndex = -1;
+        RenderedImage source = null;
+        for (int i=0; i<bands.length; i++) {
+            final int band = bands[i];
+            if (band < lower) {
+                lower = upper = 0;
+                sourceIndex = -1;
+            }
+            while (band >= upper) {
+                source = getSource(++sourceIndex);
+                lower  = upper;
+                upper += ImageUtilities.getNumBands(source);
+            }
+            sources[i] = source;
+            bandsPerSource[i] = new int[] {band - lower};
+        }
+        if (unwrapper != null) {
+            unwrapper.apply(sources, bandsPerSource);
+            return null;
+        }
+        return create(sources, bandsPerSource, (colors != null) ? Colorizer.forInstance(colors) : null, false, allowSharing, parallel);
+    }
+
     /**
      * Creates a new aggregation of bands.
      *
      * @param  sources         images to combine, in order.
      * @param  bandsPerSource  bands to use for each source image, in order. May contain {@code null} elements.
      * @param  colorizer       provider of color model to use for this image, or {@code null} for automatic.
+     * @param  forceColors     whether to force application of {@code colorizer} when a source image is returned.
      * @param  allowSharing    whether to allow the sharing of data buffers (instead of copying) if possible.
      * @param  parallel        whether parallel computation is allowed.
      * @throws IllegalArgumentException if there is an incompatibility between some source images
      *         or if some band indices are duplicated or outside their range of validity.
      * @return the band aggregate image.
      */
-    static RenderedImage create(final RenderedImage[] sources, final int[][] bandsPerSource,
-                                final Colorizer colorizer, final boolean allowSharing, final boolean parallel)
+    static RenderedImage create(final RenderedImage[] sources, final int[][] bandsPerSource, final Colorizer colorizer,
+                                final boolean forceColors, final boolean allowSharing, final boolean parallel)
     {
         final var layout = MultiSourceLayout.create(sources, bandsPerSource, allowSharing);
         final BandAggregateImage image;
@@ -74,16 +147,13 @@ class BandAggregateImage extends MultiSourceImage {
             image = new BandAggregateImage(layout, colorizer, allowSharing, parallel);
         }
         if (image.getNumSources() == 1) {
-            final RenderedImage c = image.getSource();
-            if (image.colorModel == null) {
-                return c;
-            }
-            final ColorModel cm = c.getColorModel();
-            if (cm == null || image.colorModel.equals(cm)) {
-                return c;
+            RenderedImage source = image.getSource();
+            if ((forceColors && colorizer != null)) {
+                source = RecoloredImage.applySameColors(source, image);
             }
+            return source;
         }
-        return image;
+        return ImageProcessor.unique(image);
     }
 
     /**
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 f8fb8bc318..7dfd7c1279 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
@@ -85,32 +85,28 @@ class BandSelectImage extends SourceAlignedImage {
     }
 
     /**
-     * If the given image is already a band select operation, returns the original source
-     * and updates the band indices. If there is no replacement, then the {@code image}
-     * argument is returned as-is and the {@code bands} array shall be unmodified.
+     * Returns the indices of bands in the source image for the given bands in this image.
+     * A reference to the given array will be returned if the band indices are the same.
      *
-     * @param  image  the image to check.
-     * @param  bands  the band to select in the specified source.
-     *                Will be updated in-place if the source is replaced.
-     * @return the source of the image, or {@code image} if no replacement.
+     * @param  bands  the band to select in this image.
+     * @return the bands to select in source image.
+     *
+     * @see #getSource()
      */
-    static RenderedImage unwrap(final RenderedImage image, final int[] bands) {
-        if (image instanceof BandSelectImage) {
-            final var select = (BandSelectImage) image;
-            for (int i=0; i<bands.length; i++) {
-                bands[i] = select.bands[bands[i]];
-            }
-            return select.getSource();
+    final int[] getSourceBands(final int[] subset) {
+        final int[] select = new int[subset.length];
+        for (int i=0; i<subset.length; i++) {
+            select[i] = bands[subset[i]];
         }
-        return image;
+        return Arrays.equals(subset, select) ? subset : select;
     }
 
     /**
      * Creates a new "band select" operation for the given source.
      *
      * @param  source  the image in which to select bands.
-     * @param  bands   the bands to select. Shall be a clone of user-specified argument
-     *                 because it may be modified in-place.
+     * @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) {
         final int numBands = ImageUtilities.getNumBands(source);
@@ -118,8 +114,23 @@ class BandSelectImage extends SourceAlignedImage {
             return source;
         }
         ArgumentChecks.ensureNonEmptyBounded("bands", false, 0, numBands - 1, bands);
-        source = unwrap(source, bands);
         final ColorModel cm = ColorModelFactory.createSubset(source.getColorModel(), bands);
+        /*
+         * Since this operation applies its own ColorModel anyway, skip operation that was doing nothing else
+         * than changing the color model. Operations adding properties such as stastics are kept because this
+         * class can inherit some of them (see `REDUCED_PROPERTIES`).
+         */
+        if (source instanceof RecoloredImage) {
+            source = ((RecoloredImage) source).source;
+        }
+        if (source instanceof BandSelectImage) {
+            final var select = (BandSelectImage) source;
+            bands  = select.getSourceBands(bands);
+            source = select.getSource();
+        }
+        if (source instanceof BandAggregateImage) {
+            return ((BandAggregateImage) source).subset(bands, cm, null);
+        }
         /*
          * If the image is an instance of `BufferedImage`, create the subset immediately
          * (reminder: this operation will not copy pixel data). It allows us to return a
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 eebd30af21..ad9b1fee78 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
@@ -1004,7 +1004,7 @@ public class ImageProcessor implements Cloneable {
             colorizer = this.colorizer;
             parallel = executionMode != Mode.SEQUENTIAL;
         }
-        return unique(BandAggregateImage.create(sources, bandsPerSource, colorizer, true, parallel));
+        return BandAggregateImage.create(sources, bandsPerSource, colorizer, true, true, parallel);
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java
index 743e1580aa..9fd47dc9c1 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java
@@ -44,7 +44,7 @@ abstract class MultiSourceImage extends WritableComputedImage {
      *
      * @see #getColorModel()
      */
-    protected final ColorModel colorModel;
+    private final ColorModel colorModel;
 
     /**
      * Domain of pixel coordinates. All images shall share the same pixel coordinate space,
@@ -63,7 +63,7 @@ abstract class MultiSourceImage extends WritableComputedImage {
     /**
      * Whether parallel computation is allowed.
      */
-    private final boolean parallel;
+    final boolean parallel;
 
     /**
      * Creates a new multi-sources image.
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 f649382dfe..7ce6ba198b 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
@@ -129,7 +129,7 @@ final class MultiSourceLayout extends ImageLayout {
     static MultiSourceLayout create(RenderedImage[] sources, int[][] bandsPerSource, boolean allowSharing) {
         final var aggregate = new MultiSourceArgument<RenderedImage>(sources, bandsPerSource);
         aggregate.identityAsNull();
-        aggregate.unwrap(BandSelectImage::unwrap);
+        aggregate.unwrap(BandAggregateImage::unwrap);
         aggregate.validate(ImageUtilities::getNumBands);
 
         sources            = aggregate.sources();
@@ -242,7 +242,7 @@ final class MultiSourceLayout extends ImageLayout {
             RenderedImage source = sources[i];
             final int[] bands = bandsPerSource[i];
             if (bands != null) {
-                source = BandSelectImage.create(source, bands.clone());
+                source = BandSelectImage.create(source, bands);
             }
             filteredSources[i] = source;
         }
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 be9fa9b6fb..6685ed04a8 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
@@ -21,8 +21,8 @@ import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Objects;
+import java.util.function.Consumer;
 import java.util.function.Function;
-import java.util.function.BiFunction;
 import java.util.function.ToIntFunction;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -41,8 +41,8 @@ import org.apache.sis.util.ComparisonMode;
  * <p>Instances of this class should be short-lived.
  * They are used only the time needed for constructing an image or coverage operation.</p>
  *
- * <p>This class can optionally verify if a source is itself an aggregated image or coverage.
- * This is done by an "unwrapper", which should be specified in order to provide a flattened
+ * <p>This class can optionally verify if some sources are themselves aggregated images or coverages.
+ * This is done by an {@link #unwrap(Consumer)}, which should be invoked in order to get a flattened
  * view of nested aggregations.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -54,18 +54,19 @@ import org.apache.sis.util.ComparisonMode;
  */
 public final class MultiSourceArgument<S> {
     /**
-     * The sources of sample dimensions with empty sources removed.
-     * After a {@code validate(…)} method has been invoked, this array become a
-     * (potentially modified) copy of the array argument given to the constructor.
+     * 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.
      */
     private S[] sources;
 
     /**
-     * Indices of selected sample dimensions for each source.
-     * After a {@code validate(…)} method has been invoked, this array become a
-     * (potentially modified) copy of the array argument given to the constructor,
-     * with the same length than {@link #sources} and all elements themselves copied.
+     * Indices of selected bands or sample dimensions for each source.
+     * 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.
      */
     private int[][] bandsPerSource;
 
@@ -75,11 +76,9 @@ public final class MultiSourceArgument<S> {
     private boolean identityAsNull;
 
     /**
-     * A function which, given an (image, bands) pair, may return the source of the image.
-     * If the source is returned, then the bands array is updated with the indices in that
-     * source.
+     * A method which may decompose a source in a sequence of deeper sources associated with their bands to select.
      */
-    private BiFunction<S,int[],S> unwrapper;
+    private Consumer<Unwrapper> unwrapper;
 
     /**
      * Union of all selected bands in all specified sources, or {@code null} if not applicable.
@@ -105,15 +104,36 @@ public final class MultiSourceArgument<S> {
 
     /**
      * Prepares an argument validator for the given sources and bands arguments.
-     * One of the {@code validate(…)} method should be invoked after this constructor.
+     * The optional {@code bandsPerSource} argument specifies the bands to select in each source images.
+     * That array can be {@code null} for selecting all bands in all source images,
+     * or may contain {@code null} elements for selecting all bands of the corresponding image.
+     * An empty array element (i.e. zero band to select) discards the corresponding source image.
+     *
+     * <p>One of the {@code validate(…)} method shall be invoked after this constructor.</p>
      *
      * @param  sources         the sources from which to get the sample dimensions.
      * @param  bandsPerSource  sample dimensions for each source. May contain {@code null} elements.
      */
-    public MultiSourceArgument(final S[] sources, final int[][] bandsPerSource) {
-        this.sources = sources;
+    public MultiSourceArgument(S[] sources, int[][] bandsPerSource) {
+        /*
+         * Ensure that both arrays are non-null and have the same length.
+         * Copy those arrays because their content will be overwritten.
+         */
+        ArgumentChecks.ensureNonEmpty("sources", sources);
+        final int n = sources.length;
+        if (bandsPerSource != null) {
+            if (bandsPerSource.length > n) {
+                throw new IllegalArgumentException(Errors.format(
+                        Errors.Keys.TooManyCollectionElements_3,
+                        "bandsPerSource", bandsPerSource.length, n));
+            }
+            bandsPerSource = Arrays.copyOf(bandsPerSource, n);
+        } else {
+            bandsPerSource = new int[n][];
+        }
+        this.sources        = sources.clone();
         this.bandsPerSource = bandsPerSource;
-        sourceOfGridToCRS = -1;
+        sourceOfGridToCRS   = -1;
     }
 
     /**
@@ -127,18 +147,93 @@ public final class MultiSourceArgument<S> {
     }
 
     /**
-     * Specifies a function which, given an (image, bands) pair, may return the source of the image.
-     * If the source is returned, then the bands array is updated with the indices in that source.
-     * The function shall modify the given {@code int[]} in-place and return the new source,
-     * or return the {@code S} value unchanged if no unwrapping has been done.
+     * Specifies a method which, given a source, may decompose that source
+     * in a sequence of deeper sources associated with their bands to select.
+     * The consumer will be invoked for all sources specified to the constructor.
+     * If a source can be decomposed, then the specified consumer should invoke
+     * {@code apply(…)} on the given {@code Unwrapper} instance.
      *
-     * @param  filter  the function to invoke for getting the source of an image or coverage.
+     * @param  filter  the method to invoke for getting the sources of an image or coverage.
      */
-    public void unwrap(final BiFunction<S,int[],S> filter) {
+    public void unwrap(final Consumer<Unwrapper> filter) {
         if (validated) throw new IllegalStateException();
         unwrapper = filter;
     }
 
+    /**
+     * Asks to the {@linkplain #unwrapper} if the given source can be decomposed into deeper sources.
+     *
+     * @param  index   index of {@code source} in the {@link #sources} array.
+     * @param  source  the source to potentially unwrap.
+     * @param  bands   the bands to use in the source. Shall not be {@code null}.
+     * @return whether the source has been decomposed.
+     */
+    private boolean unwrap(int index, S source, int[] bands) {
+        if (unwrapper == null) {
+            return false;
+        }
+        final Unwrapper handler = new Unwrapper(index, source, bands);
+        unwrapper.accept(handler);
+        return handler.done;
+    }
+
+    /**
+     * Replace a user-supplied source by a deeper source with the bands to select.
+     * This is used for getting a flattened view of nested aggregations.
+     */
+    public final class Unwrapper {
+        /**
+         * Index of {@link #source} in the {@link #sources} array.
+         */
+        private final int index;
+
+        /**
+         * The source to potentially unwrap.
+         */
+        public final S source;
+
+        /**
+         * The bands to use in the source (never {@code null}).
+         * This array shall not modified because it may be a reference to an internal array.
+         */
+        public final int[] bands;
+
+        /**
+         * Whether the source has been decomposed in deeper sources.
+         */
+        private boolean done;
+
+        /**
+         * Creates a new instance to be submitted to user-supplied {@link #unwrapper}.
+         */
+        private Unwrapper(final int index, final S source, final int[] bands) {
+            this.index  = index;
+            this.source = source;
+            this.bands  = bands;
+        }
+
+        /**
+         * Notifies the enclosing {@code MultiSourceArgument} that the {@linkplain #source}
+         * shall be replaced by deeper sources. The {@code componentBands} array specifies
+         * the bands to use for each source and shall take in account the {@link #bands} subset.
+         *
+         * @param components      the deeper sources to use in replacement to {@link #source}.
+         * @param componentBands  the bands to use in replacement for {@link #bands}.
+         */
+        public void apply(final S[] components, final int[][] componentBands) {
+            final int n = components.length;
+            if (componentBands.length != n) {
+                throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedArrayLengths));
+            }
+            if (done) throw new IllegalStateException();
+            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(componentBands, 0, bandsPerSource, index, n);
+            done = true;
+        }
+    }
+
     /**
      * Clones and validates the arguments given to the constructor.
      *
@@ -174,47 +269,27 @@ public final class MultiSourceArgument<S> {
      * @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;
         /*
-         * Ensure that both arrays are non-null and have the same length.
-         * Copy those arrays as their content may be overwritten.
-         */
-        ArgumentChecks.ensureNonEmpty("sources", sources);
-        final int sourceCount = sources.length;
-        if (bandsPerSource != null) {
-            if (bandsPerSource.length > sourceCount) {
-                throw new IllegalArgumentException(Errors.format(
-                        Errors.Keys.TooManyCollectionElements_3,
-                        "bandsPerSource", bandsPerSource.length, sourceCount));
-            }
-            bandsPerSource = Arrays.copyOf(bandsPerSource, sourceCount);
-        } else {
-            bandsPerSource = new int[sourceCount][];
-        }
-        sources = sources.clone();
-        /*
-         * Compute the number of sources and the total number of bands.
          * 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[]> pool = identityAsNull ? null : new HashMap<>();
-        int filteredCount = 0;
-        for (int i=0; i<sourceCount; i++) {
-            int[] selected = bandsPerSource[i];
-            if (selected != null && selected.length == 0) {
-                // Note that the source is allowed to be null in this particular case.
-                continue;
-            }
-            S source = sources[i];
-            ArgumentChecks.ensureNonNullElement("sources", i, source);
-            /*
-             * Get the number of bands, or optionally the bands themselves.
-             * This information is required before to validate arguments.
-             */
+next:   for (int i=0; i<sources.length; i++) {          // `sources.length` may change during the loop.
+            S source;
+            int[] selected;
             List<SampleDimension> sourceBands;
             int numSourceBands;
             RangeArgument range;
             do {
+                selected = bandsPerSource[i];
+                if (selected != null && selected.length == 0) {
+                    // Note that the source is allowed to be null in this particular case.
+                    continue next;
+                }
+                source = sources[i];
+                ArgumentChecks.ensureNonNullElement("sources", i, source);
                 if (getter != null) {
                     sourceBands = getter.apply(source);
                     numSourceBands = sourceBands.size();
@@ -228,9 +303,10 @@ public final class MultiSourceArgument<S> {
                  * 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.
                  */
-            } while (unwrapper != null && source != (source = unwrapper.apply(source, selected)));
+            } 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.
              */
             if (ranges != null) {
                 for (int b : selected) {
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 ea7641b54d..144510cee8 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
@@ -70,6 +70,13 @@ public final class BandAggregateImageTest extends TestCase {
         allowSharing = true;            // This is the default mode of `ImageProcessor`.
     }
 
+    /**
+     * Creates the band aggregate instance to test using current value of {@link #sourceImages}.
+     */
+    private RenderedImage createBandAggregate() {
+        return BandAggregateImage.create(sourceImages, null, null, false, allowSharing, false);
+    }
+
     /**
      * Tests the aggregation of two untiled images with forced copy of sample values.
      * This is the simplest case in this test class.
@@ -95,7 +102,7 @@ public final class BandAggregateImageTest extends TestCase {
         im2.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s * 2).toArray());
         sourceImages = new RenderedImage[] {im1, im2};
 
-        final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, false);
+        final RenderedImage result = createBandAggregate();
         assertNotNull(result);
         assertEquals(0, result.getMinTileX());
         assertEquals(0, result.getMinTileY());
@@ -186,9 +193,8 @@ public final class BandAggregateImageTest extends TestCase {
         final TiledImageMock im1 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 1, 2, firstBanded);
         final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 3, 4, secondBanded);
         initializeAllTiles(im1, im2);
-        sourceImages = new RenderedImage[] {im1, im2};
 
-        RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, false);
+        RenderedImage result = createBandAggregate();
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -255,7 +261,7 @@ public final class BandAggregateImageTest extends TestCase {
             new int[] {1},      // Take second band of image 1.
             null,               // Take all bands of image 2.
             new int[] {0}       // Take first band of image 1.
-        }, null, allowSharing, false);
+        }, null, false, allowSharing, false);
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -314,9 +320,8 @@ public final class BandAggregateImageTest extends TestCase {
         final TiledImageMock tiled4x1 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 4, 1, 3, 4, true);
         final TiledImageMock oneTile  = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 8, 4, 5, 6, true);
         initializeAllTiles(tiled2x2, tiled4x1, oneTile);
-        sourceImages = new RenderedImage[] {tiled2x2, tiled4x1, oneTile};
 
-        final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, false);
+        final RenderedImage result = createBandAggregate();
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -382,9 +387,8 @@ public final class BandAggregateImageTest extends TestCase {
         final TiledImageMock tiled4x4 = new TiledImageMock(DataBuffer.TYPE_SHORT, 2, 4, 2,  8,  8,  4,  4, 0, 0, true);
         final TiledImageMock tiled6x6 = new TiledImageMock(DataBuffer.TYPE_SHORT, 1, 2, 0, 12,  6,  6,  6, 0, 0, true);
         initializeAllTiles(untiled, tiled2x2, tiled4x4, tiled6x6);
-        sourceImages = new RenderedImage[] {untiled, tiled2x2, tiled4x4, tiled6x6};
 
-        RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, prefetch);
+        RenderedImage result = BandAggregateImage.create(sourceImages, null, null, false, allowSharing, prefetch);
         assertNotNull(result);
         assertEquals(4, result.getMinX());
         assertEquals(2, result.getMinY());
@@ -418,6 +422,31 @@ public final class BandAggregateImageTest extends TestCase {
         }
     }
 
+    /**
+     * Tests aggregation of aggregated images. The result should be a flattened view.
+     * Opportunistically tests a "band select" operation after the aggregation.
+     */
+    @Test
+    public void testNestedAggregation() {
+        final int minX   =  7;
+        final int minY   = -5;
+        final int width  =  6;
+        final int height =  4;
+        final TiledImageMock im1 = new TiledImageMock(DataBuffer.TYPE_USHORT, 3, minX, minY, width, height, 3, 2, 1, 2, true);
+        final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_USHORT, 1, minX, minY, width, height, 3, 2, 3, 4, true);
+        final TiledImageMock im3 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 2, 2, 1, true);
+        initializeAllTiles(im1, im2, im3);
+
+        RenderedImage result;
+        result = BandAggregateImage.create(new RenderedImage[] {im2, im3},    null, null, false, allowSharing, false);
+        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));
+    }
+
     /**
      * Initializes all bands of all input images to testing values.
      * The testing values are defined by a "BTYX" pattern where:
@@ -429,7 +458,8 @@ 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>
      */
-    private static void initializeAllTiles(final TiledImageMock... images) {
+    private void initializeAllTiles(final TiledImageMock... images) {
+        sourceImages = images;
         int band = 0;
         for (final TiledImageMock image : images) {
             final int numBands = image.getSampleModel().getNumBands();
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandSelectImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/BandSelectImageTest.java
index 2a1d0be7ae..60db8f4b7d 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/BandSelectImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandSelectImageTest.java
@@ -231,4 +231,18 @@ public final class BandSelectImageTest extends TestCase {
         writable.setData(data);
         assertValuesEqual(writable.getData(), 0, expectedSampleValues());
     }
+
+    /**
+     * Tests a band select on an image which is already a band select.
+     * The nested operations should be simplified to a single band select operation.
+     */
+    @Test
+    public void testNestedBandSelect() {
+        createImage(3, 2, true);
+        final ImageProcessor processor = new ImageProcessor();
+        RenderedImage test = processor.selectBands(image, 1, 2);
+        test = processor.selectBands(test, 1);
+        assertSame(image, ((BandSelectImage) test).getSource());
+        assertValuesEqual(test.getData(), 0, expectedSampleValues());
+    }
 }
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 4c072de282..ae973f5f6e 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
@@ -644,7 +644,7 @@ public final class ArraysExt extends Static {
         if (length == 0) {
             return array;               // May be null
         }
-        ArgumentChecks.ensureNonNull ("array",  array);
+        ArgumentChecks.ensureNonNull("array",  array);
         final int arrayLength = Array.getLength(array);
         ArgumentChecks.ensureBetween("first", 0, arrayLength, first);
         ArgumentChecks.ensurePositive("length", length);