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/08 16:38:06 UTC

[sis] branch geoapi-4.0 updated (297e7a67fe -> 6c85b283c4)

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 297e7a67fe Refactor `WritableRenderedImage`support in `BandedSampleConverter` for sharing more code with other writable images. Refactor `BandAggregateImage` by moving its inner helper class outside, and add `WritableRenderedImage`support. `BandAggregateImage` is no longer an "all or nothing" implementation: can have a mix of shared and copied arrays.
     new 65c2a49846 Make `getTileWidth()` and `getTileHeight()` methods final in `ComputedImage`. Add design notes in Javadoc for explaining some rational.
     new d3164ba70d Add a `MultiSourceImage` package-private abstract class and add support for prefetch operation.
     new 6c85b283c4 Result of "band select" operation should be writable if the image is writable.

The 3 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:
 .../coverage/grid/BandAggregateGridCoverage.java   |   4 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   |   4 +-
 .../org/apache/sis/image/BandAggregateImage.java   | 100 ++----------
 .../java/org/apache/sis/image/BandSelectImage.java |  76 ++++++++-
 .../java/org/apache/sis/image/BandSharing.java     |  31 ++--
 .../java/org/apache/sis/image/ComputedImage.java   |  36 +++--
 .../java/org/apache/sis/image/ImageProcessor.java  |  15 +-
 .../org/apache/sis/image/MultiSourceImage.java     | 159 ++++++++++++++++++
 ...inedImageLayout.java => MultiSourceLayout.java} |  12 +-
 .../org/apache/sis/image/MultiSourcePrefetch.java  | 178 +++++++++++++++++++++
 .../org/apache/sis/image/SourceAlignedImage.java   |   8 +-
 ...urcesArgument.java => MultiSourceArgument.java} |   4 +-
 .../sis/internal/coverage/j2d/ImageUtilities.java  |  30 ++++
 .../apache/sis/image/BandAggregateImageTest.java   |  37 ++++-
 .../org/apache/sis/image/BandSelectImageTest.java  |  65 +++++++-
 .../aggregate/BandAggregateGridResource.java       |   4 +-
 16 files changed, 610 insertions(+), 153 deletions(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java
 rename core/sis-feature/src/main/java/org/apache/sis/image/{CombinedImageLayout.java => MultiSourceLayout.java} (97%)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/image/MultiSourcePrefetch.java
 rename core/sis-feature/src/main/java/org/apache/sis/internal/coverage/{MultiSourcesArgument.java => MultiSourceArgument.java} (99%)


[sis] 03/03: Result of "band select" operation should be writable if the image is writable.

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 6c85b283c4009fcd0b658dd44de43984e3125e8a
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Apr 8 16:53:23 2023 +0200

    Result of "band select" operation should be writable if the image is writable.
---
 .../java/org/apache/sis/image/BandSelectImage.java | 76 +++++++++++++++++++++-
 .../java/org/apache/sis/image/ImageProcessor.java  |  9 +++
 .../org/apache/sis/image/BandSelectImageTest.java  | 65 +++++++++++++++---
 3 files changed, 141 insertions(+), 9 deletions(-)

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 07b4f7cd12..60fbb426ec 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
@@ -25,10 +25,13 @@ import java.awt.image.Raster;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
 import java.awt.image.ColorModel;
+import java.awt.image.TileObserver;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
 
 
@@ -43,7 +46,7 @@ import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
  * @version 1.4
  * @since   1.1
  */
-final class BandSelectImage extends SourceAlignedImage {
+class BandSelectImage extends SourceAlignedImage {
     /**
      * Properties to inherit from the source image, after bands reduction if applicable.
      *
@@ -119,6 +122,8 @@ final class BandSelectImage extends SourceAlignedImage {
             image = new BufferedImage(cm,
                     bi.getRaster().createWritableChild(0, 0, bi.getWidth(), bi.getHeight(), 0, 0, bands),
                     bi.isAlphaPremultiplied(), properties);
+        } else if (source instanceof WritableRenderedImage) {
+            image = new Writable(source, cm, bands);
         } else {
             image = new BandSelectImage(source, cm, bands);
         }
@@ -189,6 +194,75 @@ final class BandSelectImage extends SourceAlignedImage {
         return parent.createChild(x, y, parent.getWidth(), parent.getHeight(), x, y, bands);
     }
 
+    /**
+     * Applies the band selection on the given writable raster.
+     * The child is created in the same way than {@code computeTile(…)}.
+     */
+    final WritableRaster apply(final WritableRaster parent) {
+        final int x = parent.getMinX();
+        final int y = parent.getMinY();
+        return parent.createWritableChild(x, y, parent.getWidth(), parent.getHeight(), x, y, bands);
+    }
+
+    /**
+     * A {@code BandSelectImage} where the source is a writable rendered image.
+     */
+    private static final class Writable extends BandSelectImage implements WritableRenderedImage {
+        /** Creates a new "band select" operation for the given source. */
+        Writable(final RenderedImage source, final ColorModel cm, final int[] bands) {
+            super(source, cm, bands);
+        }
+
+        /** Returns the source as a writable image. */
+        private WritableRenderedImage target() {
+            return (WritableRenderedImage) getSource();
+        }
+
+        /** Checks out a tile for writing. */
+        @Override public WritableRaster getWritableTile(final int tileX, final int tileY) {
+            markTileWritable(tileX, tileY, true);
+            final WritableRaster parent = target().getWritableTile(tileX, tileY);
+            return apply(parent);
+        }
+
+        /** Relinquishes the right to write to a tile. */
+        @Override public void releaseWritableTile(final int tileX, final int tileY) {
+            target().releaseWritableTile(tileX, tileY);
+            markTileWritable(tileX, tileY, false);
+        }
+
+        /** Adds an observer to be notified when a tile is checked out for writing. */
+        @Override public void addTileObserver(final TileObserver observer) {
+            target().addTileObserver(observer);
+        }
+
+        /** Removes an observer from the list of observers notified when a tile is checked out for writing. */
+        @Override public void removeTileObserver(final TileObserver observer) {
+            target().removeTileObserver(observer);
+        }
+
+        /** Sets a region of the image to the contents of the given raster. */
+        @Override public void setData(final Raster data) {
+            final WritableRenderedImage target = target();
+            final var executor = new TileOpExecutor(target, data.getBounds()) {
+                @Override protected void writeTo(final WritableRaster tile) {
+                    apply(tile).setRect(data);
+                }
+            };
+            executor.writeTo(target);
+        }
+
+        /** Restores the identity behavior for writable image. */
+        @Override public int hashCode() {
+            return System.identityHashCode(this);
+        }
+
+        /** Restores the identity behavior for writable image. */
+        @Override public boolean equals(final Object object) {
+            return object == this;
+        }
+    }
+
     /**
      * Returns a hash code value for this image.
      */
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 56238cb8a2..34d0e60ca3 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
@@ -899,6 +899,10 @@ public class ImageProcessor implements Cloneable {
      * pixel values are not copied. Consequently, changes in the source image are reflected
      * immediately in the returned image.</p>
      *
+     * <p>If the given image is an instance of {@link WritableRenderedImage},
+     * then the returned image will also be a {@link WritableRenderedImage}.
+     * In such case values written in the returned image will be written directly in the source image.</p>
+     *
      * <h4>Properties used</h4>
      * This operation uses the following properties in addition to method parameters:
      * <ul>
@@ -965,6 +969,11 @@ 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,
+     * 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>
      * <ul>
      *   <li>All images shall use the same {@linkplain SampleModel#getDataType() data type}.</li>
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 df68cc3d1d..2a1d0be7ae 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
@@ -21,12 +21,14 @@ import java.util.Hashtable;
 import java.awt.image.DataBuffer;
 import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
 import java.awt.image.RenderedImage;
 import java.awt.image.BufferedImage;
 import java.awt.image.ColorModel;
 import java.awt.image.IndexColorModel;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -37,7 +39,7 @@ import static org.apache.sis.test.FeatureAssert.*;
  * Tests {@link BandSelectImage}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  * @since   1.1
  */
 public final class BandSelectImageTest extends TestCase {
@@ -46,6 +48,11 @@ public final class BandSelectImageTest extends TestCase {
      */
     private static final int WIDTH = 3, HEIGHT = 4;
 
+    /**
+     * Random number generator used for the test.
+     */
+    private Random random;
+
     /**
      * The source image as an instance of custom implementation.
      */
@@ -68,7 +75,7 @@ public final class BandSelectImageTest extends TestCase {
     private void createImage(final int numBands, final int checkedBand, final boolean icm) {
         image = new TiledImageMock(DataBuffer.TYPE_BYTE, numBands, 0, 0, WIDTH, HEIGHT, WIDTH, HEIGHT, 0, 0, false);
         image.initializeAllTiles(checkedBand);
-        final Random random = new Random();
+        random = TestUtilities.createRandomNumberGenerator();
         for (int i=0; i<numBands; i++) {
             if (i != checkedBand) {
                 image.setRandomValues(i, random, 100);
@@ -91,6 +98,18 @@ public final class BandSelectImageTest extends TestCase {
         bufferedImage = new BufferedImage(cm, (WritableRaster) image.getTile(0, 0), false, properties);
     }
 
+    /**
+     * The expected sample values in the determinist band initialized by {@link #createImage(int, int, boolean)}.
+     */
+    private static int[][] expectedSampleValues() {
+        return new int[][] {
+            {100, 101, 102},
+            {110, 111, 112},
+            {120, 121, 122},
+            {130, 131, 132}
+        };
+    }
+
     /**
      * Computes a dummy resolution for the given band.
      */
@@ -110,12 +129,7 @@ public final class BandSelectImageTest extends TestCase {
         assertEquals("numBands", numBands, tile.getNumBands());
         assertEquals("numBands", numBands, ImageUtilities.getNumBands(image));
         assertEquals("sampleModel", image.getSampleModel(), tile.getSampleModel());
-        assertValuesEqual(tile, checkedBand, new int[][] {
-            {100, 101, 102},
-            {110, 111, 112},
-            {120, 121, 122},
-            {130, 131, 132}
-        });
+        assertValuesEqual(tile, checkedBand, expectedSampleValues());
     }
 
     /**
@@ -182,4 +196,39 @@ public final class BandSelectImageTest extends TestCase {
         verifySamples(test, 3, 2);
         verifyProperties(test, 3, 0, 2);
     }
+
+    /**
+     * Tests write operation.
+     */
+    @Test
+    public void testWritable() {
+        createImage(2, 1, true);
+        final ImageProcessor processor = new ImageProcessor();
+        RenderedImage test = processor.selectBands(image, 1);
+        final int[][] expected = expectedSampleValues();
+        final Raster data = test.getData();
+        assertValuesEqual(data, 0, expected);
+        /*
+         * Above code where read operations for making sure that we initialized the test correctly.
+         * Code below is the actual test for write operations.
+         */
+        final WritableRenderedImage writable = (WritableRenderedImage) test;
+        final int tileX = writable.getMinTileX();
+        final int tileY = writable.getMinTileY();
+        final WritableRaster tile = writable.getWritableTile(tileX, tileY);
+        for (int i=0; i<3; i++) {
+            final int x = random.nextInt(tile.getWidth());
+            final int y = random.nextInt(tile.getHeight());
+            final int s = random.nextInt(10);
+            tile.setSample(x, y, 0, s);
+            expected[y][x] = s;
+        }
+        writable.releaseWritableTile(tileX, tileY);
+        assertValuesEqual(writable.getData(), 0, expected);
+        /*
+         * Try to restore orginal values.
+         */
+        writable.setData(data);
+        assertValuesEqual(writable.getData(), 0, expectedSampleValues());
+    }
 }


[sis] 02/03: Add a `MultiSourceImage` package-private abstract class and add support for prefetch operation.

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 d3164ba70d09272dea3950f305dfc45f404bae18
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Apr 8 15:29:12 2023 +0200

    Add a `MultiSourceImage` package-private abstract class and add support for prefetch operation.
---
 .../coverage/grid/BandAggregateGridCoverage.java   |   4 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   |   4 +-
 .../org/apache/sis/image/BandAggregateImage.java   | 100 ++----------
 .../java/org/apache/sis/image/BandSharing.java     |  31 ++--
 .../java/org/apache/sis/image/ImageProcessor.java  |   6 +-
 .../org/apache/sis/image/MultiSourceImage.java     | 159 ++++++++++++++++++
 ...inedImageLayout.java => MultiSourceLayout.java} |  12 +-
 .../org/apache/sis/image/MultiSourcePrefetch.java  | 178 +++++++++++++++++++++
 ...urcesArgument.java => MultiSourceArgument.java} |   4 +-
 .../apache/sis/image/BandAggregateImageTest.java   |  37 ++++-
 .../aggregate/BandAggregateGridResource.java       |   4 +-
 11 files changed, 416 insertions(+), 123 deletions(-)

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 6b95c83080..113033a9fb 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
@@ -24,7 +24,7 @@ import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.image.DataType;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.internal.feature.Resources;
-import org.apache.sis.internal.coverage.MultiSourcesArgument;
+import org.apache.sis.internal.coverage.MultiSourceArgument;
 import org.apache.sis.internal.util.CollectionsExt;
 
 
@@ -89,7 +89,7 @@ final class BandAggregateGridCoverage extends GridCoverage {
      * @throws IllegalArgumentException if there is an incompatibility between some source coverages
      *         or if some band indices are duplicated or outside their range of validity.
      */
-    BandAggregateGridCoverage(final MultiSourcesArgument<GridCoverage> aggregate, final ImageProcessor processor) {
+    BandAggregateGridCoverage(final MultiSourceArgument<GridCoverage> aggregate, final ImageProcessor processor) {
         super(aggregate.domain(GridCoverage::getGridGeometry), aggregate.ranges());
         this.sources           = aggregate.sources();
         this.bandsPerSource    = aggregate.bandsPerSource();
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 a57a73b479..f7f9017fc9 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
@@ -39,7 +39,7 @@ import org.apache.sis.image.Colorizer;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.image.Interpolation;
 import org.apache.sis.internal.coverage.SampleDimensions;
-import org.apache.sis.internal.coverage.MultiSourcesArgument;
+import org.apache.sis.internal.coverage.MultiSourceArgument;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.collection.WeakHashSet;
@@ -761,7 +761,7 @@ public class GridCoverageProcessor implements Cloneable {
      * @since 1.4
      */
     public GridCoverage aggregateRanges(GridCoverage[] sources, int[][] bandsPerSource) {
-        final var aggregate = new MultiSourcesArgument<>(sources, bandsPerSource);
+        final var aggregate = new MultiSourceArgument<>(sources, bandsPerSource);
         aggregate.identityAsNull();
         aggregate.validate(GridCoverage::getSampleDimensions);
         if (aggregate.isIdentity()) {
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 46c0a540cf..7ffbd60648 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
@@ -16,8 +16,6 @@
  */
 package org.apache.sis.image;
 
-import java.util.Arrays;
-import java.util.Objects;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.BandedSampleModel;
@@ -43,34 +41,7 @@ import org.apache.sis.internal.coverage.j2d.ImageUtilities;
  *
  * @since 1.4
  */
-class BandAggregateImage extends WritableComputedImage {
-    /**
-     * The source images with only the bands to aggregate, in order.
-     * Those images are views; the band sample values are not copied.
-     */
-    protected final RenderedImage[] filteredSources;
-
-    /**
-     * Color model of the aggregated image.
-     *
-     * @see #getColorModel()
-     */
-    private final ColorModel colorModel;
-
-    /**
-     * Domain of pixel coordinates. All images shall share the same pixel coordinate space,
-     * meaning that a pixel at coordinates (<var>x</var>, <var>y</var>) in this image will
-     * contain the sample values of all source images at the same coordinates.
-     * It does <em>not</em> mean that all source images shall have the same bounds.
-     */
-    private final int minX, minY, width, height;
-
-    /**
-     * Index of the first tile. Contrarily to pixel coordinates,
-     * the tile coordinate space does not need to be the same for all images.
-     */
-    private final int minTileX, minTileY;
-
+class BandAggregateImage extends MultiSourceImage {
     /**
      * Whether the sharing of data arrays is allowed.
      * When a source tile has the same bounds and scanline stride than the target tile,
@@ -87,19 +58,20 @@ class BandAggregateImage extends WritableComputedImage {
      * @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  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 Colorizer colorizer, final boolean allowSharing, final boolean parallel)
     {
-        final var layout = CombinedImageLayout.create(sources, bandsPerSource, allowSharing);
+        final var layout = MultiSourceLayout.create(sources, bandsPerSource, allowSharing);
         final BandAggregateImage image;
         if (layout.isWritable()) {
-            image = new Writable(layout, colorizer, allowSharing);
+            image = new Writable(layout, colorizer, allowSharing, parallel);
         } else {
-            image = new BandAggregateImage(layout, colorizer, allowSharing);
+            image = new BandAggregateImage(layout, colorizer, allowSharing, parallel);
         }
         if (image.filteredSources.length == 1) {
             final RenderedImage c = image.filteredSources[0];
@@ -120,30 +92,13 @@ class BandAggregateImage extends WritableComputedImage {
      * @param  layout     pixel and tile coordinate spaces of this image, together with sample model.
      * @param  colorizer  provider of color model to use for this image, or {@code null} for automatic.
      */
-    BandAggregateImage(final CombinedImageLayout layout, final Colorizer colorizer, final boolean allowSharing) {
-        super(layout.sampleModel, layout.sources);
+    BandAggregateImage(final MultiSourceLayout layout, final Colorizer colorizer,
+                       final boolean allowSharing, final boolean parallel)
+    {
+        super(layout, colorizer, parallel);
         this.allowSharing = allowSharing;
-        final Rectangle r = layout.domain;
-        minX            = r.x;
-        minY            = r.y;
-        width           = r.width;
-        height          = r.height;
-        minTileX        = layout.minTileX;
-        minTileY        = layout.minTileY;
-        filteredSources = layout.filteredSources;
-        colorModel      = layout.createColorModel(colorizer);
-        ensureCompatible(colorModel);
     }
 
-    /** Returns the information inferred at construction time. */
-    @Override public ColorModel getColorModel() {return colorModel;}
-    @Override public int        getWidth()      {return width;}
-    @Override public int        getHeight()     {return height;}
-    @Override public int        getMinX()       {return minX;}
-    @Override public int        getMinY()       {return minY;}
-    @Override public int        getMinTileX()   {return minTileX;}
-    @Override public int        getMinTileY()   {return minTileY;}
-
     /**
      * Creates a raster containing the selected bands of source images.
      *
@@ -159,15 +114,13 @@ class BandAggregateImage extends WritableComputedImage {
         /*
          * If we are allowed to share the data arrays, try that first.
          * The cast to `BandedSampleModel` is safe because this is the
-         * type given by `CombinedImageLayout` in the constructor.
+         * type given by `MultiSourceLayout` in the constructor.
          */
         BandSharedRaster shared = null;
         if (allowSharing) {
             final BandSharing sharing = BandSharing.create((BandedSampleModel) sampleModel);
             if (sharing != null) {
-                final long x = Math.multiplyFull(tileX - minTileX, getTileWidth())  + minX;
-                final long y = Math.multiplyFull(tileY - minTileY, getTileHeight()) + minY;
-                tile = shared = sharing.createRaster(x, y, filteredSources);
+                tile = shared = sharing.createRaster(tileToPixel(tileX, tileY), filteredSources);
             }
         }
         /*
@@ -206,8 +159,10 @@ class BandAggregateImage extends WritableComputedImage {
          * @param  layout     pixel and tile coordinate spaces of this image, together with sample model.
          * @param  colorizer  provider of color model to use for this image, or {@code null} for automatic.
          */
-        Writable(final CombinedImageLayout layout, final Colorizer colorizer, final boolean allowSharing) {
-            super(layout, colorizer, allowSharing);
+        Writable(final MultiSourceLayout layout, final Colorizer colorizer,
+                 final boolean allowSharing, final boolean parallel)
+        {
+            super(layout, colorizer, allowSharing, parallel);
         }
 
         /**
@@ -291,32 +246,11 @@ class BandAggregateImage extends WritableComputedImage {
         }
     }
 
-    /**
-     * Returns a hash code value for this image.
-     */
-    @Override
-    public int hashCode() {
-        return sampleModel.hashCode() + 37 * (Arrays.hashCode(filteredSources) + 31 * Objects.hashCode(colorModel));
-    }
-
     /**
      * Compares the given object with this image for equality.
-     *
-     * <h4>Implementation note</h4>
-     * We do not invoke {@link #equalsBase(Object)} for saving the comparisons of {@link ComputedImage#sources} array.
-     * The comparison of {@link #filteredSources} array will indirectly include the comparison of raw source images.
      */
     @Override
     public boolean equals(final Object object) {
-        if (object instanceof BandAggregateImage) {
-            final BandAggregateImage other = (BandAggregateImage) object;
-            return minTileX == other.minTileX &&
-                   minTileY == other.minTileY &&
-                   getBounds().equals(other.getBounds()) &&
-                   sampleModel.equals(other.sampleModel) &&
-                   Objects.equals(colorModel, other.colorModel) &&
-                   Arrays.equals(filteredSources, other.filteredSources);
-        }
-        return false;
+        return super.equals(object) && ((BandAggregateImage) object).allowSharing == allowSharing;
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java
index 29ffb0fba5..4f2bf46e9c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java
@@ -102,12 +102,11 @@ abstract class BandSharing {
      * Prepares sharing the arrays of the given sources when possible.
      * This method does not allocate new {@link DataBuffer} banks.
      *
-     * @param  x        <var>x</var> pixel coordinate of the tile.
-     * @param  y        <var>y</var> pixel coordinate of the tile.
-     * @param  sources  the sources for which to aggregate all bands.
+     * @param  location  smallest (<var>x</var>,<var>y</var>) pixel coordinates of the tile.
+     * @param  sources   the sources for which to aggregate all bands.
      * @return data buffer size, or 0 if there is nothing to share.
      */
-    private int prepare(final long x, final long y, final RenderedImage[] sources) {
+    private int prepare(final Point location, final RenderedImage[] sources) {
         final int tileWidth      = target.getWidth();
         final int tileHeight     = target.getHeight();
         final int scanlineStride = target.getScanlineStride();
@@ -121,17 +120,15 @@ abstract class BandSharing {
             if (source.getTileWidth()  == tileWidth &&
                 source.getTileHeight() == tileHeight)
             {
-                long tileX = x - source.getTileGridXOffset();
-                long tileY = y - source.getTileGridYOffset();
+                int tileX = Math.subtractExact(location.x, source.getTileGridXOffset());
+                int tileY = Math.subtractExact(location.y, source.getTileGridYOffset());
                 if (((tileX % tileWidth) | (tileY % tileHeight)) == 0) {
                     tileX /= tileWidth;
                     tileY /= tileHeight;
-                    final int tx = Math.toIntExact(tileX);
-                    final int ty = Math.toIntExact(tileY);
                     final int n  = si << 1;
-                    sourceTileIndices[n  ] = tx;
-                    sourceTileIndices[n+1] = ty;
-                    final Raster raster = source.getTile(tx, ty);
+                    sourceTileIndices[n  ] = tileX;
+                    sourceTileIndices[n+1] = tileY;
+                    final Raster raster = source.getTile(tileX, tileY);
                     final SampleModel c = raster.getSampleModel();
                     if (c instanceof ComponentSampleModel) {
                         final var sm = (ComponentSampleModel) c;
@@ -172,18 +169,16 @@ abstract class BandSharing {
      * Creates a raster sharing the arrays of given sources when possible.
      * This method assumes a target {@link BandedSampleModel} where all band offsets.
      *
-     * @param  x        <var>x</var> pixel coordinate of the tile.
-     * @param  y        <var>y</var> pixel coordinate of the tile.
-     * @param  sources  the sources for which to aggregate all bands.
-     * @return a raster containing the aggregation of all bands, or {@code null} if the is nothing to share.
+     * @param  location  smallest (<var>x</var>,<var>y</var>) pixel coordinates of the tile.
+     * @param  sources   the sources for which to aggregate all bands.
+     * @return a raster  containing the aggregation of all bands, or {@code null} if there is nothing to share.
      */
-    final BandSharedRaster createRaster(final long x, final long y, final RenderedImage[] sources) {
-        final int size = prepare(x, y, sources);
+    final BandSharedRaster createRaster(final Point location, final RenderedImage[] sources) {
+        final int size = prepare(location, sources);
         if (size == 0) {
             return null;
         }
         final DataBuffer buffer = allocate(size);
-        final var location = new Point(Math.toIntExact(x), Math.toIntExact(y));
         return new BandSharedRaster(sourceTileIndices, parents, target, buffer, location);
     }
 
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 fab9c30fe6..56238cb8a2 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
@@ -976,6 +976,7 @@ public class ImageProcessor implements Cloneable {
      * This operation uses the following properties in addition to method parameters:
      * <ul>
      *   <li>{@linkplain #getColorizer() Colorizer}.</li>
+     *   <li>{@linkplain #getExecutionMode() Execution mode} (parallel or sequential).</li>
      * </ul>
      *
      * @param  sources  images whose bands shall be aggregated, in order. At least one image must be provided.
@@ -989,10 +990,12 @@ public class ImageProcessor implements Cloneable {
     public RenderedImage aggregateBands(final RenderedImage[] sources, final int[][] bandsPerSource) {
         ArgumentChecks.ensureNonEmpty("sources", sources);
         final Colorizer colorizer;
+        final boolean parallel;
         synchronized (this) {
             colorizer = this.colorizer;
+            parallel = executionMode != Mode.SEQUENTIAL;
         }
-        return unique(BandAggregateImage.create(sources, bandsPerSource, colorizer, true));
+        return unique(BandAggregateImage.create(sources, bandsPerSource, colorizer, true, parallel));
     }
 
     /**
@@ -1503,6 +1506,7 @@ public class ImageProcessor implements Cloneable {
      * @throws ImagingOpException if an error occurred during calculation.
      */
     public List<NavigableMap<Double,Shape>> isolines(final RenderedImage data, final double[][] levels, final MathTransform gridToCRS) {
+        ArgumentChecks.ensureNonNull("data", data);
         final boolean parallel;
         synchronized (this) {
             parallel = parallel(data);
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
new file mode 100644
index 0000000000..ed67b5446c
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceImage.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.awt.Point;
+import java.util.Arrays;
+import java.util.Objects;
+import java.awt.Rectangle;
+import java.awt.image.ColorModel;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRenderedImage;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.util.Disposable;
+
+
+/**
+ * An image which is the result of a computation involving more than one source.
+ * All sources shall use the same pixel coordinate system. However the sources
+ * do not need to have the same bounds or use the same tile matrix.
+ *
+ * <p>This implementation is for images that are <em>potentially</em> writable.
+ * Whether the image is effectively writable depends on whether all sources are
+ * instances of {@link WritableRenderedImage}.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since   1.4
+ */
+abstract class MultiSourceImage extends WritableComputedImage {
+    /**
+     * The source images, potentially with a preprocessing applied.
+     * Those sources may be different than {@link #getSources()} for example with the
+     * application of a "band select" operation for retaining only the bands needed.
+     */
+    protected final RenderedImage[] filteredSources;
+
+    /**
+     * Color model of this image.
+     *
+     * @see #getColorModel()
+     */
+    protected final ColorModel colorModel;
+
+    /**
+     * Domain of pixel coordinates. All images shall share the same pixel coordinate space,
+     * meaning that a pixel at coordinates (<var>x</var>, <var>y</var>) in this image will
+     * contain the sample values of all source images at the same coordinates.
+     * It does <em>not</em> mean that all source images shall have the same bounds.
+     */
+    private final int minX, minY, width, height;
+
+    /**
+     * Index of the first tile. Contrarily to pixel coordinates,
+     * the tile coordinate space does not need to be the same for all images.
+     */
+    private final int minTileX, minTileY;
+
+    /**
+     * Whether parallel computation is allowed.
+     */
+    private final boolean parallel;
+
+    /**
+     * Creates a new multi-sources image.
+     *
+     * @param  layout     pixel and tile coordinate spaces of this image, together with sample model.
+     * @param  colorizer  provider of color model to use for this image, or {@code null} for automatic.
+     * @param  parallel   whether parallel computation is allowed.
+     */
+    MultiSourceImage(final MultiSourceLayout layout, final Colorizer colorizer, final boolean parallel) {
+        super(layout.sampleModel, layout.sources);
+        final Rectangle r = layout.domain;
+        minX            = r.x;
+        minY            = r.y;
+        width           = r.width;
+        height          = r.height;
+        minTileX        = layout.minTileX;
+        minTileY        = layout.minTileY;
+        filteredSources = layout.filteredSources;
+        colorModel      = layout.createColorModel(colorizer);
+        ensureCompatible(colorModel);
+        this.parallel = parallel;
+    }
+
+    /** Returns the information inferred at construction time. */
+    @Override public final ColorModel getColorModel() {return colorModel;}
+    @Override public final int        getWidth()      {return width;}
+    @Override public final int        getHeight()     {return height;}
+    @Override public final int        getMinX()       {return minX;}
+    @Override public final int        getMinY()       {return minY;}
+    @Override public final int        getMinTileX()   {return minTileX;}
+    @Override public final int        getMinTileY()   {return minTileY;}
+
+    /**
+     * Converts a tile (column, row) indices to smallest (<var>x</var>, <var>y</var>) pixel coordinates
+     * inside the tile. The returned value is a coordinate of the pixel in upper-left corner.
+     *
+     * @param  tileX  the tile index for which to get pixel coordinate.
+     * @param  tileY  the tile index for which to get pixel coordinate.
+     * @return smallest (<var>x</var>, <var>y</var>) pixel coordinates inside the tile.
+     */
+    final Point tileToPixel(final int tileX, final int tileY) {
+        return new Point(Math.toIntExact((((long) tileX) - minTileX) * getTileWidth()  + minX),
+                         Math.toIntExact((((long) tileY) - minTileY) * getTileHeight() + minY));
+    }
+
+    /**
+     * Notifies the source images that tiles will be computed soon in the given region.
+     * This method forwards the notification to all images that are instances of {@link PlanarImage}.
+     */
+    @Override
+    protected Disposable prefetch(final Rectangle tiles) {
+        /*
+         * Convert tile indices to pixel indices. The latter will be converted back to
+         * tile indices for each source because the tile numbering may not be the same.
+         */
+        final Rectangle aoi = ImageUtilities.tilesToPixels(this, tiles);
+        return new MultiSourcePrefetch(filteredSources, aoi).run(parallel);
+    }
+
+    /**
+     * Returns a hash code value for this image.
+     */
+    @Override
+    public int hashCode() {
+        return hashCodeBase() + 37 * (Arrays.hashCode(filteredSources) + 31 * Objects.hashCode(colorModel));
+    }
+
+    /**
+     * Compares the given object with this image for equality.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (equalsBase(object)) {
+            final MultiSourceImage other = (MultiSourceImage) object;
+            return parallel == other.parallel &&
+                   minTileX == other.minTileX &&
+                   minTileY == other.minTileY &&
+                   getBounds().equals(other.getBounds()) &&
+                   Objects.equals(colorModel, other.colorModel) &&
+                   Arrays.equals(filteredSources, other.filteredSources);
+        }
+        return false;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java
similarity index 97%
rename from core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
rename to core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java
index 7f283984fe..afa30b76e5 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourceLayout.java
@@ -33,7 +33,7 @@ import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.coverage.j2d.ImageLayout;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
-import org.apache.sis.internal.coverage.MultiSourcesArgument;
+import org.apache.sis.internal.coverage.MultiSourceArgument;
 import org.apache.sis.coverage.grid.DisjointExtentException;
 
 
@@ -54,7 +54,7 @@ import org.apache.sis.coverage.grid.DisjointExtentException;
  *
  * @since 1.4
  */
-final class CombinedImageLayout extends ImageLayout {
+final class MultiSourceLayout extends ImageLayout {
     /**
      * The source images. This is a copy of the user-specified array,
      * except that images associated to an empty set of bands are discarded.
@@ -126,8 +126,8 @@ final class CombinedImageLayout extends ImageLayout {
      *         or if some band indices are duplicated or outside their range of validity.
      */
     @Workaround(library="JDK", version="1.8")
-    static CombinedImageLayout create(RenderedImage[] sources, int[][] bandsPerSource, boolean allowSharing) {
-        final var aggregate = new MultiSourcesArgument<RenderedImage>(sources, bandsPerSource);
+    static MultiSourceLayout create(RenderedImage[] sources, int[][] bandsPerSource, boolean allowSharing) {
+        final var aggregate = new MultiSourceArgument<RenderedImage>(sources, bandsPerSource);
         aggregate.identityAsNull();
         aggregate.validate(ImageUtilities::getNumBands);
 
@@ -202,7 +202,7 @@ final class CombinedImageLayout extends ImageLayout {
         final var preferredTileSize = new Dimension((int) cx, (int) cy);
         final boolean exactTileSize = ((cx | cy) >>> Integer.SIZE) == 0;
         allowSharing &= exactTileSize;
-        return new CombinedImageLayout(sources, bandsPerSource, domain, preferredTileSize, exactTileSize,
+        return new MultiSourceLayout(sources, bandsPerSource, domain, preferredTileSize, exactTileSize,
                 chooseMinTile(tileGridXOffset, domain.x, preferredTileSize.width),
                 chooseMinTile(tileGridYOffset, domain.y, preferredTileSize.height),
                 commonDataType, aggregate.numBands(), allowSharing ? scanlineStride : 0);
@@ -219,7 +219,7 @@ final class CombinedImageLayout extends ImageLayout {
      * @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 CombinedImageLayout(final RenderedImage[] sources, final int[][] bandsPerSource,
+    private MultiSourceLayout(final RenderedImage[] sources, final int[][] bandsPerSource,
             final Rectangle domain, final Dimension preferredTileSize, final boolean exactTileSize,
             final int minTileX, final int minTileY, final int commonDataType, final int numBands,
             final int scanlineStride)
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourcePrefetch.java b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourcePrefetch.java
new file mode 100644
index 0000000000..8873f58c24
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/MultiSourcePrefetch.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.Callable;
+import java.awt.Rectangle;
+import java.awt.image.RenderedImage;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.util.Disposable;
+import org.apache.sis.internal.system.CommonExecutor;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+
+
+/**
+ * A helper class for forwarding a {@code prefetch(…)} operation to multiple sources.
+ * This implementation assumes that all sources share the same pixel coordinates space.
+ * However the tile matrix does not need to be the same.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since   1.4
+ */
+final class MultiSourcePrefetch implements Disposable {
+    /**
+     * A filtered list of images on which to prefetch at least one tile.
+     * This array contains only {@link PlanarImage} instances which intersect the area of interest.
+     */
+    private final PlanarImage[] sources;
+
+    /**
+     * Indices of tiles to prefetch for each image in the {@link #sources} array.
+     */
+    private final Rectangle[] tileIndices;
+
+    /**
+     * Number of valid elements in {@link #sources} and {@link #tileIndices} arrays.
+     * Note that it will not be worth to use parallelism if the count is less than two.
+     */
+    private int count;
+
+    /**
+     * Handlers to invoke for releasing resources after the prefetch operation is completed.
+     */
+    private Disposable[] cleaners;
+
+    /**
+     * Number of valid elements in the {@link #cleaners} array.
+     */
+    private int cleanerCount;
+
+    /**
+     * If an error occurred while invoking {@code prefetch(…)} or {@code dispose()} on a source, that error.
+     * An error during disposal will not prevent other handlers to be disposed as well. If errors also occur
+     * during the disposal of other handlers, the other exceptions are added as suppressed exceptions.
+     */
+    private RuntimeException error;
+
+    /**
+     * Prepares (without launching) a prefetch operation using the given source images.
+     *
+     * @param  images  sources on which to apply a prefetch operation.
+     * @param  aoi     pixel coordinates of the region to prefetch.
+     */
+    MultiSourcePrefetch(final RenderedImage[] images, final Rectangle aoi) {
+        sources = new PlanarImage[images.length];
+        tileIndices = new Rectangle[images.length];
+        for (final RenderedImage source : images) {
+            if (source instanceof PlanarImage) {
+                Rectangle r = new Rectangle(aoi);
+                ImageUtilities.clipBounds(source, r);
+                r = ImageUtilities.pixelsToTiles(source, r);
+                if (!r.isEmpty()) {
+                    tileIndices[count] = r;
+                    sources[count++] = (PlanarImage) source;
+                }
+            }
+        }
+    }
+
+    /**
+     * Forwards the prefetchs calls to source images.
+     *
+     * <h4>Implementation note</h4>
+     * In many cases the background threads are not really necessary because {@code prefetch(…)} will
+     * only forward to another {@code prefetch(…)} until we reach a final {@code prefetch(…)} which
+     * happen to be a no-op. But it some cases, that final {@code prefetch(…)} will read tiles from
+     * a TIFF or netCDF file (for example).
+     *
+     * @param  parallel  whether parallelism is allowed.
+     * @return a handler for disposing resources after prefetch, or {@code null} if none.
+     */
+    final Disposable run(boolean parallel) {
+        switch (count) {
+            case 0: return null;
+            case 1: parallel = false;
+        }
+        @SuppressWarnings({"unchecked","rawtypes"})
+        final var workers = (Future<Disposable>[]) (parallel ? new Future[count] : null);
+        cleaners = new Disposable[count];
+        for (int i=0; i<count; i++) {
+            final PlanarImage source = sources[i];
+            final Rectangle r = tileIndices[i];
+            if (parallel) {
+                Callable<Disposable> worker = () -> source.prefetch(r);
+                workers[i] = CommonExecutor.instance().submit(worker);
+            } else {
+                final Disposable cleaner = source.prefetch(r);
+                if (cleaner != null) {
+                    cleaners[cleanerCount++] = cleaner;
+                }
+            }
+        }
+        /*
+         * Block until all background threads finished their work. This is needed because `PrefetchedImage`
+         * will start to query tiles after this method returned, so the source images need to be ready.
+         */
+        if (parallel) {
+            for (final Future<Disposable> worker : workers) try {
+                final Disposable cleaner = worker.get();
+                if (cleaner != null) {
+                    cleaners[cleanerCount++] = cleaner;
+                }
+            } catch (Exception e) {
+                addError(e);
+                dispose();      // Will rethrow the exception after disposal.
+            }
+        }
+        switch (cleanerCount) {
+            case 0:  return null;
+            case 1:  return cleaners[0];
+            default: return this;
+        }
+    }
+
+    /**
+     * Disposes the handlers of all sources.
+     */
+    @Override
+    public void dispose() {
+        for (int i=0; i<cleanerCount; i++) try {
+            cleaners[i].dispose();
+        } catch (Exception e) {
+            addError(e);
+        }
+        if (error != null) {
+            throw error;
+        }
+    }
+
+    /**
+     * Declares that an exception occurred. The exception will be thrown after all handlers have been disposed.
+     * If more than one exception occurs, all additional errors are added as suppressed exceptions.
+     */
+    private void addError(final Exception e) {
+        if (error != null) {
+            error.addSuppressed(e);
+        } else if (e instanceof RuntimeException) {
+            error = (RuntimeException) e;
+        } else {
+            error = (ImagingOpException) new ImagingOpException(e.getMessage()).initCause(e);
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourcesArgument.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java
similarity index 99%
rename from core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourcesArgument.java
rename to core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java
index 5978d0a5c3..27d82af545 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourcesArgument.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/MultiSourceArgument.java
@@ -50,7 +50,7 @@ import org.apache.sis.util.ComparisonMode;
  *
  * @since 1.4
  */
-public final class MultiSourcesArgument<S> {
+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
@@ -101,7 +101,7 @@ public final class MultiSourcesArgument<S> {
      * @param  sources         the sources from which to get the sample dimensions.
      * @param  bandsPerSource  sample dimensions for each source. May contain {@code null} elements.
      */
-    public MultiSourcesArgument(final S[] sources, final int[][] bandsPerSource) {
+    public MultiSourceArgument(final S[] sources, final int[][] bandsPerSource) {
         this.sources = sources;
         this.bandsPerSource = bandsPerSource;
         sourceOfGridToCRS = -1;
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 d3ec94a516..ea7641b54d 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
@@ -95,7 +95,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);
+        final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, false);
         assertNotNull(result);
         assertEquals(0, result.getMinTileX());
         assertEquals(0, result.getMinTileY());
@@ -188,7 +188,7 @@ public final class BandAggregateImageTest extends TestCase {
         initializeAllTiles(im1, im2);
         sourceImages = new RenderedImage[] {im1, im2};
 
-        RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing);
+        RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, false);
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -255,7 +255,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);
+        }, null, allowSharing, false);
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -316,7 +316,7 @@ public final class BandAggregateImageTest extends TestCase {
         initializeAllTiles(tiled2x2, tiled4x1, oneTile);
         sourceImages = new RenderedImage[] {tiled2x2, tiled4x1, oneTile};
 
-        final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing);
+        final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, false);
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
@@ -350,7 +350,25 @@ public final class BandAggregateImageTest extends TestCase {
      */
     @Test
     @DependsOnMethod("testImagesUsingSameExtentButDifferentTileSizes")
-    public void testImagesUsingDifferentExtentsAndDifferentSquaredTiling() {
+    public void testImagesUsingDifferentExtentsAndDifferentTiling() {
+        testHeterogeneous(false);
+    }
+
+    /**
+     * Tests {@link BandAggregateImage#prefetch(Rectangle)}.
+     */
+    @Test
+    @DependsOnMethod("testImagesUsingDifferentExtentsAndDifferentTiling")
+    public void testPrefetch() {
+        testHeterogeneous(true);
+    }
+
+    /**
+     * Implementation of test methods using tiles of different extents and different tile matrices.
+     *
+     * @param  prefetch  whether to test prefetch operation.
+     */
+    private void testHeterogeneous(final boolean prefetch) {
         /*
          * Tip: band number match image tile width. i.e:
          *
@@ -366,7 +384,7 @@ public final class BandAggregateImageTest extends TestCase {
         initializeAllTiles(untiled, tiled2x2, tiled4x4, tiled6x6);
         sourceImages = new RenderedImage[] {untiled, tiled2x2, tiled4x4, tiled6x6};
 
-        final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing);
+        RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing, prefetch);
         assertNotNull(result);
         assertEquals(4, result.getMinX());
         assertEquals(2, result.getMinY());
@@ -380,6 +398,9 @@ public final class BandAggregateImageTest extends TestCase {
         assertEquals(2, result.getNumYTiles());
         assertEquals(6, result.getSampleModel().getNumBands());
 
+        if (prefetch) {
+            result = new ImageProcessor().prefetch(result, new Rectangle(4, 2, 8, 4));
+        }
         final Raster raster = result.getData();
         assertEquals(new Rectangle(4, 2, 8, 4), raster.getBounds());
         assertArrayEquals(
@@ -392,7 +413,9 @@ public final class BandAggregateImageTest extends TestCase {
             },
             raster.getPixels(4, 2, 8, 4, (int[]) null)
         );
-        verifySharing(result, false, allowSharing, true, false, false, false);
+        if (!prefetch) {
+            verifySharing(result, false, allowSharing, true, false, false, false);
+        }
     }
 
     /**
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 adb90fecb4..52e0f8b141 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
@@ -26,7 +26,7 @@ import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridCoverageProcessor;
 import org.apache.sis.coverage.grid.IllegalGridGeometryException;
-import org.apache.sis.internal.coverage.MultiSourcesArgument;
+import org.apache.sis.internal.coverage.MultiSourceArgument;
 import org.apache.sis.internal.coverage.RangeArgument;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.GridCoverageResource;
@@ -163,7 +163,7 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource {
     {
         super(parent);
         try {
-            final var aggregate = new MultiSourcesArgument<GridCoverageResource>(sources, bandsPerSource);
+            final var aggregate = new MultiSourceArgument<GridCoverageResource>(sources, bandsPerSource);
             aggregate.validate(BandAggregateGridResource::range);
             this.name             = name;
             this.sources          = aggregate.sources();


[sis] 01/03: Make `getTileWidth()` and `getTileHeight()` methods final in `ComputedImage`. Add design notes in Javadoc for explaining some rational.

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 65c2a49846fd3aafb3d23415c923ed5f66a6b1f7
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Apr 8 15:27:44 2023 +0200

    Make `getTileWidth()` and `getTileHeight()` methods final in `ComputedImage`.
    Add design notes in Javadoc for explaining some rational.
---
 .../java/org/apache/sis/image/ComputedImage.java   | 36 ++++++++++++----------
 .../org/apache/sis/image/SourceAlignedImage.java   |  8 ++---
 .../sis/internal/coverage/j2d/ImageUtilities.java  | 30 ++++++++++++++++++
 3 files changed, 53 insertions(+), 21 deletions(-)

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 126fe58caf..c991a18cb4 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
@@ -43,6 +43,7 @@ import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.coverage.grid.GridExtent;     // For javadoc
 import org.apache.sis.internal.feature.Resources;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 
 
 /**
@@ -401,37 +402,41 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
      * @return the sample model of this image.
      */
     @Override
-    public SampleModel getSampleModel() {
+    public final SampleModel getSampleModel() {
         return sampleModel;
     }
 
     /**
-     * Returns the width of tiles in this image. The default implementation returns {@link SampleModel#getWidth()}.
+     * Returns the width of tiles in this image.
+     * In {@code ComputedImage} implementation, this is fixed to {@link SampleModel#getWidth()}.
      *
-     * <h4>Note</h4>
-     * A raster can have a smaller width than its sample model, for example when a raster is a view over a subregion
-     * of another raster. But this is not recommended in the particular case of this {@code ComputedImage} class,
-     * because it would cause {@link #createTile(int, int)} to consume more memory than necessary.
+     * <h4>Design note</h4>
+     * In theory it is legal to have a tile width smaller than the sample model width,
+     * for example when a raster is a view over a subregion of another raster.
+     * But this is not allowed in {@code ComputedImage} class, because it would
+     * cause {@link #createTile(int, int)} to consume more memory than necessary.
      *
      * @return the width of this image in pixels.
      */
     @Override
-    public int getTileWidth() {
+    public final int getTileWidth() {
         return sampleModel.getWidth();
     }
 
     /**
-     * Returns the height of tiles in this image. The default implementation returns {@link SampleModel#getHeight()}.
+     * Returns the height of tiles in this image.
+     * In {@code ComputedImage} implementation, this is fixed to {@link SampleModel#getHeight()}.
      *
-     * <h4>Note</h4>
-     * A raster can have a smaller height than its sample model, for example when a raster is a view over a subregion
-     * of another raster. But this is not recommended in the particular case of this {@code ComputedImage} class,
-     * because it would cause {@link #createTile(int, int)} to consume more memory than necessary.
+     * <h4>Design note</h4>
+     * In theory it is legal to have a tile height smaller than the sample model height,
+     * for example when a raster is a view over a subregion of another raster.
+     * But this is not allowed in {@code ComputedImage} class, because it would
+     * cause {@link #createTile(int, int)} to consume more memory than necessary.
      *
      * @return the height of this image in pixels.
      */
     @Override
-    public int getTileHeight() {
+    public final int getTileHeight() {
         return sampleModel.getHeight();
     }
 
@@ -588,9 +593,8 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
      * @return initially empty tile for the given indices (cannot be null).
      */
     protected WritableRaster createTile(final int tileX, final int tileY) {
-        // A temporary `int` overflow may occur before the final addition.
-        final int x = Math.toIntExact((((long) tileX) - getMinTileX()) * getTileWidth()  + getMinX());
-        final int y = Math.toIntExact((((long) tileY) - getMinTileY()) * getTileHeight() + getMinY());
+        final int x = ImageUtilities.tileToPixelX(this, tileX);
+        final int y = ImageUtilities.tileToPixelY(this, tileY);
         return WritableRaster.createWritableRaster(sampleModel, new Point(x,y));
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java
index 608c3a3c7e..35df3ce8c6 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/SourceAlignedImage.java
@@ -33,10 +33,10 @@ import org.apache.sis.util.Workaround;
  * Tiles in this image have the same size than tiles in the source image.
  * See {@link ComputedImage} javadoc for more information about tile computation.
  *
- * <div class="note"><b>Relationship with other classes</b><br>
+ * <h2>Relationship with other classes</h2>
  * This class is similar to {@link ImageAdapter} except that it extends {@link ComputedImage}
  * and does not forward {@link #getTile(int, int)}, {@link #getData()} and other data methods
- * to the source image.</div>
+ * to the source image.
  *
  * <h2>Sub-classing</h2>
  * Subclasses need to implement at least the {@link #computeTile(int, int, WritableRaster)} method.
@@ -46,7 +46,7 @@ import org.apache.sis.util.Workaround;
  * The {@link #equals(Object)} and {@link #hashCode()} methods should also be overridden.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.1
  */
 abstract class SourceAlignedImage extends ComputedImage {
@@ -182,8 +182,6 @@ abstract class SourceAlignedImage extends ComputedImage {
     @Override public final int getMinTileY()        {return getSource().getMinTileY();}
     @Override public final int getNumXTiles()       {return getSource().getNumXTiles();}
     @Override public final int getNumYTiles()       {return getSource().getNumYTiles();}
-    @Override public final int getTileWidth()       {return getSource().getTileWidth();}
-    @Override public final int getTileHeight()      {return getSource().getTileHeight();}
     @Override public final int getTileGridXOffset() {return getSource().getTileGridXOffset();}
     @Override public final int getTileGridYOffset() {return getSource().getTileGridYOffset();}
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
index 8b6bdbb63c..9c0280ba60 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
@@ -486,9 +486,16 @@ public final class ImageUtilities extends Static {
     /**
      * Converts a <var>x</var> pixel coordinates to a tile index.
      *
+     * <h4>Implementation note</h4>
+     * This method performs its calculation using <cite>tile grid offset</cite> instead of minimum coordinate
+     * values because the former does not assume that image coordinates start at the beginning of first tile.
+     * In practice it would be risky to have image {@code minX} different than first tile {@code minX},
+     * but Apache SIS tries to handle the most general cases when possible.
+     *
      * @param  image  the image containing tiles.
      * @param  x      the pixel coordinate for which to get tile index.
      * @return tile index for the given pixel coordinate.
+     * @throws ArithmeticException if the result overflows 32 bits integer.
      */
     public static int pixelToTileX(final RenderedImage image, final int x) {
         return toIntExact(floorDiv((x - (long) image.getTileGridXOffset()), image.getTileWidth()));
@@ -496,10 +503,12 @@ public final class ImageUtilities extends Static {
 
     /**
      * Converts a <var>y</var> pixel coordinates to a tile index.
+     * See {@link #pixelToTileX(RenderedImage, int)} for an implementation note.
      *
      * @param  image  the image containing tiles.
      * @param  y      the pixel coordinate for which to get tile index.
      * @return tile index for the given pixel coordinate.
+     * @throws ArithmeticException if the result overflows 32 bits integer.
      */
     public static int pixelToTileY(final RenderedImage image, final int y) {
         return toIntExact(floorDiv((y - (long) image.getTileGridYOffset()), image.getTileHeight()));
@@ -509,9 +518,16 @@ public final class ImageUtilities extends Static {
      * Converts a tile column index to smallest <var>x</var> pixel coordinate inside the tile.
      * The returned value is a coordinate of the pixel in upper-left corner.
      *
+     * <h4>Implementation note</h4>
+     * This method performs its calculation using <cite>tile grid offset</cite> instead of minimum coordinate
+     * values because the former does not assume that image coordinates start at the beginning of first tile.
+     * In practice it would be risky to have image {@code minX} different than first tile {@code minX},
+     * but Apache SIS tries to handle the most general cases when possible.
+     *
      * @param  image  the image containing tiles.
      * @param  tileX  the tile index for which to get pixel coordinate.
      * @return smallest <var>x</var> pixel coordinate inside the tile.
+     * @throws ArithmeticException if the result overflows 32 bits integer.
      */
     public static int tileToPixelX(final RenderedImage image, final int tileX) {
         // Following `long` arithmetic never overflows even if all values are `Integer.MAX_VALUE`.
@@ -521,10 +537,12 @@ public final class ImageUtilities extends Static {
     /**
      * Converts a tile row index to smallest <var>y</var> pixel coordinate inside the tile.
      * The returned value is a coordinate of the pixel in upper-left corner.
+     * See {@link #tileToPixelX(RenderedImage, int)} for an implementation note.
      *
      * @param  image  the image containing tiles.
      * @param  tileY  the tile index for which to get pixel coordinate.
      * @return smallest <var>y</var> pixel coordinate inside the tile.
+     * @throws ArithmeticException if the result overflows 32 bits integer.
      */
     public static int tileToPixelY(final RenderedImage image, final int tileY) {
         return toIntExact(multiplyFull(tileY, image.getTileHeight()) + image.getTileGridYOffset());
@@ -534,9 +552,15 @@ public final class ImageUtilities extends Static {
      * Converts pixel coordinates to pixel indices.
      * This method does <strong>not</strong> clip the rectangle to image bounds.
      *
+     * <h4>Implementation note</h4>
+     * This method performs its calculation using <cite>tile grid offset</cite> instead of minimum coordinate
+     * values because the former does not assume that image coordinates start at the beginning of first tile.
+     * The intend is to be consistent with {@link #pixelToTileX(RenderedImage, int)}.
+     *
      * @param  image   the image containing tiles.
      * @param  pixels  the pixel coordinates for which to get tile indices.
      * @return tile indices that fully contain the pixel coordinates.
+     * @throws ArithmeticException if the result overflows 32 bits integer.
      */
     public static Rectangle pixelsToTiles(final RenderedImage image, final Rectangle pixels) {
         final Rectangle r = new Rectangle();
@@ -562,9 +586,15 @@ public final class ImageUtilities extends Static {
      * Tiles will be fully included in the returned range of pixel indices.
      * This method does <strong>not</strong> clip the rectangle to image bounds.
      *
+     * <h4>Implementation note</h4>
+     * This method performs its calculation using <cite>tile grid offset</cite> instead of minimum coordinate
+     * values because the former does not assume that image coordinates start at the beginning of first tile.
+     * The intend is to be consistent with {@link #tileToPixelX(RenderedImage, int)}.
+     *
      * @param  image  the image containing tiles.
      * @param  tiles  the tile indices for which to get pixel coordinates.
      * @return pixel coordinates that fully contain the tiles.
+     * @throws ArithmeticException if the result overflows 32 bits integer.
      */
     public static Rectangle tilesToPixels(final RenderedImage image, final Rectangle tiles) {
         final Rectangle r = new Rectangle();