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 2021/07/29 13:07:15 UTC

[sis] branch geoapi-4.0 updated (8b96787 -> d53b8a5)

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 8b96787  Rename `SimpleQuery` as `FeatureQuery` and adapt property/method names to terminology used in OGC Filter specification ("projection" and "selection").
     new 7544551  Define a mapping between `GeometryType` and the integer codes used in Well-Known Binary (WKB).
     new 01cd18c  Replace `DataType.ordinal()` by `DataType.toDataBufferType()` for clarity. Various javadoc fixes.
     new a061e01  Put in place the abstract classes needed for uncompression. Support uncompressed image of bytes for testing and for the case with subsampling along X axis (in which case we can not use `HyperRectangleReader`).
     new 23ff7d0  Fix the case where subsampling is larger than tile size.
     new d06a9b1  Add AbstractGridCoverage.RangeArgument.select(SampleModel)` method with capability to "compress" the sample model for skipping bands at reading time (instead of ignoring them after they are in memory).
     new 973c7c0  Get `SampleModel` and `ColorModel` managed by `TiledGridCoverage` base class. Add `TiledGridCoverage.selectedBands` field and clarifications in Javadoc.
     new d53b8a5  Take in account the band subset specified by the `range` argument in a call to `GridCoverage.render(…)` on a GeoTIFF image.

The 7 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:
 .../org/apache/sis/coverage/grid/GridCoverage.java |   2 +-
 .../main/java/org/apache/sis/image/DataType.java   |  21 +-
 .../java/org/apache/sis/image/ImageProcessor.java  |   2 +-
 .../sis/internal/coverage/j2d/RasterFactory.java   |  29 +-
 .../internal/coverage/j2d/SampleModelFactory.java  | 295 +++++++++++++++++++++
 .../apache/sis/internal/feature/GeometryType.java  |  77 ++++++
 .../java/org/apache/sis/image/DataTypeTest.java    |  12 +-
 .../coverage/j2d/SampleModelFactoryTest.java       | 148 +++++++++++
 .../GeometryTypeTest.java}                         |  28 +-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   2 +
 .../org/apache/sis/internal/geotiff/Inflater.java  | 110 ++++++++
 .../apache/sis/internal/geotiff/Uncompressed.java  | 160 +++++++++++
 .../apache/sis/internal/geotiff/package-info.java  |   6 +-
 .../sis/storage/geotiff/CompressedSubset.java      | 197 ++++++++++++++
 .../org/apache/sis/storage/geotiff/DataCube.java   |  50 ++--
 .../org/apache/sis/storage/geotiff/DataSubset.java | 214 ++++++++++-----
 .../sis/storage/geotiff/ImageFileDirectory.java    |  55 ++--
 .../sis/storage/geotiff/SelfConsistencyTest.java   |   2 +-
 .../sis/storage/netcdf/SelfConsistencyTest.java    |   2 +-
 .../sis/internal/storage/AbstractGridResource.java | 132 +++++++--
 .../sis/internal/storage/TiledGridCoverage.java    |  61 +++--
 .../sis/internal/storage/TiledGridResource.java    | 138 ++++++++--
 .../sis/internal/storage/io/ChannelData.java       |  18 +-
 .../internal/storage/io/HyperRectangleReader.java  |  11 +
 .../sis/test/storage/CoverageReadConsistency.java  | 214 ++++++++++++---
 25 files changed, 1727 insertions(+), 259 deletions(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/SampleModelFactory.java
 create mode 100644 core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/SampleModelFactoryTest.java
 copy core/sis-feature/src/test/java/org/apache/sis/internal/{filter/sqlmm/SQLMMTest.java => feature/GeometryTypeTest.java} (53%)
 create mode 100644 storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
 create mode 100644 storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
 create mode 100644 storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java

[sis] 02/07: Replace `DataType.ordinal()` by `DataType.toDataBufferType()` for clarity. Various javadoc fixes.

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 01cd18c065395fd0421893acade7a667def32080
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jul 23 15:13:56 2021 +0200

    Replace `DataType.ordinal()` by `DataType.toDataBufferType()` for clarity.
    Various javadoc fixes.
---
 .../org/apache/sis/coverage/grid/GridCoverage.java  |  2 +-
 .../main/java/org/apache/sis/image/DataType.java    | 21 +++++++++++++++++----
 .../java/org/apache/sis/image/ImageProcessor.java   |  2 +-
 .../sis/internal/coverage/j2d/RasterFactory.java    |  4 ++--
 .../java/org/apache/sis/image/DataTypeTest.java     | 12 ++++++------
 .../org/apache/sis/storage/geotiff/DataSubset.java  |  8 ++++----
 .../sis/storage/geotiff/ImageFileDirectory.java     |  2 +-
 .../sis/storage/geotiff/SelfConsistencyTest.java    |  2 +-
 .../sis/storage/netcdf/SelfConsistencyTest.java     |  2 +-
 9 files changed, 34 insertions(+), 21 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index e0debbd..bca11bb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -261,7 +261,7 @@ public abstract class GridCoverage extends BandedCoverage {
         final Colorizer colorizer = new Colorizer(Colorizer.GRAYSCALE);
         final ColorModel colors;
         if (colorizer.initialize(sampleDimensions[visibleBand]) || colorizer.initialize(source.getColorModel())) {
-            colors = colorizer.createColorModel(bandType.ordinal(), sampleDimensions.length, visibleBand);
+            colors = colorizer.createColorModel(bandType.toDataBufferType(), sampleDimensions.length, visibleBand);
         } else {
             colors = Colorizer.NULL_COLOR_MODEL;
         }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/DataType.java b/core/sis-feature/src/main/java/org/apache/sis/image/DataType.java
index 29a84a7..f4c1c19 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/DataType.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/DataType.java
@@ -180,6 +180,8 @@ public enum DataType {
 
     /**
      * Returns the enumeration value for the given {@link DataBuffer} constant.
+     * This method is the converse of {@link #toDataBufferType()}.
+     * It is provided for interoperability with Java2D.
      *
      * @param  type  one of {@code DataBuffer.TYPE_*} constants.
      * @return the data type (never {@code null}) for the given data buffer type.
@@ -198,7 +200,7 @@ public enum DataType {
      *
      * @return size in bits of this data type.
      */
-    public int size() {
+    public final int size() {
         return DataBuffer.getDataTypeSize(ordinal());
     }
 
@@ -208,7 +210,7 @@ public enum DataType {
      *
      * @return {@code true} if this type is an unsigned integer type.
      */
-    public boolean isUnsigned() {
+    public final boolean isUnsigned() {
         return ordinal() <= DataBuffer.TYPE_USHORT;
     }
 
@@ -218,7 +220,7 @@ public enum DataType {
      *
      * @return {@code true} if this type is an integer type.
      */
-    public boolean isInteger() {
+    public final boolean isInteger() {
         return ordinal() <= DataBuffer.TYPE_INT;
     }
 
@@ -237,8 +239,19 @@ public enum DataType {
      * @return the smallest of {@link #FLOAT} or {@link #DOUBLE} types
      *         which can store all values of this type without any lost.
      */
-    public DataType toFloat() {
+    public final DataType toFloat() {
         final int type = ordinal();
         return (type < DataBuffer.TYPE_INT || type == DataBuffer.TYPE_FLOAT) ? FLOAT : DOUBLE;
     }
+
+    /**
+     * Returns the {@link DataBuffer} constant for this enumeration value.
+     * This method is the converse of {@link #forDataBufferType(int)}.
+     * It is provided for interoperability with Java2D.
+     *
+     * @return one of the {@link DataBuffer} constants.
+     */
+    public final int toDataBufferType() {
+        return ordinal();
+    }
 }
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 94e1d36..3b85591 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
@@ -793,7 +793,7 @@ public class ImageProcessor implements Cloneable {
         }
         // No need to clone `sourceRanges` because it is not stored by `BandedSampleConverter`.
         return unique(BandedSampleConverter.create(source, layout,
-                sourceRanges, converters, targetType.ordinal(), colorModel));
+                sourceRanges, converters, targetType.toDataBufferType(), colorModel));
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
index a73c7dc..e350521 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
@@ -73,7 +73,7 @@ public final class RasterFactory extends Static {
      * <p><b>Warning:</b> displaying this image is very slow, except in a few special cases.
      * It should be used only when no standard color model can be used.</p>
      *
-     * @param  dataType       the color model type as one of {@code DataBuffer.TYPE_*} constants.
+     * @param  dataType       the buffer type as one of {@code DataBuffer.TYPE_*} constants.
      * @param  width          the desired image width.
      * @param  height         the desired image height.
      * @param  numComponents  the number of components.
@@ -207,7 +207,7 @@ public final class RasterFactory extends Static {
      *
      * @param  dataType  type of buffer to create.
      * @param  data      the data, one for each band.
-     * @return buffer of the given type (ever null).
+     * @return buffer of the given type (never null).
      * @throws UnsupportedOperationException if a buffer is not backed by an accessible array.
      * @throws ReadOnlyBufferException if a buffer is backed by an array but is read-only.
      * @throws ArrayStoreException if the type of a backing array is not {@code dataType}.
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/DataTypeTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/DataTypeTest.java
index 6e911ce..a0b6f60 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/DataTypeTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/DataTypeTest.java
@@ -37,12 +37,12 @@ public final strictfp class DataTypeTest extends TestCase {
      */
     @Test
     public void verifyOrdinalValues() {
-        assertEquals(DataBuffer.TYPE_BYTE  , DataType.BYTE  .ordinal());
-        assertEquals(DataBuffer.TYPE_USHORT, DataType.USHORT.ordinal());
-        assertEquals(DataBuffer.TYPE_SHORT , DataType.SHORT .ordinal());
-        assertEquals(DataBuffer.TYPE_INT   , DataType.INT   .ordinal());
-        assertEquals(DataBuffer.TYPE_FLOAT , DataType.FLOAT .ordinal());
-        assertEquals(DataBuffer.TYPE_DOUBLE, DataType.DOUBLE.ordinal());
+        assertEquals(DataBuffer.TYPE_BYTE  , DataType.BYTE  .toDataBufferType());
+        assertEquals(DataBuffer.TYPE_USHORT, DataType.USHORT.toDataBufferType());
+        assertEquals(DataBuffer.TYPE_SHORT , DataType.SHORT .toDataBufferType());
+        assertEquals(DataBuffer.TYPE_INT   , DataType.INT   .toDataBufferType());
+        assertEquals(DataBuffer.TYPE_FLOAT , DataType.FLOAT .toDataBufferType());
+        assertEquals(DataBuffer.TYPE_DOUBLE, DataType.DOUBLE.toDataBufferType());
     }
 
     /**
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
index 82eb663..c23fc34 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -283,7 +283,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
     WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[] lower, final long[] upper,
                              final int[] subsampling, final Point location) throws IOException, DataStoreException
     {
-        final int  type   = model.getDataType();
+        final DataType type = DataType.forDataBufferType(model.getDataType());
         final long width  = subtractExact(upper[0], lower[0]);
         final long height = subtractExact(upper[1], lower[1]);
         /*
@@ -305,7 +305,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
          * a subregion is read. Note that the `length` value may be different than `capacity` if the tile to read
          * is smaller than the "standard" tile size of the image. It happens often when reading the last strip.
          */
-        final long length  = multiplyExact(DataBuffer.getDataTypeSize(type) / Byte.SIZE,
+        final long length  = multiplyExact(type.size() / Byte.SIZE,
                              multiplyExact(multiplyExact(width, height), numInterleaved));
         final int capacity = multiplyExact(multiplyExact(model.getWidth(), model.getHeight()), numInterleaved);
         final long[] size = new long[] {multiplyFull(numInterleaved, getTileSize(0)), getTileSize(1)};
@@ -322,7 +322,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
          * Read each plane ("banks" in Java2D terminology). Note that a single bank contain all bands
          * in the interleaved sample model case.
          */
-        final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type), source.reader.input);
+        final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type.toDataBufferType()), source.reader.input);
         final Region region = new Region(size, lower, upper, subsampling);
         final Buffer[] banks = new Buffer[numBanks];
         for (int b=0; b<numBanks; b++) {
@@ -346,7 +346,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
             }
             banks[b] = buffer.limit(capacity);
         }
-        final DataBuffer buffer = RasterFactory.wrap(DataType.forDataBufferType(type), banks);
+        final DataBuffer buffer = RasterFactory.wrap(type, banks);
         return WritableRaster.createWritableRaster(model, buffer, location);
     }
 }
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 4f6bb4a..a71ee3a 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -1355,7 +1355,7 @@ final class ImageFileDirectory extends DataCube {
         if (sampleModel == null) {
             final DataType type = getDataType();
             try {
-                final int bt = type.ordinal();
+                final int bt = type.toDataBufferType();
                 if (bitsPerSample != type.size()) {
                     /*
                      * Supported types for both sample models are TYPE_BYTE, TYPE_USHORT, and TYPE_INT.
diff --git a/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/SelfConsistencyTest.java b/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/SelfConsistencyTest.java
index 92e0723..bd9e820 100644
--- a/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/SelfConsistencyTest.java
+++ b/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/SelfConsistencyTest.java
@@ -61,7 +61,7 @@ public final strictfp class SelfConsistencyTest extends CoverageReadConsistency
     }
 
     /**
-     * Closes the test file to be used for all tests.
+     * Closes the test file used by all tests.
      *
      * @throws DataStoreException if an error occurred while closing the file.
      */
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/SelfConsistencyTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/SelfConsistencyTest.java
index 50d9c16..fe12a72 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/SelfConsistencyTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/SelfConsistencyTest.java
@@ -62,7 +62,7 @@ public final strictfp class SelfConsistencyTest extends CoverageReadConsistency
     }
 
     /**
-     * Closes the test file to be used for all tests.
+     * Closes the test file used by all tests.
      *
      * @throws DataStoreException if an error occurred while closing the file.
      */

[sis] 04/07: Fix the case where subsampling is larger than tile size.

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 23ff7d0843bae4b6b10eb1309a0f025426e6739f
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jul 24 23:19:58 2021 +0200

    Fix the case where subsampling is larger than tile size.
---
 .../org/apache/sis/storage/geotiff/DataSubset.java | 34 ++++++++++++++++------
 .../sis/internal/storage/TiledGridCoverage.java    |  7 ++++-
 .../sis/test/storage/CoverageReadConsistency.java  |  3 ++
 3 files changed, 34 insertions(+), 10 deletions(-)

diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
index 920ecb7..97170c6 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -292,18 +292,34 @@ class DataSubset extends TiledGridCoverage implements Localized {
             final Point  origin      = new Point();
             final long[] offsets     = new long[tileOffsets.size() / numTiles];
             final long[] byteCounts  = new long[tileByteCounts.size() / numTiles];
+            boolean needsCompaction  = false;
             for (int i=0; i<numMissings; i++) {
                 final Tile tile = missings[i];
-                origin.x = tile.originX;
-                origin.y = tile.originY;
-                tile.getRegionInsideTile(lower, upper, subsampling, BIDIMENSIONAL);
-                tile.copyTileInfo(tileOffsets,    offsets,    numTiles);
-                tile.copyTileInfo(tileByteCounts, byteCounts, numTiles);
-                for (int b=0; b<offsets.length; b++) {
-                    offsets[b] = addExact(offsets[b], reader().origin);
+                if (tile.getRegionInsideTile(lower, upper, subsampling, BIDIMENSIONAL)) {
+                    origin.x = tile.originX;
+                    origin.y = tile.originY;
+                    tile.copyTileInfo(tileOffsets,    offsets,    numTiles);
+                    tile.copyTileInfo(tileByteCounts, byteCounts, numTiles);
+                    for (int b=0; b<offsets.length; b++) {
+                        offsets[b] = addExact(offsets[b], reader().origin);
+                    }
+                    result[tile.indexInResultArray] = tile.cache(
+                            readSlice(offsets, byteCounts, lower, upper, subsampling, origin));
+                } else {
+                    needsCompaction = true;
+                }
+            }
+            /*
+             * If the subsampling is larger than tile size, some tiles were empty and excluded.
+             * The corresponding elements in the `result` array were left to the null value.
+             * We need to compact the array by removing those null elements.
+             */
+            if (needsCompaction) {
+                int n = 0;
+                for (final WritableRaster tile : result) {
+                    if (tile != null) result[n++] = tile;
                 }
-                result[tile.indexInResultArray] = tile.cache(
-                        readSlice(offsets, byteCounts, lower, upper, subsampling, origin));
+                return Arrays.copyOf(result, n);
             }
         }
         return result;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
index 7ea234e..3b30e63 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
@@ -584,8 +584,9 @@ public abstract class TiledGridCoverage extends GridCoverage {
          * @param  upper        a pre-allocated array where to store relative coordinates after the last pixel.
          * @param  subsampling  a pre-allocated array where to store subsampling.
          * @param  dimension    number of elements to write in the {@code lower} and {@code upper} arrays.
+         * @return {@code true} on success, or {@code false} if the tile is empty.
          */
-        public void getRegionInsideTile(final long[] lower, final long[] upper, final int[] subsampling, int dimension) {
+        public boolean getRegionInsideTile(final long[] lower, final long[] upper, final int[] subsampling, int dimension) {
             System.arraycopy(coverage.subsampling, 0, subsampling, 0, dimension);
             while (--dimension >= 0) {
                 final int  tileSize  = coverage.tileSize[dimension];
@@ -620,8 +621,12 @@ public abstract class TiledGridCoverage extends GridCoverage {
                         offset += s;
                     }
                 }
+                if (offset >= upper[dimension]) {
+                    return false;
+                }
                 lower[dimension] = offset;
             }
+            return true;
         }
 
         /**
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
index 585db35..a3f5286 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
@@ -354,6 +354,9 @@ nextSlice:  for (;;) {
         if (allowSubsamplings) {
             final int subX = subsampling[0];
             final int subY = subsampling[1];
+            if (subX > image.getTileWidth() || subY > image.getTileHeight()) {
+                return null;        // `SubsampledImage` does not support this case.
+            }
             int offX = StrictMath.floorMod(image.getMinX(), subX);
             int offY = StrictMath.floorMod(image.getMinY(), subY);
             if (offX != 0) {sliceAOI.x--; offX = subX - offX;}

[sis] 03/07: Put in place the abstract classes needed for uncompression. Support uncompressed image of bytes for testing and for the case with subsampling along X axis (in which case we can not use `HyperRectangleReader`).

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 a061e01940ae3660a6110e0fbac19adc8f7c4411
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jul 23 19:28:33 2021 +0200

    Put in place the abstract classes needed for uncompression.
    Support uncompressed image of bytes for testing and for the case with subsampling along X axis (in which case we can not use `HyperRectangleReader`).
---
 .../sis/internal/coverage/j2d/RasterFactory.java   |  25 ++++
 .../org/apache/sis/internal/geotiff/Inflater.java  |  98 +++++++++++++
 .../apache/sis/internal/geotiff/Uncompressed.java  | 151 +++++++++++++++++++++
 .../apache/sis/internal/geotiff/package-info.java  |   6 +-
 .../sis/storage/geotiff/CompressedSubset.java      | 117 ++++++++++++++++
 .../org/apache/sis/storage/geotiff/DataCube.java   |  18 ++-
 .../org/apache/sis/storage/geotiff/DataSubset.java | 119 ++++++++++------
 .../sis/internal/storage/TiledGridResource.java    |  10 ++
 .../sis/internal/storage/io/ChannelData.java       |  18 ++-
 .../sis/test/storage/CoverageReadConsistency.java  |  37 ++++-
 10 files changed, 550 insertions(+), 49 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
index e350521..0afb1a4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/RasterFactory.java
@@ -33,6 +33,11 @@ import java.awt.image.RasterFormatException;
 import java.awt.image.WritableRaster;
 import java.awt.image.BufferedImage;
 import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.nio.IntBuffer;
+import java.nio.FloatBuffer;
+import java.nio.DoubleBuffer;
 import java.nio.ReadOnlyBufferException;
 import org.apache.sis.image.DataType;
 import org.apache.sis.internal.feature.Resources;
@@ -200,6 +205,26 @@ public final class RasterFactory extends Static {
     }
 
     /**
+     * Creates a NIO buffer of the specified capacity.
+     * The buffer position will be 0 and its limit will be its capacity.
+     *
+     * @param  dataType  type of buffer to create.
+     * @param  capacity  the {@code Buffer} size.
+     * @return buffer of the specified type and size.
+     */
+    public static Buffer createBuffer(final DataType dataType, final int capacity) {
+        switch (dataType) {
+            case USHORT: // Fallthrough
+            case SHORT:  return ShortBuffer .allocate(capacity);
+            case BYTE:   return ByteBuffer  .allocate(capacity);
+            case INT:    return IntBuffer   .allocate(capacity);
+            case FLOAT:  return FloatBuffer .allocate(capacity);
+            case DOUBLE: return DoubleBuffer.allocate(capacity);
+            default: throw new AssertionError(dataType);
+        }
+    }
+
+    /**
      * Wraps the backing arrays of given NIO buffers into Java2D buffers.
      * This method wraps the underlying array of primitive types; data are not copied.
      * For each buffer, the data starts at {@linkplain Buffer#position() buffer position}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
new file mode 100644
index 0000000..e5fa40c
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.geotiff;
+
+import java.io.IOException;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+
+import static org.apache.sis.util.ArgumentChecks.ensureStrictlyPositive;
+import static org.apache.sis.util.ArgumentChecks.ensurePositive;
+import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+
+
+/**
+ * Decompression algorithm.
+ * Decompression is applied row-by-row.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public abstract class Inflater {
+    /**
+     * The source of data to decompress.
+     */
+    protected final ChannelDataInput input;
+
+    /**
+     * Number of chunk per row, as a strictly positive integer.
+     * A chunk is a pixel, except if we can optimize by reading the whole row as a single chunk.
+     */
+    protected final int chunksPerRow;
+
+    /**
+     * Number of sample values per chunk, as a strictly positive integer.
+     * A chunk is a pixel, except if we can optimize by reading the whole row as a single chunk.
+     */
+    protected final int samplesPerChunk;
+
+    /**
+     * Number of sample values to skip between pixels. Positive but often zero.
+     */
+    protected final int interpixels;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param  input            the source of data to decompress.
+     * @param  pixelsPerRow     number of pixels per row. Must be strictly positive.
+     * @param  samplesPerPixel  number of sample values per pixel. Must be strictly positive.
+     * @param  interpixels      number of sample values to skip between pixels. May be zero.
+     */
+    protected Inflater(final ChannelDataInput input, final int pixelsPerRow, final int samplesPerPixel, final int interpixels) {
+        ensureStrictlyPositive("pixelsPerRow",    pixelsPerRow);
+        ensureStrictlyPositive("samplesPerPixel", samplesPerPixel);
+        ensurePositive        ("interpixels",     interpixels);
+        ensureNonNull         ("input",           input);
+        if (interpixels == 0) {
+            chunksPerRow    = 1;
+            samplesPerChunk = Math.multiplyExact(samplesPerPixel, pixelsPerRow);
+        } else {
+            chunksPerRow    = pixelsPerRow;
+            samplesPerChunk = samplesPerPixel;
+        }
+        this.interpixels = interpixels;
+        this.input       = input;
+    }
+
+    /**
+     * Reads a row of sample values and stores them in the target buffer.
+     *
+     * @throws IOException if an error occurred while reading the input channel.
+     */
+    public abstract void uncompressRow() throws IOException;
+
+    /**
+     * Reads the given amount of sample values without storing them.
+     * The given value is in units of sample values, not in bytes.
+     *
+     * @param  n  number of uncompressed sample values to ignore.
+     * @throws IOException if an error occurred while reading the input channel.
+     */
+    public abstract void skip(long n) throws IOException;
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
new file mode 100644
index 0000000..3cb3ce5
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.geotiff;
+
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.Classes;
+
+
+/**
+ * A pseudo-inflater which copy values unchanged.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public abstract class Uncompressed extends Inflater {
+    /**
+     * Stream position where to perform the next reading.
+     */
+    private long streamPosition;
+
+    /**
+     * Whether {@link #streamPosition} needs to be refreshed by
+     * a call to {@link ChannelDataInput#getStreamPosition()}.
+     */
+    private boolean positionNeedsRefresh;
+
+    /**
+     * Number of bytes in a sample value.
+     */
+    private final int sampleSize;
+
+    /**
+     * For constructors in inner classes.
+     */
+    private Uncompressed(ChannelDataInput input, long start, int pixelsPerRow, int samplesPerPixel, int interpixels, final int sampleSize) {
+        super(input, pixelsPerRow, samplesPerPixel, interpixels);
+        this.streamPosition = start;
+        this.sampleSize = sampleSize;
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param  input            the source of data to decompress.
+     * @param  start            stream position where to start reading.
+     * @param  pixelsPerRow     number of pixels per row. Must be strictly positive.
+     * @param  samplesPerPixel  number of sample values per pixel. Must be strictly positive.
+     * @param  interpixels      number of sample values to skip between pixels. May be zero.
+     * @param  target           where to store sample values.
+     * @return the inflater for the given targe type.
+     * @throws IllegalArgumentException if the buffer type is not recognized.
+     */
+    public static Uncompressed create(final ChannelDataInput input, final long start,
+            final int pixelsPerRow, final int samplesPerPixel, final int interpixels, final Buffer target)
+    {
+        if (target instanceof ByteBuffer) return new Bytes(input, start, pixelsPerRow, samplesPerPixel, interpixels, (ByteBuffer) target);
+        throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, Classes.getClass(target)));
+    }
+
+    /**
+     * Reads a row of sample values and stores them in the target buffer.
+     * Subclasses must override and invoke {@code super.uncompress()} before to do the actual reading.
+     */
+    @Override
+    public void uncompressRow() throws IOException {
+        if (!positionNeedsRefresh) {
+            positionNeedsRefresh = true;
+            input.seek(streamPosition);
+        }
+    }
+
+    /**
+     * Skips the given amount of sample values without storing them.
+     * The given value is in units of sample values, not in bytes.
+     *
+     * @param  n  number of uncompressed sample values to ignore.
+     * @throws IOException if an error occurred while reading the input channel.
+     */
+    @Override
+    public final void skip(final long n) throws IOException {
+        if (n != 0) {
+            if (positionNeedsRefresh) {
+                positionNeedsRefresh = false;
+                streamPosition = input.getStreamPosition();
+            }
+            streamPosition = Math.addExact(streamPosition, n * sampleSize);
+        }
+    }
+
+    /**
+     * Inflater when the values to read and store are bytes.
+     */
+    private static final class Bytes extends Uncompressed {
+        /** Where to copy the values that we will read. */
+        private final ByteBuffer target;
+
+        /** Creates a new inflater which will write in the given buffer. */
+        Bytes(ChannelDataInput input, long start, int pixelsPerRow, int samplesPerPixel, int interpixels, ByteBuffer target) {
+            super(input, start, pixelsPerRow, samplesPerPixel, interpixels, Byte.BYTES);
+            this.target = target;
+        }
+
+        /**
+         * Reads and decompress a row of sample values.
+         */
+        @Override public void uncompressRow() throws IOException {
+            super.uncompressRow();
+            for (int i = chunksPerRow; --i > 0;) {      // (chunksPerRow - 1) iterations.
+                int n = samplesPerChunk;
+                do target.put(input.readByte());
+                while (--n != 0);
+                /*
+                 * Following loop is executed only if there is subsampling on the X axis.
+                 * We invoke `readByte()` in a loop instead of invoking `skip` because if
+                 * the number of bytes to skip is small, this is more efficient.
+                 */
+                for (n = interpixels; --n >= 0;) {
+                    input.readByte();
+                }
+            }
+            /*
+             * Read the last element that was not read in first above `for` loop, but without
+             * skipping `interpixels` values after it. This is necessary for avoiding EOF if
+             * the last pixel to read is in the last column of the tile.
+             */
+            int n = samplesPerChunk;
+            do target.put(input.readByte());
+            while (--n != 0);
+        }
+    }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
index 65e8c85..a802b5e 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/package-info.java
@@ -23,8 +23,12 @@
  * This package is for internal use by SIS only. Classes in this package
  * may change in incompatible ways in any future version without notice.
  *
+ * <p>Current version contains the classes for decompressing sample values.
+ * Those classes may move in another package in a future version if we want
+ * to share them with other image formats.</p>
+ *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.1
  * @since   0.8
  * @module
  */
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
new file mode 100644
index 0000000..bf1b2b2
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
@@ -0,0 +1,117 @@
+/*
+ * 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.storage.geotiff;
+
+import java.awt.Point;
+import java.awt.image.WritableRaster;
+import java.io.IOException;
+import java.nio.Buffer;
+import org.apache.sis.internal.storage.TiledGridResource;
+import org.apache.sis.internal.coverage.j2d.RasterFactory;
+import org.apache.sis.internal.geotiff.Uncompressed;
+import org.apache.sis.internal.geotiff.Inflater;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.image.DataType;
+
+import static java.lang.Math.toIntExact;
+import static org.apache.sis.internal.jdk9.JDK9.multiplyFull;
+
+
+/**
+ * Raster data obtained from a compressed GeoTIFF file in the domain requested by user.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class CompressedSubset extends DataSubset {
+    /**
+     * Number of sample values to skip for moving to the next row of a tile.
+     */
+    private final long scanlineStride;
+
+    /**
+     * Creates a new data subset. All parameters should have been validated
+     * by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
+     * This constructor should be invoked inside a synchronized block.
+     *
+     * @param  source   the resource which contain this {@code DataSubset}.
+     * @param  subset   description of the {@code owner} subset to cover.
+     * @param  rasters  potentially shared cache of rasters read by this {@code DataSubset}.
+     * @throws ArithmeticException if the number of tiles overflows 32 bits integer arithmetic.
+     */
+    CompressedSubset(final DataCube source, final TiledGridResource.Subset subset) throws DataStoreException {
+        super(source, subset);
+        scanlineStride = multiplyFull(getTileSize(0), numInterleaved);
+    }
+
+    /**
+     * Computes the number of pixels to read in dimension <var>i</var>
+     * The arguments given to this method are the ones given to the {@code readSlice(…)} method.
+     */
+    private static int pixelCount(final long[] lower, final long[] upper, final int[] subsampling, final int i) {
+        final int n = toIntExact((upper[i] - lower[i] - 1) / subsampling[i] + 1);
+        assert (n > 0) : n;
+        return n;
+    }
+
+    /**
+     * Reads a two-dimensional slice of the data cube from the given input channel.
+     *
+     * @param  offsets      position in the channel where tile data begins, one value per bank.
+     * @param  byteCounts   number of bytes for the compressed tile data, one value per bank.
+     * @param  lower        (<var>x</var>, <var>y</var>) coordinates of the first pixel to read relative to the tile.
+     * @param  upper        (<var>x</var>, <var>y</var>) coordinates after the last pixel to read relative to the tile.
+     * @param  subsampling  (<var>sx</var>, <var>sy</var>) subsampling factors.
+     * @param  location     pixel coordinates in the upper-left corner of the tile to return.
+     * @return image decoded from the GeoTIFF file.
+     */
+    @Override
+    WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[] lower, final long[] upper,
+                             final int[] subsampling, final Point location) throws IOException, DataStoreException
+    {
+        final DataType type   = getDataType();
+        final int      width  = pixelCount(lower, upper, subsampling, 0);
+        final int      height = pixelCount(lower, upper, subsampling, 1);
+        final int      skipY  = subsampling[1] - 1;
+        final int      skipX  = numInterleaved * (subsampling[0] - 1);
+        final long     head   = numInterleaved * lower[0];
+        final long     tail   = numInterleaved * (getTileSize(0) - (width*subsampling[0] + lower[0])) + skipX;
+        final Buffer[] banks  = new Buffer[numBanks];
+        for (int b=0; b<numBanks; b++) {
+            final Buffer   bank = RasterFactory.createBuffer(type, capacity);
+            final Inflater algo = Uncompressed.create(reader().input, offsets[b], width, numInterleaved, skipX, bank);
+            for (long y = lower[1]; --y >= 0;) {
+                algo.skip(scanlineStride);
+            }
+            for (int y = height; --y > 0;) {        // (height - 1) iterations.
+                algo.skip(head);
+                algo.uncompressRow();
+                algo.skip(tail);
+                for (int j=skipY; --j>=0;) {
+                    algo.skip(scanlineStride);
+                }
+            }
+            algo.skip(head);                        // Last iteration without last `skip(…)` calls.
+            algo.uncompressRow();
+            fillRemainingRows(bank.flip());
+            banks[b] = bank;
+        }
+        return WritableRaster.createWritableRaster(model, RasterFactory.wrap(type, banks), location);
+    }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
index 1cbc0c1..0c2794d 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
@@ -19,6 +19,7 @@ package org.apache.sis.storage.geotiff;
 import java.nio.file.Path;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.coverage.grid.GridCoverage;
@@ -121,6 +122,14 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
     abstract Compression getCompression();
 
     /**
+     * Returns {@code true} if the sample model interleaves 2 or more sample values per pixel.
+     */
+    private boolean isInterleaved() throws DataStoreException {
+        final SampleModel model = getSampleModel();
+        return model.getNumBands() != 1 && !(model instanceof BandedSampleModel);
+    }
+
+    /**
      * Creates a {@link GridCoverage} which will load pixel data in the given domain.
      *
      * @param  domain  desired grid extent and resolution, or {@code null} for reading the whole domain.
@@ -141,7 +150,14 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
                             Resources.Keys.MissingValue_2, Tags.name(Tags.Compression)));
                 }
                 switch (compression) {
-                    case NONE: coverage = new DataSubset(this, subset); break;
+                    case NONE: {
+                        if (subset.hasSubsampling(0) && isInterleaved()) {
+                            coverage = new CompressedSubset(this, subset);
+                        } else {
+                            coverage = new DataSubset(this, subset);
+                        }
+                        break;
+                    }
                     default: {
                         throw new DataStoreContentException(reader.resources().getString(
                                 Resources.Keys.UnsupportedCompressionMethod_1, compression));
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
index c23fc34..920ecb7 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -27,7 +27,6 @@ import java.awt.image.WritableRaster;
 import org.apache.sis.image.DataType;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.InternalDataStoreException;
 import org.apache.sis.internal.storage.io.Region;
 import org.apache.sis.internal.storage.io.HyperRectangleReader;
 import org.apache.sis.internal.storage.TiledGridCoverage;
@@ -101,6 +100,25 @@ class DataSubset extends TiledGridCoverage implements Localized {
     private final int numTiles;
 
     /**
+     * Number of banks in the image data buffer.
+     * This is equal to the number of bands only for planar images, and 1 in all other cases.
+     */
+    protected final int numBanks;
+
+    /**
+     * Number of interleaved sample values in a pixel. For planar images, this is equal to 1.
+     * For interleaved sample model, this is equal to the number of bands. This value is often
+     * equal to the {@linkplain java.awt.image.ComponentSampleModel#getPixelStride() pixel stride}.
+     */
+    protected final int numInterleaved;
+
+    /**
+     * Number of sample values in a bank (not necessarily a band).
+     * This is tile width × height × {@link #numInterleaved}.
+     */
+    protected final int capacity;
+
+    /**
      * Creates a new data subset. All parameters should have been validated
      * by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
      * This constructor should be invoked inside a synchronized block.
@@ -117,6 +135,23 @@ class DataSubset extends TiledGridCoverage implements Localized {
         final Vector[] tileArrayInfo = source.getTileArrayInfo();
         this.tileOffsets    = tileArrayInfo[0];
         this.tileByteCounts = tileArrayInfo[1];
+        /*
+         * "Banks" (in `java.awt.image.DataBuffer` sense) are synonymous to "bands" for planar image only.
+         * Otherwise there is only one bank no matter the amount of bands. Each bank will be read separately.
+         */
+        if (model instanceof BandedSampleModel) {
+            numBanks = model.getNumBands();
+            numInterleaved = 1;
+        } else {
+            numBanks = 1;
+            numInterleaved = model.getNumBands();
+        }
+        final int n = tileOffsets.size();
+        if (numBanks > n / numTiles) {
+            throw new DataStoreContentException(source.reader.errors().getString(
+                    Errors.Keys.TooFewCollectionElements_3, "tileOffsets", numBanks * numTiles, n));
+        }
+        capacity = multiplyExact(multiplyExact(model.getWidth(), model.getHeight()), numInterleaved);
     }
 
     /**
@@ -136,6 +171,20 @@ class DataSubset extends TiledGridCoverage implements Localized {
     }
 
     /**
+     * Returns the type of data in all tiles.
+     */
+    protected final DataType getDataType() {
+        return DataType.forDataBufferType(model.getDataType());
+    }
+
+    /**
+     * Returns the GeoTIFF reader which contains this subset.
+     */
+    final Reader reader() {
+        return source.reader;
+    }
+
+    /**
      * Information about a tile to be read. A list of {@code Tile} is created and sorted by increasing offsets
      * before the read operation begins, in order to read tiles in the order they are written in the TIFF file.
      */
@@ -218,7 +267,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
         final WritableRaster[] result = new WritableRaster[iterator.tileCountInQuery];
         final Tile[] missings = new Tile[iterator.tileCountInQuery];
         int numMissings = 0;
-        synchronized (source.reader.store) {
+        synchronized (reader().store) {
             do {
                 final WritableRaster tile = iterator.getCachedTile();
                 if (tile != null) {
@@ -250,6 +299,9 @@ class DataSubset extends TiledGridCoverage implements Localized {
                 tile.getRegionInsideTile(lower, upper, subsampling, BIDIMENSIONAL);
                 tile.copyTileInfo(tileOffsets,    offsets,    numTiles);
                 tile.copyTileInfo(tileByteCounts, byteCounts, numTiles);
+                for (int b=0; b<offsets.length; b++) {
+                    offsets[b] = addExact(offsets[b], reader().origin);
+                }
                 result[tile.indexInResultArray] = tile.cache(
                         readSlice(offsets, byteCounts, lower, upper, subsampling, origin));
             }
@@ -283,70 +335,61 @@ class DataSubset extends TiledGridCoverage implements Localized {
     WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[] lower, final long[] upper,
                              final int[] subsampling, final Point location) throws IOException, DataStoreException
     {
-        final DataType type = DataType.forDataBufferType(model.getDataType());
+        final DataType type = getDataType();
         final long width  = subtractExact(upper[0], lower[0]);
         final long height = subtractExact(upper[1], lower[1]);
         /*
-         * "Banks" (in `java.awt.image.DataBuffer` sense) are synonymous to "bands" for planar image only.
-         * Otherwise there is only one bank not matter the amount of bands. Each bank is read separately.
-         */
-        int numBanks = 1;
-        int numInterleaved = model.getNumBands();
-        if (model instanceof BandedSampleModel) {
-            numBanks = numInterleaved;
-            numInterleaved = 1;
-        }
-        if (numBanks > offsets.length) {
-            throw new DataStoreContentException(source.reader.errors().getString(
-                    Errors.Keys.TooFewCollectionElements_3, "tileOffsets", numBanks * numTiles, tileOffsets.size()));
-        }
-        /*
          * The number of bytes to read should not be greater than `byteCount`. It may be smaller however if only
          * a subregion is read. Note that the `length` value may be different than `capacity` if the tile to read
          * is smaller than the "standard" tile size of the image. It happens often when reading the last strip.
          */
         final long length  = multiplyExact(type.size() / Byte.SIZE,
                              multiplyExact(multiplyExact(width, height), numInterleaved));
-        final int capacity = multiplyExact(multiplyExact(model.getWidth(), model.getHeight()), numInterleaved);
         final long[] size = new long[] {multiplyFull(numInterleaved, getTileSize(0)), getTileSize(1)};
         /*
          * If we use an interleaved sample model, each "element" from `HyperRectangleReader` perspective is actually
          * a group of `numInterleaved` values. Note that in such case, we can not handle subsampling on the first axis.
+         * Such case should be handled by the `CompressedSubset` subclass instead, even if there is no compression.
          */
-        if (numInterleaved != 1 && subsampling[0] != 1) {
-            throw new InternalDataStoreException();
-        }
+        assert numInterleaved == 1 || subsampling[0] == 1;
         lower[0] *= numInterleaved;
         upper[0] *= numInterleaved;
         /*
          * Read each plane ("banks" in Java2D terminology). Note that a single bank contain all bands
          * in the interleaved sample model case.
          */
-        final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type.toDataBufferType()), source.reader.input);
+        final HyperRectangleReader hr = new HyperRectangleReader(ImageUtilities.toNumberEnum(type.toDataBufferType()), reader().input);
         final Region region = new Region(size, lower, upper, subsampling);
         final Buffer[] banks = new Buffer[numBanks];
         for (int b=0; b<numBanks; b++) {
             if (b < byteCounts.length && length > byteCounts[b]) {
-                throw new DataStoreContentException(source.reader.resources().getString(
+                throw new DataStoreContentException(reader().resources().getString(
                         Resources.Keys.UnexpectedTileLength_2, length, byteCounts[b]));
             }
-            hr.setOrigin(addExact(source.reader.origin, offsets[b]));
-            final Buffer buffer = hr.readAsBuffer(region, capacity);
-            /*
-             * The buffer returned by `readAsBuffer(…)` may have less data than the buffer capacity
-             * if the current tile is smaller than the expected tile size (e.g. truncated last tile).
-             * Following code applies the fill value if it is different than the default value (zero).
-             */
-            if (fillValue != null) {
-                final int end = buffer.limit();
-                if (end != capacity) {
-                    Vector.create(buffer.limit(capacity), ImageUtilities.isUnsignedType(model))
-                          .fill(end, capacity, fillValue);
-                }
-            }
-            banks[b] = buffer.limit(capacity);
+            hr.setOrigin(offsets[b]);
+            final Buffer bank = hr.readAsBuffer(region, capacity);
+            fillRemainingRows(bank);
+            banks[b] = bank;
         }
         final DataBuffer buffer = RasterFactory.wrap(type, banks);
         return WritableRaster.createWritableRaster(model, buffer, location);
     }
+
+    /**
+     * Applies the fill value if it is different than the default value (zero) to all remaining rows.
+     * This method is needed because the buffer filled by read methods may have less data than the buffer
+     * capacity if the current tile is smaller than the expected tile size (e.g. last tile is truncated).
+     *
+     * @param  bank  the buffer where to fill remaining rows.
+     */
+    final void fillRemainingRows(final Buffer bank) {
+        if (fillValue != null) {
+            final int end = bank.limit();
+            if (end != capacity) {
+                Vector.create(bank.limit(capacity), ImageUtilities.isUnsignedType(model))
+                      .fill(end, capacity, fillValue);
+                bank.limit(capacity);
+            }
+        }
+    }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
index fd0c6ee..8a13cbd 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
@@ -195,5 +195,15 @@ public abstract class TiledGridResource extends AbstractGridResource {
              */
             cache = sharedCache ? caller.rasters : new WeakValueHashMap<>(Integer.class);
         }
+
+        /**
+         * Returns {@code true} if this subset contains a subsampling in the given dimension.
+         *
+         * @param  dimension  the dimension to test.
+         * @return {@code true} if there is a subsampling in the given dimension.
+         */
+        public boolean hasSubsampling(final int dimension) {
+            return subsampling[dimension] != 1;
+        }
     }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
index 49e5195..0a3059b 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelData.java
@@ -32,7 +32,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureBetween;
  * querying or modifying the stream position. This class does not define any read or write operations.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.5
+ * @version 1.1
  * @since   0.5 (derived from 0.3)
  * @module
  */
@@ -135,7 +135,7 @@ public abstract class ChannelData implements Markable {
      * @return the bit offset of the stream.
      */
     public final int getBitOffset() {
-        final long position = bufferOffset + buffer.position();
+        final long position = position();
         if ((bitPosition >>> BIT_OFFSET_SIZE) != position) {
             bitPosition = position << BIT_OFFSET_SIZE;
         }
@@ -149,8 +149,7 @@ public abstract class ChannelData implements Markable {
      */
     public final void setBitOffset(final int bitOffset) {
         ensureBetween("bitOffset", 0, Byte.SIZE - 1, bitOffset);
-        final long position = bufferOffset + buffer.position();
-        bitPosition = (position << BIT_OFFSET_SIZE) | bitOffset;
+        bitPosition = (position() << BIT_OFFSET_SIZE) | bitOffset;
     }
 
     /**
@@ -167,7 +166,14 @@ public abstract class ChannelData implements Markable {
      */
     @Override
     public long getStreamPosition() {
-        return bufferOffset + buffer.position();
+        return position();
+    }
+
+    /**
+     * Returns the current byte position of the stream, ignoring overriding by subclasses.
+     */
+    private long position() {
+        return Math.addExact(bufferOffset, buffer.position());
     }
 
     /**
@@ -187,7 +193,7 @@ public abstract class ChannelData implements Markable {
      * @param position the new position of the stream.
      */
     public final void setStreamPosition(final long position) {
-        bufferOffset = position - buffer.position();
+        bufferOffset = Math.subtractExact(position, buffer.position());
         // Clearing the bit offset is needed if we don't want to handle the case of ChannelDataOutput,
         // which use a different stream position calculation when the bit offset is non-zero.
         clearBitOffset();
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
index 1d4424a..585db35 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
@@ -266,9 +266,11 @@ nextSlice:  for (;;) {
                             if (!failOnMismatch) break;
                             final Point pr = itr.getPosition();
                             final Point pc = itc.getPosition();
-                            assertArrayEquals("Mismatch at position (" + pr.x + ", " + pr.y + ") in full image " +
-                                              "and (" + pc.x + ", " + pc.y + ") in tested sub-image",
-                                              expected, actual, STRICT);
+                            final StringBuilder message = new StringBuilder(100).append("Mismatch at position (")
+                                    .append(pr.x).append(", ").append(pr.y).append(") in full image and (")
+                                    .append(pc.x).append(", ").append(pc.y).append(") in tested sub-image");
+                            findMatchPosition(itr, pr, expected, message);
+                            assertArrayEquals(message.toString(), expected, actual, STRICT);
                         }
                     }
                     assertFalse(itc.next());
@@ -363,4 +365,33 @@ nextSlice:  for (;;) {
         }
         return new PixelIterator.Builder().setRegionOfInterest(sliceAOI).create(image);
     }
+
+    /**
+     * Explores pixel values around the given position in search for a pixel having the expected values.
+     * If a match is found, the error message is completed with information about the match position.
+     */
+    private static void findMatchPosition(final PixelIterator ir, final Point pr, final double[] expected, final StringBuilder message) {
+        double[] actual = null;
+        for (int dy=0; dy<10; dy++) {
+            for (int dx=0; dx<10; dx++) {
+                if ((dx | dy) != 0) {
+                    for (int c=0; c<4; c++) {
+                        final int x = (c & 1) == 0 ? -dx : dx;
+                        final int y = (c & 2) == 0 ? -dy : dy;
+                        try {
+                            ir.moveTo(pr.x + x, pr.y + y);
+                        } catch (IndexOutOfBoundsException e) {
+                            continue;
+                        }
+                        actual = ir.getPixel(actual);
+                        if (Arrays.equals(expected, actual)) {
+                            message.append(" (note: found a match at offset (").append(x).append(", ").append(y)
+                                   .append(") in full image)");
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+    }
 }

[sis] 06/07: Get `SampleModel` and `ColorModel` managed by `TiledGridCoverage` base class. Add `TiledGridCoverage.selectedBands` field and clarifications in Javadoc.

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 973c7c035dbdd967179e84b82fd4e7e036a1dc42
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jul 28 11:35:26 2021 +0200

    Get `SampleModel` and `ColorModel` managed by `TiledGridCoverage` base class.
    Add `TiledGridCoverage.selectedBands` field and clarifications in Javadoc.
---
 .../sis/storage/geotiff/CompressedSubset.java      |   6 +-
 .../org/apache/sis/storage/geotiff/DataCube.java   |  30 +-----
 .../org/apache/sis/storage/geotiff/DataSubset.java |  10 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |   4 +-
 .../sis/internal/storage/TiledGridCoverage.java    |  39 +++----
 .../sis/internal/storage/TiledGridResource.java    | 116 +++++++++++++++++----
 6 files changed, 131 insertions(+), 74 deletions(-)

diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
index bf1b2b2..d071da6 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
@@ -91,7 +91,11 @@ final class CompressedSubset extends DataSubset {
         final int      skipY  = subsampling[1] - 1;
         final int      skipX  = numInterleaved * (subsampling[0] - 1);
         final long     head   = numInterleaved * lower[0];
-        final long     tail   = numInterleaved * (getTileSize(0) - (width*subsampling[0] + lower[0])) + skipX;
+        final long     tail   = numInterleaved * (getTileSize(0) - width*subsampling[0]) + skipX - head;
+        /*
+         * `head` and `tail` are the number of sample values to skip at the beginning and end of each row.
+         * Then `skipX` is the number of sample values to skip between each pixel.
+         */
         final Buffer[] banks  = new Buffer[numBanks];
         for (int b=0; b<numBanks; b++) {
             final Buffer   bank = RasterFactory.createBuffer(type, capacity);
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
index 0c2794d..8fd4870 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
@@ -17,7 +17,6 @@
 package org.apache.sis.storage.geotiff;
 
 import java.nio.file.Path;
-import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
 import java.awt.image.BandedSampleModel;
 import org.apache.sis.storage.DataStoreException;
@@ -76,33 +75,6 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
     }
 
     /**
-     * Returns the Java2D sample model describing pixel type and layout.
-     * The raster size is the tile size as stored in the GeoTIFF file.
-     *
-     * <h4>Multi-dimensional data cube</h4>
-     * If this resource has more than 2 dimensions, then this model is for the two first ones (usually horizontal).
-     * The images for all levels in additional dimensions shall use the same sample model.
-     *
-     * @throws DataStoreContentException if the type is not recognized.
-     */
-    protected abstract SampleModel getSampleModel() throws DataStoreException;
-
-    /**
-     * Returns the Java2D color model for rendering images.
-     *
-     * @throws DataStoreContentException if the type is not recognized.
-     */
-    protected abstract ColorModel getColorModel() throws DataStoreException;
-
-    /**
-     * Returns the value to use for filling empty spaces in rasters,
-     * or {@code null} if none, not different than zero or not valid for the target data type.
-     * This value is used if a tile contains less pixels than expected.
-     * The zero value is excluded because tiles are already initialized to zero by default.
-     */
-    protected abstract Number fillValue();
-
-    /**
      * Returns the total number of tiles. This is used for computing the stride between a
      * band and the next band in {@link #tileOffsets} and {@link #tileByteCounts} vectors.
      */
@@ -143,7 +115,7 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
         final GridCoverage coverage;
         try {
             synchronized (reader.store) {
-                final Subset subset = new Subset(this, domain, range);
+                final Subset subset = new Subset(domain, range, false);
                 final Compression compression = getCompression();
                 if (compression == null) {
                     throw new DataStoreContentException(reader.resources().getString(
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
index 97170c6..562af30 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -129,7 +129,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
      * @throws ArithmeticException if the number of tiles overflows 32 bits integer arithmetic.
      */
     DataSubset(final DataCube source, final TiledGridResource.Subset subset) throws DataStoreException {
-        super(subset, source.getSampleModel(), source.getColorModel(), source.fillValue());
+        super(subset);
         this.source   = source;
         this.numTiles = toIntExact(source.getNumTiles());
         final Vector[] tileArrayInfo = source.getTileArrayInfo();
@@ -290,8 +290,8 @@ class DataSubset extends TiledGridCoverage implements Localized {
             final long[] upper       = new long[BIDIMENSIONAL];   // Coordinates after the last pixel to read relative to the tile.
             final int[]  subsampling = new int [BIDIMENSIONAL];
             final Point  origin      = new Point();
-            final long[] offsets     = new long[tileOffsets.size() / numTiles];
-            final long[] byteCounts  = new long[tileByteCounts.size() / numTiles];
+            final long[] offsets     = new long[numBanks];
+            final long[] byteCounts  = new long[numBanks];
             boolean needsCompaction  = false;
             for (int i=0; i<numMissings; i++) {
                 final Tile tile = missings[i];
@@ -303,8 +303,8 @@ class DataSubset extends TiledGridCoverage implements Localized {
                     for (int b=0; b<offsets.length; b++) {
                         offsets[b] = addExact(offsets[b], reader().origin);
                     }
-                    result[tile.indexInResultArray] = tile.cache(
-                            readSlice(offsets, byteCounts, lower, upper, subsampling, origin));
+                    WritableRaster r = readSlice(offsets, byteCounts, lower, upper, subsampling, origin);
+                    result[tile.indexInResultArray] = tile.cache(r);
                 } else {
                     needsCompaction = true;
                 }
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index a9fbe5c..cb6540b 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -332,7 +332,7 @@ final class ImageFileDirectory extends DataCube {
     /**
      * The "no data" or background pixel value, or NaN if undefined.
      *
-     * @see #fillValue()
+     * @see #getFillValue()
      */
     private double noData = Double.NaN;
 
@@ -1495,7 +1495,7 @@ final class ImageFileDirectory extends DataCube {
      * not different than zero or not valid for the target data type.
      */
     @Override
-    protected Number fillValue() {
+    protected Number getFillValue() {
         if (Double.isFinite(noData) && noData != 0) {
             final long min, max;
             switch (sampleFormat) {
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
index 3b30e63..f9abba0 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
@@ -124,6 +124,17 @@ public abstract class TiledGridCoverage extends GridCoverage {
     private final int[] subsampling, subsamplingOffsets;
 
     /**
+     * Indices of {@link TiledGridResource} bands which have been retained for
+     * inclusion in this {@code TiledGridCoverage}, in strictly increasing order.
+     * This is {@code null} if all bands shall be included.
+     *
+     * <p>If the user specified bands out of order, the change of band order is taken in account
+     * by the sample {@link #model}. This {@code selectedBands} array does not show any change
+     * of order for making sequential readings easier.</p>
+     */
+    protected final int[] selectedBands;
+
+    /**
      * Cache of rasters read by this {@code TiledGridCoverage}. This cache may be shared with other coverages
      * created for the same {@link TiledGridResource} resource. For each value, the raster {@code minX} and
      * {@code minY} values can be anything, depending which {@code TiledGridCoverage} was first to load the tile.
@@ -136,6 +147,8 @@ public abstract class TiledGridCoverage extends GridCoverage {
     /**
      * The sample model for all rasters. The size of this sample model is the values of
      * the two first elements of {@link #tileSize} divided by subsampling after clipping.
+     * If user requested to read only a subset of the bands, this sample model is already
+     * the subset.
      */
     protected final SampleModel model;
 
@@ -152,21 +165,17 @@ public abstract class TiledGridCoverage extends GridCoverage {
     /**
      * Creates a new tiled grid coverage. All parameters should have been validated before this call.
      *
-     * @param  subset     description of the {@link TiledGridResource} subset to cover.
-     * @param  model      description of image layout before clipping and subsampling.
-     * @param  colors     Java2D color model for images rendered from this coverage.
-     * @param  fillValue  the value to use for filling empty spaces in rasters, of {@code null}.
+     * @param  subset  description of the {@link TiledGridResource} subset to cover.
      * @throws ArithmeticException if the number of tiles overflows 32 bits integer arithmetic.
      */
-    protected TiledGridCoverage(final TiledGridResource.Subset subset, SampleModel model,
-                                final ColorModel colors, final Number fillValue)
-    {
-        super(subset.domain,  subset.ranges);
+    protected TiledGridCoverage(final TiledGridResource.Subset subset) {
+        super(subset.domain, subset.ranges);
         final GridExtent extent = subset.domain.getExtent();
         final int dimension = subset.sourceExtent.getDimension();
         readExtent          = subset.readExtent;
         subsampling         = subset.subsampling;
         subsamplingOffsets  = subset.subsamplingOffsets;
+        selectedBands       = subset.selectedBands;
         rasters             = subset.cache;
         tileSize            = subset.tileSize;
         tmcOfFirstTile      = new long[dimension];
@@ -188,22 +197,14 @@ public abstract class TiledGridCoverage extends GridCoverage {
          * At this point, `tileStride` is the total number of tiles in source.
          * This value is not stored but its computation is still useful because
          * we want `ArithmeticException` to be thrown if the value is too high.
-         *
-         * Note: the compatible sample model will require only the memory needed for the new size.
-         * But the subset sample model will use a `DataBuffer` that still consume all the memory of
-         * the sample model for all bands (contrarily to the sample model for a smaller raster size).
-         * It is okay for `PixelInterleavedSampleModel` because we will need to read all bands anyway.
-         * It will be more complicated for the `BandedSampleModel` case.
          */
+        SampleModel model = subset.modelForBandSubset;
         if (model.getWidth() != subSize[0] || model.getHeight() != subSize[1]) {
             model = model.createCompatibleSampleModel(subSize[0], subSize[1]);
         }
-        if (subset.selectedBands != null) {
-            model = model.createSubsetSampleModel(subset.selectedBands);
-        }
         this.model     = model;
-        this.colors    = colors;
-        this.fillValue = fillValue;
+        this.colors    = subset.colorsForBandSubset;
+        this.fillValue = subset.fillValue;
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
index 8a13cbd..cdd4e59 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
@@ -18,7 +18,10 @@ package org.apache.sis.internal.storage;
 
 import java.util.List;
 import java.util.Arrays;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
 import java.awt.image.WritableRaster;
+import java.awt.image.RasterFormatException;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridDerivation;
 import org.apache.sis.coverage.grid.GridExtent;
@@ -27,12 +30,13 @@ import org.apache.sis.coverage.grid.GridRoundingMode;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.collection.WeakValueHashMap;
+import org.apache.sis.util.ArraysExt;
 
 
 /**
  * Base class of grid coverage resource storing data in tiles.
- * Word "tile" is used for simplicity but can be understood as "chunk"
- * in a <var>n</var>-dimensional generalization.
+ * The word "tile" is used for simplicity but can be understood
+ * as "chunk" in a <var>n</var>-dimensional generalization.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -66,15 +70,49 @@ public abstract class TiledGridResource extends AbstractGridResource {
      * The length of the returned array is the number of dimensions.
      *
      * @return the size of tiles in this resource.
+     * @throws DataStoreException if an error occurred while fetching tile size information.
      */
-    protected abstract int[] getTileSize();
+    protected abstract int[] getTileSize() throws DataStoreException;
+
+    /**
+     * Returns the Java2D sample model describing pixel type and layout for all bands.
+     * The raster size is the {@linkplain #getTileSize() tile size} as stored in the resource.
+     *
+     * <h4>Multi-dimensional data cube</h4>
+     * If this resource has more than 2 dimensions, then this model is for the two first ones (usually horizontal).
+     * The images for all levels in additional dimensions shall use the same sample model.
+     *
+     * @return the sample model for tiles at full resolution with all their bands.
+     * @throws DataStoreException if an error occurred during sample model construction.
+     */
+    protected abstract SampleModel getSampleModel() throws DataStoreException;
+
+    /**
+     * Returns the Java2D color model for rendering images, or {@code null} if none.
+     * The color model shall be compatible with the sample model returned by {@link #getSampleModel()}.
+     *
+     * @return a color model compatible with {@link #getSampleModel()}, or {@code null} if none.
+     * @throws DataStoreException if an error occurred during color model construction.
+     */
+    protected abstract ColorModel getColorModel() throws DataStoreException;
+
+    /**
+     * Returns the value to use for filling empty spaces in rasters,
+     * or {@code null} if none, not different than zero or not valid for the target data type.
+     * This value is used if a tile contains less pixels than expected.
+     * The zero value is excluded because tiles are already initialized to zero by default.
+     *
+     * @return the value to use for filling empty spaces in rasters.
+     * @throws DataStoreException if an error occurred while fetching filling information.
+     */
+    protected abstract Number getFillValue() throws DataStoreException;
 
     /**
      * Parameters that describe the resource subset to be accepted by the {@link TiledGridCoverage} constructor.
      * This is a temporary class used only for transferring information from {@link TiledGridResource}.
      * This class does not perform I/O operations.
      */
-    public static final class Subset {
+    public final class Subset {
         /**
          * The full size of the coverage in the enclosing {@link TiledGridResource}.
          */
@@ -100,9 +138,13 @@ public abstract class TiledGridResource extends AbstractGridResource {
         final List<? extends SampleDimension> ranges;
 
         /**
-         * Indices of {@link TiledGridResource} bands which have been retained
-         * for inclusion in the {@link TiledGridCoverage} to construct.
+         * Indices of {@link TiledGridResource} bands which have been retained for inclusion
+         * in the {@link TiledGridCoverage} to construct, in strictly increasing order.
          * This is {@code null} if all bands shall be included.
+         *
+         * <p>If the user specified bands out of order, the change of band order is taken in
+         * account by the {@link #modelForBandSubset}. This {@code selectedBands} array does
+         * not show any change of order for making sequential readings easier.</p>
          */
         final int[] selectedBands;
 
@@ -119,11 +161,29 @@ public abstract class TiledGridResource extends AbstractGridResource {
         final int[] subsamplingOffsets;
 
         /**
-         * Size of tiles in each dimension.
+         * Size of tiles (or chunks) in the resource, without clipping and subsampling.
          */
         final int[] tileSize;
 
         /**
+         * The sample model for the bands to read (not the full set of bands in the resource).
+         * The width is {@code tileSize[0]} and the height it {@code tileSize[1]},
+         * i.e. subsampling is <strong>not</strong> applied.
+         */
+        final SampleModel modelForBandSubset;
+
+        /**
+         * The color model for the bands to read (not the full set of bands in the resource).
+         */
+        final ColorModel colorsForBandSubset;
+
+        /**
+         * Value to use for filling empty spaces in rasters, or {@code null} if none,
+         * not different than zero or not valid for the target data type.
+         */
+        final Number fillValue;
+
+        /**
          * Cache to use for tiles loaded by the {@link TiledGridCoverage}.
          * It is a reference to {@link TiledGridResource#rasters} if shareable.
          */
@@ -132,18 +192,25 @@ public abstract class TiledGridResource extends AbstractGridResource {
         /**
          * Creates parameters for the given domain and range.
          *
-         * @param  caller  the resource for which a subset is created.
          * @param  domain  the domain argument specified by user in a call to {@code GridCoverageResource.read(…)}.
          * @param  range   the range argument specified by user in a call to {@code GridCoverageResource.read(…)}.
+         * @param  loadAllBands {@code true} if the reader will load all bands regardless the {@code range} subset,
+         *         or {@code false} if only requested bands will be loaded and other bands will be skipped. See
+         *         {@link AbstractGridResource.RangeArgument#select(SampleModel, boolean) RangeArgument.select(…)}
+         *         for more information.
+         *
          * @throws ArithmeticException if pixel indices exceed 64 bits integer capacity.
          * @throws DataStoreException if a call to {@link TiledGridResource} method failed.
+         * @throws RasterFormatException if the sample model is not recognized.
+         * @throws IllegalArgumentException if an error occurred in an operation
+         *         such as creating the {@code SampleModel} subset for selected bands.
          */
-        public Subset(final TiledGridResource caller, GridGeometry domain, final int[] range) throws DataStoreException {
-            List<SampleDimension> bands        = caller.getSampleDimensions();
-            final RangeArgument   rangeIndices = caller.validateRangeArgument(bands.size(), range);
-            final GridGeometry    gridGeometry = caller.getGridGeometry();
+        public Subset(GridGeometry domain, final int[] range, final boolean loadAllBands) throws DataStoreException {
+            List<SampleDimension> bands        = getSampleDimensions();
+            final RangeArgument   rangeIndices = validateRangeArgument(bands.size(), range);
+            final GridGeometry    gridGeometry = getGridGeometry();
             sourceExtent = gridGeometry.getExtent();
-            tileSize = caller.getTileSize();
+            tileSize = getTileSize();
             boolean sharedCache = true;
             if (domain == null) {
                 domain             = gridGeometry;
@@ -178,22 +245,35 @@ public abstract class TiledGridResource extends AbstractGridResource {
                     }
                 }
             }
+            /*
+             * Get the bands selected by user in strictly increasing order of source band index.
+             * If user has specified bands in a different order, that change of band order will
+             * be handled by the `SampleModel`, not in `selectedBands` array.
+             */
             int[] selectedBands = null;
             if (!rangeIndices.isIdentity()) {
+                sharedCache = false;
                 bands = Arrays.asList(rangeIndices.select(bands));
                 selectedBands = new int[rangeIndices.getNumBands()];
                 for (int i=0; i<selectedBands.length; i++) {
-                    selectedBands[rangeIndices.getTargetIndex(i)] = rangeIndices.getSourceIndex(i);
+                    selectedBands[i] = rangeIndices.getSourceIndex(i);
+                }
+                assert ArraysExt.isSorted(selectedBands, true);
+                if (ArraysExt.isRange(0, selectedBands)) {
+                    selectedBands = null;
                 }
             }
-            this.domain        = domain;
-            this.ranges        = bands;
-            this.selectedBands = selectedBands;
+            this.domain              = domain;
+            this.ranges              = bands;
+            this.selectedBands       = selectedBands;
+            this.modelForBandSubset  = rangeIndices.select(getSampleModel(), loadAllBands);
+            this.colorsForBandSubset = rangeIndices.select(getColorModel());
+            this.fillValue           = getFillValue();
             /*
              * All `TiledGridCoverage` instances can share the same cache if they read all tiles fully.
              * If they read only sub-regions or apply subsampling, then they will need their own cache.
              */
-            cache = sharedCache ? caller.rasters : new WeakValueHashMap<>(Integer.class);
+            cache = sharedCache ? rasters : new WeakValueHashMap<>(Integer.class);
         }
 
         /**

[sis] 01/07: Define a mapping between `GeometryType` and the integer codes used in Well-Known Binary (WKB).

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 7544551ad9a5041ef39567f5b365af5316ccf394
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jul 23 12:45:10 2021 +0200

    Define a mapping between `GeometryType` and the integer codes used in Well-Known Binary (WKB).
---
 .../apache/sis/internal/feature/GeometryType.java  | 77 ++++++++++++++++++++++
 .../sis/internal/feature/GeometryTypeTest.java     | 54 +++++++++++++++
 .../apache/sis/test/suite/FeatureTestSuite.java    |  1 +
 3 files changed, 132 insertions(+)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryType.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryType.java
index 04c028e..f432a9d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryType.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryType.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.internal.feature;
 
+import java.util.Locale;
+
 
 /**
  * Implementation-neutral description of the type of geometry.
@@ -80,4 +82,79 @@ public enum GeometryType {
      * Set of geometries of any type except other geometry collection.
      */
     GEOMETRY_COLLECTION;
+
+    /**
+     * The type of this geometry as specified in Well-Known Binary (WKB) specification.
+     * This is also the integer value declared in the {@code "GEOMETRY_TYPE"} column of
+     * the {@code "GEOMETRY_COLUMNS} table of a spatial database.
+     *
+     * <p>The WKB specification defines values in the [0 … 15] range for 2D geometries
+     * and adds 1000 for geometries having <var>Z</var> values.
+     * Then 2000 is added again for geometries having <var>M</var> values.</p>
+     *
+     * @return the geometry type specified in WKB specification.
+     *
+     * @see #forBinaryType(int)
+     */
+    public final int binaryType() {
+        return ordinal();
+    }
+
+    /**
+     * Returns the enumeration value for the given name.
+     * This method is case-insensitive.
+     *
+     * @param  name  the geometry type name, or {@code null}.
+     * @return enumeration value for the given name, or {@code null} if the name was null.
+     * @throws IllegalArgumentException if the name is not recognized.
+     */
+    public static GeometryType forName(String name) {
+        if (name != null) {
+            name = name.trim().toUpperCase(Locale.US);
+            int length = name.length();
+            if (length > 0) {
+                // Remove Z, M or ZM suffix.
+                if (/*non-empty*/ name.charAt(length - 1) == 'M') length--;
+                if (length > 0 && name.charAt(length - 1) == 'Z') length--;
+                name = name.substring(0, length);
+                switch (name) {
+                    case "MULTIPOINT":      return MULTI_POINT;
+                    case "MULTILINESTRING": return MULTI_LINESTRING;
+                    case "MULTIPOLYGON":    return MULTI_POLYGON;
+                    case "GEOMCOLLECTION":  return GEOMETRY_COLLECTION;
+                    default: return valueOf(name);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the enumeration value for the given WKB type, or {@code null} if unknown.
+     * Types for geometries having <var>Z</var> and <var>M</var> are replaced by 2D types.
+     *
+     * @param  type  WKB geometry type.
+     * @return enumeration value for the given type, or {@code null}.
+     *
+     * @see #binaryType()
+     */
+    public static GeometryType forBinaryType(int type) {
+        if (type >= 1000 && type < 4000) {
+            type %= 1000;
+        }
+        switch (type) {
+            default: return null;
+            case 0:  return GEOMETRY;
+            case 1:  return POINT;
+            case 2:  return LINESTRING;
+            case 3:  return POLYGON;
+            case 4:  return MULTI_POINT;
+            case 5:  return MULTI_LINESTRING;
+            case 6:  return MULTI_POLYGON;
+            case 7:  return GEOMETRY_COLLECTION;
+        //  case 13: return CURVE;
+        //  case 14: return SURFACE;
+        //  case 15: return POLYHEDRALSURFACE;
+        }
+    }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometryTypeTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometryTypeTest.java
new file mode 100644
index 0000000..713a613
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometryTypeTest.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.feature;
+
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link GeometryType}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final strictfp class GeometryTypeTest extends TestCase {
+    /**
+     * Tests {@link GeometryType#forBinaryType(int)} and verifies {@link GeometryType#binaryType()} values.
+     */
+    @Test
+    public void testBinaryType() {
+        for (final GeometryType type : GeometryType.values()) {
+            assertSame(type.name(), type, GeometryType.forBinaryType(type.binaryType()));
+        }
+    }
+
+    /**
+     * Tests {@link GeometryType#forName(String)}.
+     */
+    @Test
+    public void testForName() {
+        assertSame(GeometryType.MULTI_POLYGON, GeometryType.forName("multi_polygon"));
+        assertSame(GeometryType.MULTI_POLYGON, GeometryType.forName("MULTIPOLYGON"));
+        assertSame(GeometryType.GEOMETRY_COLLECTION, GeometryType.forName("GEOMETRY_COLLECTION"));
+        assertSame(GeometryType.GEOMETRY_COLLECTION, GeometryType.forName("GeomCollection"));
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 22f6f13..ed26d95 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -60,6 +60,7 @@ import org.junit.runners.Suite;
     org.apache.sis.filter.TemporalFunctionTest.class,
     org.apache.sis.filter.SpatialUsingJTS_Test.class,
     org.apache.sis.internal.feature.AttributeConventionTest.class,
+    org.apache.sis.internal.feature.GeometryTypeTest.class,
     org.apache.sis.internal.filter.FunctionNamesTest.class,
     org.apache.sis.internal.filter.sqlmm.SQLMMTest.class,
     org.apache.sis.internal.filter.sqlmm.RegistryUsingJTS_Test.class,

[sis] 07/07: Take in account the band subset specified by the `range` argument in a call to `GridCoverage.render(…)` on a GeoTIFF image.

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 d53b8a5db8e77371dfb32beebaabda80defe2f0d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jul 28 18:26:49 2021 +0200

    Take in account the band subset specified by the `range` argument in a call to `GridCoverage.render(…)` on a GeoTIFF image.
---
 .../org/apache/sis/internal/geotiff/Inflater.java  |  52 +++---
 .../apache/sis/internal/geotiff/Uncompressed.java  |  53 +++---
 .../sis/storage/geotiff/CompressedSubset.java      | 106 ++++++++++--
 .../org/apache/sis/storage/geotiff/DataCube.java   |  12 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java | 107 ++++++------
 .../sis/storage/geotiff/ImageFileDirectory.java    |  10 ++
 .../sis/internal/storage/AbstractGridResource.java |  18 +-
 .../sis/internal/storage/TiledGridCoverage.java    |  15 ++
 .../sis/internal/storage/TiledGridResource.java    |  14 +-
 .../internal/storage/io/HyperRectangleReader.java  |  11 ++
 .../sis/test/storage/CoverageReadConsistency.java  | 182 ++++++++++++++++-----
 11 files changed, 431 insertions(+), 149 deletions(-)

diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
index e5fa40c..9cd502f 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Inflater.java
@@ -41,47 +41,59 @@ public abstract class Inflater {
 
     /**
      * Number of chunk per row, as a strictly positive integer.
-     * A chunk is a pixel, except if we can optimize by reading the whole row as a single chunk.
+     * See {@link #samplesPerChunk} for more details about what is a "chunk".
      */
     protected final int chunksPerRow;
 
     /**
      * Number of sample values per chunk, as a strictly positive integer.
-     * A chunk is a pixel, except if we can optimize by reading the whole row as a single chunk.
+     * A chunk can be:
+     * <ul>
+     *   <li>A sample value ({@code samplesPerChunk} = 1).</li>
+     *   <li>A pixel with one sample value per band ({@code samplesPerChunk} = number of bands).</li>
+     *   <li>A full row (optimization when it is possible to read the row in a single I/O method call).</li>
+     * </ul>
      */
     protected final int samplesPerChunk;
 
     /**
-     * Number of sample values to skip between pixels. Positive but often zero.
+     * Number of sample values to skip between chunks on the same row, or {@code null} if none.
+     * If non-null then after reading the chunk at zero-based index <var>x</var>, inflater shall skip
+     * {@code skipAfterChunks[x % skipAfterChunks.length]} sample values before to read the next chunk.
+     * The <var>x</var> index is reset to zero at the beginning of every new row.
      */
-    protected final int interpixels;
+    protected final int[] skipAfterChunks;
 
     /**
      * Creates a new instance.
      *
-     * @param  input            the source of data to decompress.
-     * @param  pixelsPerRow     number of pixels per row. Must be strictly positive.
-     * @param  samplesPerPixel  number of sample values per pixel. Must be strictly positive.
-     * @param  interpixels      number of sample values to skip between pixels. May be zero.
+     * @param  input              the source of data to decompress.
+     * @param  elementsPerRow     number of elements (usually pixels) per row. Must be strictly positive.
+     * @param  samplesPerElement  number of sample values per element (usually pixel). Must be strictly positive.
+     * @param  skipAfterElements  number of sample values to skip between elements (pixels). May be empty or null.
      */
-    protected Inflater(final ChannelDataInput input, final int pixelsPerRow, final int samplesPerPixel, final int interpixels) {
-        ensureStrictlyPositive("pixelsPerRow",    pixelsPerRow);
-        ensureStrictlyPositive("samplesPerPixel", samplesPerPixel);
-        ensurePositive        ("interpixels",     interpixels);
-        ensureNonNull         ("input",           input);
-        if (interpixels == 0) {
-            chunksPerRow    = 1;
-            samplesPerChunk = Math.multiplyExact(samplesPerPixel, pixelsPerRow);
+    protected Inflater(final ChannelDataInput input, final int elementsPerRow, final int samplesPerElement, final int[] skipAfterElements) {
+        ensureNonNull("input", input);
+        ensureStrictlyPositive("elementsPerRow",    elementsPerRow);
+        ensureStrictlyPositive("samplesPerElement", samplesPerElement);
+        this.input = input;
+        skipAfterChunks = skipAfterElements;
+        if (skipAfterElements != null) {
+            for (int i=0; i<skipAfterElements.length; i++) {
+                ensurePositive("skipAfterElements", skipAfterElements[i]);
+            }
+            chunksPerRow    = elementsPerRow;
+            samplesPerChunk = samplesPerElement;
         } else {
-            chunksPerRow    = pixelsPerRow;
-            samplesPerChunk = samplesPerPixel;
+            chunksPerRow    = 1;
+            samplesPerChunk = Math.multiplyExact(samplesPerElement, elementsPerRow);
         }
-        this.interpixels = interpixels;
-        this.input       = input;
     }
 
     /**
      * Reads a row of sample values and stores them in the target buffer.
+     * This is not a complete row; caller may invoke {@link #skip(long)}
+     * for ignoring leading and trailing sample values.
      *
      * @throws IOException if an error occurred while reading the input channel.
      */
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
index 3cb3ce5..9b695a6 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Uncompressed.java
@@ -26,6 +26,9 @@ import org.apache.sis.util.Classes;
 
 /**
  * A pseudo-inflater which copy values unchanged.
+ * This implementation is useful for handling more complex subsampling
+ * than what {@link org.apache.sis.internal.storage.io.HyperRectangleReader} can handle.
+ * It is also useful for testing purpose.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -52,8 +55,10 @@ public abstract class Uncompressed extends Inflater {
     /**
      * For constructors in inner classes.
      */
-    private Uncompressed(ChannelDataInput input, long start, int pixelsPerRow, int samplesPerPixel, int interpixels, final int sampleSize) {
-        super(input, pixelsPerRow, samplesPerPixel, interpixels);
+    private Uncompressed(final ChannelDataInput input, final long start, final int elementsPerRow,
+                         final int samplesPerElement, final int[] skipAfterElements, final int sampleSize)
+    {
+        super(input, elementsPerRow, samplesPerElement, skipAfterElements);
         this.streamPosition = start;
         this.sampleSize = sampleSize;
     }
@@ -61,25 +66,26 @@ public abstract class Uncompressed extends Inflater {
     /**
      * Creates a new instance.
      *
-     * @param  input            the source of data to decompress.
-     * @param  start            stream position where to start reading.
-     * @param  pixelsPerRow     number of pixels per row. Must be strictly positive.
-     * @param  samplesPerPixel  number of sample values per pixel. Must be strictly positive.
-     * @param  interpixels      number of sample values to skip between pixels. May be zero.
-     * @param  target           where to store sample values.
+     * @param  input   the source of data to decompress.
+     * @param  start   stream position where to start reading.
+     * @param  count   number of elements (usually pixels) per row. Must be strictly positive.
+     * @param  size    number of sample values per element (usually pixel). Must be strictly positive.
+     * @param  skips   number of sample values to skip between elements (pixels). May be empty or null.
+     * @param  target  where to store sample values.
      * @return the inflater for the given targe type.
      * @throws IllegalArgumentException if the buffer type is not recognized.
      */
     public static Uncompressed create(final ChannelDataInput input, final long start,
-            final int pixelsPerRow, final int samplesPerPixel, final int interpixels, final Buffer target)
+            final int count, final int size, final int[] skips, final Buffer target)
     {
-        if (target instanceof ByteBuffer) return new Bytes(input, start, pixelsPerRow, samplesPerPixel, interpixels, (ByteBuffer) target);
+        if (target instanceof ByteBuffer) return new Bytes(input, start, count, size, skips, (ByteBuffer) target);
         throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, Classes.getClass(target)));
     }
 
     /**
      * Reads a row of sample values and stores them in the target buffer.
-     * Subclasses must override and invoke {@code super.uncompress()} before to do the actual reading.
+     * Subclasses must override this method and invoke {@code super.uncompress()}
+     * before to do the actual reading.
      */
     @Override
     public void uncompressRow() throws IOException {
@@ -108,23 +114,22 @@ public abstract class Uncompressed extends Inflater {
     }
 
     /**
-     * Inflater when the values to read and store are bytes.
+     * Inflater for sample values stored as bytes.
      */
     private static final class Bytes extends Uncompressed {
         /** Where to copy the values that we will read. */
         private final ByteBuffer target;
 
         /** Creates a new inflater which will write in the given buffer. */
-        Bytes(ChannelDataInput input, long start, int pixelsPerRow, int samplesPerPixel, int interpixels, ByteBuffer target) {
-            super(input, start, pixelsPerRow, samplesPerPixel, interpixels, Byte.BYTES);
+        Bytes(ChannelDataInput input, long start, int count, int size, int[] skips, ByteBuffer target) {
+            super(input, start, count, size, skips, Byte.BYTES);
             this.target = target;
         }
 
-        /**
-         * Reads and decompress a row of sample values.
-         */
+        /** Reads and decompress a row of sample values. */
         @Override public void uncompressRow() throws IOException {
             super.uncompressRow();
+            int ip = 0;
             for (int i = chunksPerRow; --i > 0;) {      // (chunksPerRow - 1) iterations.
                 int n = samplesPerChunk;
                 do target.put(input.readByte());
@@ -134,14 +139,18 @@ public abstract class Uncompressed extends Inflater {
                  * We invoke `readByte()` in a loop instead of invoking `skip` because if
                  * the number of bytes to skip is small, this is more efficient.
                  */
-                for (n = interpixels; --n >= 0;) {
-                    input.readByte();
+                if (skipAfterChunks != null) {
+                    for (n = skipAfterChunks[ip]; --n >= 0;) {
+                        input.readByte();
+                    }
+                    if (++ip >= skipAfterChunks.length) ip = 0;
                 }
             }
             /*
-             * Read the last element that was not read in first above `for` loop, but without
-             * skipping `interpixels` values after it. This is necessary for avoiding EOF if
-             * the last pixel to read is in the last column of the tile.
+             * Read the last element that was not read in above `for` loop,
+             * but without skipping `skipAfterElements` sample values after.
+             * This is necessary for avoiding EOF if the last pixel to read
+             * is in the last column of the tile.
              */
             int n = samplesPerChunk;
             do target.put(input.readByte());
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
index d071da6..1b36eb1 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CompressedSubset.java
@@ -41,11 +41,57 @@ import static org.apache.sis.internal.jdk9.JDK9.multiplyFull;
  */
 final class CompressedSubset extends DataSubset {
     /**
-     * Number of sample values to skip for moving to the next row of a tile.
+     * Number of sample values to skip for moving to the next row of a tile in the GeoTIFF file.
+     * This is not necessarily the same scanline stride than for the tiles created by this class.
      */
     private final long scanlineStride;
 
     /**
+     * Number of sample values to skip before to read the first value of the first pixel in a row.
+     * The first pixel is at column index 0; subsampling offset is not included in this calculation.
+     */
+    private final int beforeFirstBand;
+
+    /**
+     * Number of sample values to skip for reaching end-of-row after reading the last value of the
+     * <em>first</em> pixel in a row. For computing the actual number of sample values to skip,
+     * the number of sample values read of skipped before the last pixel must be subtracted.
+     */
+    private final int afterLastBand;
+
+    /**
+     * Number of sample values to skip after an element has been read, or {@code null} if none.
+     * An element is a sample value or a complete pixel, depending on {@link #samplesPerElement}.
+     * The {@code skipAfterElements} array is used in a cyclic way:
+     *
+     * <ul>
+     *   <li>Skip {@code skipAfterElements[0]} sample values between element 0 and element 1.</li>
+     *   <li>Skip {@code skipAfterElements[1]} sample values between element 1 and element 2.</li>
+     *   <li><i>etc</i>. When we reach the array end, continue at {@code skipAfterElements[0]}.</li>
+     *   <li>When we start a new row, unconditionally restart at {@code skipAfterElements[0]}.</li>
+     * </ul>
+     *
+     * More generally, skip {@code skipAfterElements[x % skipAfterElements.length]} sample values
+     * after element at the zero-based column index <var>x</var>.
+     */
+    private final int[] skipAfterElements;
+
+    /**
+     * Number of sample values that compose an element (pixel or sample) in the GeoTIFF file.
+     * The value of this field can be:
+     *
+     * <ul>
+     *   <li>1 if an element is a sample value.</li>
+     *   <li>{@link #sourcePixelStride} if an element is a full pixel.</li>
+     *   <li>Any intermediate value if some optimizations have been applied,
+     *       for example for taking advantage of consecutive indices in {@link #selectedBands}.</li>
+     * </ul>
+     *
+     * This value shall always be a divisor of {@link #targetPixelStride}.
+     */
+    private final int samplesPerElement;
+
+    /**
      * Creates a new data subset. All parameters should have been validated
      * by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
      * This constructor should be invoked inside a synchronized block.
@@ -57,7 +103,33 @@ final class CompressedSubset extends DataSubset {
      */
     CompressedSubset(final DataCube source, final TiledGridResource.Subset subset) throws DataStoreException {
         super(source, subset);
-        scanlineStride = multiplyFull(getTileSize(0), numInterleaved);
+        scanlineStride = multiplyFull(getTileSize(0), sourcePixelStride);
+        final int between = sourcePixelStride * (getSubsampling(0) - 1);
+        int afterLastBand = sourcePixelStride * (getTileSize(0) - 1);
+        if (selectedBands != null && sourcePixelStride > 1) {
+            final int n = selectedBands.length;
+            skipAfterElements = new int[n];
+            int b = sourcePixelStride;
+            for (int i = n; --i >= 0;) {
+                // Number of sample values to skip after each band.
+                skipAfterElements[i] = b - (b = selectedBands[i]) - 1;
+            }
+            beforeFirstBand         = b;
+            afterLastBand          += skipAfterElements[n-1];       // Add trailing bands that were left unread.
+            skipAfterElements[n-1] += between + beforeFirstBand;    // Add pixels skipped by subsampling and move to first band.
+            samplesPerElement       = 1;
+            // TODO: we could optimize if we find that all sample values to read are consecutive.
+        } else {
+            skipAfterElements = (between != 0) ? new int[] {between} : null;
+            samplesPerElement = sourcePixelStride;
+            beforeFirstBand   = 0;
+        }
+        this.afterLastBand = afterLastBand;
+        /*
+         * Invariant documented in Javadoc.
+         * Calculation correctness depends on this condition.
+         */
+        assert targetPixelStride % samplesPerElement == 0 : samplesPerElement;
     }
 
     /**
@@ -79,39 +151,43 @@ final class CompressedSubset extends DataSubset {
      * @param  upper        (<var>x</var>, <var>y</var>) coordinates after the last pixel to read relative to the tile.
      * @param  subsampling  (<var>sx</var>, <var>sy</var>) subsampling factors.
      * @param  location     pixel coordinates in the upper-left corner of the tile to return.
-     * @return image decoded from the GeoTIFF file.
+     * @return a single tile decoded from the GeoTIFF file.
      */
     @Override
     WritableRaster readSlice(final long[] offsets, final long[] byteCounts, final long[] lower, final long[] upper,
                              final int[] subsampling, final Point location) throws IOException, DataStoreException
     {
-        final DataType type   = getDataType();
-        final int      width  = pixelCount(lower, upper, subsampling, 0);
-        final int      height = pixelCount(lower, upper, subsampling, 1);
-        final int      skipY  = subsampling[1] - 1;
-        final int      skipX  = numInterleaved * (subsampling[0] - 1);
-        final long     head   = numInterleaved * lower[0];
-        final long     tail   = numInterleaved * (getTileSize(0) - width*subsampling[0]) + skipX - head;
+        final DataType type       = getDataType();
+        final int  width          = pixelCount(lower, upper, subsampling, 0);
+        final int  height         = pixelCount(lower, upper, subsampling, 1);
+        final int  elementsPerRow = width * (targetPixelStride / samplesPerElement);
+        final int  betweenRows    = subsampling[1] - 1;
+        final long head           = beforeFirstBand + sourcePixelStride * (lower[0]);
+        final long tail           = afterLastBand   - sourcePixelStride * (lower[0] + (width-1)*subsampling[0]);
         /*
          * `head` and `tail` are the number of sample values to skip at the beginning and end of each row.
-         * Then `skipX` is the number of sample values to skip between each pixel.
+         * `betweenPixels` is the number of sample values to skip between each pixel, but the actual skips
+         * are more complicated if only a subset of the bands are read. The actual number of sample values
+         * to skip between "elements" is detailed by `skipAfterElements`.
          */
-        final Buffer[] banks  = new Buffer[numBanks];
+        final Buffer[] banks = new Buffer[numBanks];
         for (int b=0; b<numBanks; b++) {
             final Buffer   bank = RasterFactory.createBuffer(type, capacity);
-            final Inflater algo = Uncompressed.create(reader().input, offsets[b], width, numInterleaved, skipX, bank);
+            final Inflater algo = Uncompressed.create(reader().input, offsets[b], elementsPerRow, samplesPerElement, skipAfterElements, bank);
             for (long y = lower[1]; --y >= 0;) {
                 algo.skip(scanlineStride);
+                // TODO: after we finished to implement decompression algorithms,
+                // revisit if we can safely replace the loop by a multiplication.
             }
             for (int y = height; --y > 0;) {        // (height - 1) iterations.
                 algo.skip(head);
                 algo.uncompressRow();
                 algo.skip(tail);
-                for (int j=skipY; --j>=0;) {
+                for (int j=betweenRows; --j>=0;) {
                     algo.skip(scanlineStride);
                 }
             }
-            algo.skip(head);                        // Last iteration without last `skip(…)` calls.
+            algo.skip(head);                        // Last iteration without the trailing `skip(…)` calls.
             algo.uncompressRow();
             fillRemainingRows(bank.flip());
             banks[b] = bank;
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
index 8fd4870..23f2e66 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
@@ -75,6 +75,16 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
     }
 
     /**
+     * Returns the number of components per pixel in the image stored in GeoTIFF file.
+     * This the same value than the one returned by {@code getSampleModel().getNumBands()},
+     * and is also the size of the collection returned by {@link #getSampleDimensions()}.
+     *
+     * @see #getSampleModel()
+     * @see SampleModel#getNumBands()
+     */
+    abstract int getNumBands();
+
+    /**
      * Returns the total number of tiles. This is used for computing the stride between a
      * band and the next band in {@link #tileOffsets} and {@link #tileByteCounts} vectors.
      */
@@ -123,7 +133,7 @@ abstract class DataCube extends TiledGridResource implements ResourceOnFileSyste
                 }
                 switch (compression) {
                     case NONE: {
-                        if (subset.hasSubsampling(0) && isInterleaved()) {
+                        if (subset.hasBandSubset() || (subset.hasSubsampling(0) && isInterleaved())) {
                             coverage = new CompressedSubset(this, subset);
                         } else {
                             coverage = new DataSubset(this, subset);
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
index 562af30..313b262 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -100,21 +100,32 @@ class DataSubset extends TiledGridCoverage implements Localized {
     private final int numTiles;
 
     /**
-     * Number of banks in the image data buffer.
+     * Number of banks retained for the target image data buffer.
      * This is equal to the number of bands only for planar images, and 1 in all other cases.
+     * If the user asked to read only a subset of the bands, then "number of bands" in above
+     * sentence is the {@linkplain #selectedBands subset} size.
      */
     protected final int numBanks;
 
     /**
-     * Number of interleaved sample values in a pixel. For planar images, this is equal to 1.
-     * For interleaved sample model, this is equal to the number of bands. This value is often
-     * equal to the {@linkplain java.awt.image.ComponentSampleModel#getPixelStride() pixel stride}.
+     * Number of interleaved sample values in a pixel in the GeoTIFF file (ignoring band subset).
+     * For planar images (banded sample model), this is equal to 1. For pixel interleaved image,
+     * this is equal to the number of bands in the original image.
+     *
+     * @see java.awt.image.ComponentSampleModel#getPixelStride()
      */
-    protected final int numInterleaved;
+    protected final int sourcePixelStride;
+
+    /**
+     * Number of interleaved sample values in a pixel of the image to load in memory.
+     * This is similar to {@link #sourcePixelStride}, but taking in account the number
+     * of bands requested by user at reading time.
+     */
+    protected final int targetPixelStride;
 
     /**
      * Number of sample values in a bank (not necessarily a band).
-     * This is tile width × height × {@link #numInterleaved}.
+     * This is tile width × height × {@code targetPixelStride}.
      */
     protected final int capacity;
 
@@ -139,19 +150,24 @@ class DataSubset extends TiledGridCoverage implements Localized {
          * "Banks" (in `java.awt.image.DataBuffer` sense) are synonymous to "bands" for planar image only.
          * Otherwise there is only one bank no matter the amount of bands. Each bank will be read separately.
          */
+        final int maxBank;
         if (model instanceof BandedSampleModel) {
             numBanks = model.getNumBands();
-            numInterleaved = 1;
+            sourcePixelStride = targetPixelStride = 1;
+            maxBank = (selectedBands != null) ? selectedBands[selectedBands.length - 1] : numBanks - 1;
+            // Note: `selectedBands` is in strictly increasing order, so taking the last value is okay.
         } else {
+            maxBank  = 0;
             numBanks = 1;
-            numInterleaved = model.getNumBands();
+            sourcePixelStride = source.getNumBands();
+            targetPixelStride = model .getNumBands();
         }
         final int n = tileOffsets.size();
-        if (numBanks > n / numTiles) {
+        if (maxBank >= n / numTiles) {
             throw new DataStoreContentException(source.reader.errors().getString(
-                    Errors.Keys.TooFewCollectionElements_3, "tileOffsets", numBanks * numTiles, n));
+                    Errors.Keys.TooFewCollectionElements_3, "tileOffsets", (maxBank + 1) * numTiles, n));
         }
-        capacity = multiplyExact(multiplyExact(model.getWidth(), model.getHeight()), numInterleaved);
+        capacity = multiplyExact(multiplyExact(model.getWidth(), model.getHeight()), targetPixelStride);
     }
 
     /**
@@ -192,8 +208,8 @@ class DataSubset extends TiledGridCoverage implements Localized {
         /**
          * Value of {@link DataSubset#tileOffsets} at index {@link #indexInTileVector}.
          * If pixel data are stored in different planes ("banks" in Java2D terminology),
-         * this is the smallest value of all banks.
-         * This is the value that we want in increasing order.
+         * then current implementation takes only the offset of the first bank to read.
+         * This field contains the value that we want in increasing order.
          *
          * @see #compareTo(Tile)
          */
@@ -204,16 +220,13 @@ class DataSubset extends TiledGridCoverage implements Localized {
          *
          * @param iterator  the iterator for which to create a snapshot of its current position.
          */
-        Tile(final AOI domain, final Vector tileOffsets, final int numTiles) {
+        Tile(final AOI domain, final Vector tileOffsets, final int[] selectedBanks, final int numTiles) {
             super(domain);
             int p = indexInTileVector;
-            long offset = tileOffsets.longValue(p);
-            final int limit = tileOffsets.size() - numTiles;
-            while (p < limit) {
-                // TODO: should take in account only the bands selected by user.
-                offset = Math.min(offset, tileOffsets.longValue(p += numTiles));
+            if (selectedBanks != null) {
+                p += selectedBanks[0] * numTiles;
             }
-            byteOffset = offset;
+            byteOffset = tileOffsets.longValue(p);
         }
 
         /**
@@ -225,11 +238,10 @@ class DataSubset extends TiledGridCoverage implements Localized {
          * @param  target    the array where to copy vector values.
          * @param  numTiles  value of {@link DataSubset#numTiles}.
          */
-        final void copyTileInfo(final Vector source, final long[] target, final int numTiles) {
-            int i = indexInTileVector;
+        final void copyTileInfo(final Vector source, final long[] target, final int[] selectedBanks, final int numTiles) {
             for (int j=0; j<target.length; j++) {
+                final int i = indexInTileVector + numTiles * (selectedBanks != null ? selectedBanks[j] : j);
                 target[j] = source.longValue(i);
-                i += numTiles;
             }
         }
 
@@ -263,10 +275,14 @@ class DataSubset extends TiledGridCoverage implements Localized {
         /*
          * Prepare an array for all tiles to be returned. Tiles that are already in memory will be stored
          * in this array directly. Other tiles will be declared in the `missings` array and loaded later.
+         * Each tile will either store all sample values in an interleaved fashion inside a single bank
+         * (`sourcePixelStride` > 1) or use one separated bank per band (`sourcePixelStride` == 1).
          */
+        final int[] selectedBanks = (sourcePixelStride == 1) ? selectedBands : null;
         final WritableRaster[] result = new WritableRaster[iterator.tileCountInQuery];
         final Tile[] missings = new Tile[iterator.tileCountInQuery];
         int numMissings = 0;
+        boolean needsCompaction = false;
         synchronized (reader().store) {
             do {
                 final WritableRaster tile = iterator.getCachedTile();
@@ -274,7 +290,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
                     result[iterator.getIndexInResultArray()] = tile;
                 } else {
                     // Tile not yet loaded. Add to a queue of tiles to load later.
-                    missings[numMissings++] = new Tile(iterator, tileOffsets, numTiles);
+                    missings[numMissings++] = new Tile(iterator, tileOffsets, selectedBanks, numTiles);
                 }
             } while (iterator.next());
             Arrays.sort(missings, 0, numMissings);
@@ -292,14 +308,13 @@ class DataSubset extends TiledGridCoverage implements Localized {
             final Point  origin      = new Point();
             final long[] offsets     = new long[numBanks];
             final long[] byteCounts  = new long[numBanks];
-            boolean needsCompaction  = false;
             for (int i=0; i<numMissings; i++) {
                 final Tile tile = missings[i];
                 if (tile.getRegionInsideTile(lower, upper, subsampling, BIDIMENSIONAL)) {
                     origin.x = tile.originX;
                     origin.y = tile.originY;
-                    tile.copyTileInfo(tileOffsets,    offsets,    numTiles);
-                    tile.copyTileInfo(tileByteCounts, byteCounts, numTiles);
+                    tile.copyTileInfo(tileOffsets,    offsets,    selectedBanks, numTiles);
+                    tile.copyTileInfo(tileByteCounts, byteCounts, selectedBanks, numTiles);
                     for (int b=0; b<offsets.length; b++) {
                         offsets[b] = addExact(offsets[b], reader().origin);
                     }
@@ -309,18 +324,18 @@ class DataSubset extends TiledGridCoverage implements Localized {
                     needsCompaction = true;
                 }
             }
-            /*
-             * If the subsampling is larger than tile size, some tiles were empty and excluded.
-             * The corresponding elements in the `result` array were left to the null value.
-             * We need to compact the array by removing those null elements.
-             */
-            if (needsCompaction) {
-                int n = 0;
-                for (final WritableRaster tile : result) {
-                    if (tile != null) result[n++] = tile;
-                }
-                return Arrays.copyOf(result, n);
+        }
+        /*
+         * If the subsampling is larger than tile size, some tiles were empty and excluded.
+         * The corresponding elements in the `result` array were left to the null value.
+         * We need to compact the array by removing those null elements.
+         */
+        if (needsCompaction) {
+            int n = 0;
+            for (final WritableRaster tile : result) {
+                if (tile != null) result[n++] = tile;
             }
+            return Arrays.copyOf(result, n);
         }
         return result;
     }
@@ -342,7 +357,7 @@ class DataSubset extends TiledGridCoverage implements Localized {
      * @param  upper        (<var>x</var>, <var>y</var>) coordinates after the last pixel to read relative to the tile.
      * @param  subsampling  (<var>sx</var>, <var>sy</var>) subsampling factors.
      * @param  location     pixel coordinates in the upper-left corner of the tile to return.
-     * @return image decoded from the GeoTIFF file.
+     * @return a single tile decoded from the GeoTIFF file.
      * @throws IOException if an I/O error occurred.
      * @throws DataStoreException if a logical error occurred.
      * @throws RuntimeException if the Java2D image can not be created for another reason
@@ -360,16 +375,16 @@ class DataSubset extends TiledGridCoverage implements Localized {
          * is smaller than the "standard" tile size of the image. It happens often when reading the last strip.
          */
         final long length  = multiplyExact(type.size() / Byte.SIZE,
-                             multiplyExact(multiplyExact(width, height), numInterleaved));
-        final long[] size = new long[] {multiplyFull(numInterleaved, getTileSize(0)), getTileSize(1)};
+                             multiplyExact(multiplyExact(width, height), sourcePixelStride));
+        final long[] size = new long[] {multiplyFull(sourcePixelStride, getTileSize(0)), getTileSize(1)};
         /*
-         * If we use an interleaved sample model, each "element" from `HyperRectangleReader` perspective is actually
-         * a group of `numInterleaved` values. Note that in such case, we can not handle subsampling on the first axis.
+         * If we use an interleaved sample model, each "element" from `HyperRectangleReader` perspective is actually a
+         * group of `sourcePixelStride` values. Note that in such case, we can not handle subsampling on the first axis.
          * Such case should be handled by the `CompressedSubset` subclass instead, even if there is no compression.
          */
-        assert numInterleaved == 1 || subsampling[0] == 1;
-        lower[0] *= numInterleaved;
-        upper[0] *= numInterleaved;
+        assert sourcePixelStride == 1 || subsampling[0] == 1;
+        lower[0] *= sourcePixelStride;
+        upper[0] *= sourcePixelStride;
         /*
          * Read each plane ("banks" in Java2D terminology). Note that a single bank contain all bands
          * in the interleaved sample model case.
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index cb6540b..778c3f5 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -221,6 +221,8 @@ final class ImageFileDirectory extends DataCube {
      * The {@code samplesPerPixel} value is usually 1 for bilevel, grayscale and palette-color images,
      * and 3 for RGB images. If this value is higher, then the {@code ExtraSamples} TIFF tag should
      * give an indication of the meaning of the additional channels.
+     *
+     * @see #getNumBands()
      */
     private short samplesPerPixel;
 
@@ -1360,6 +1362,14 @@ final class ImageFileDirectory extends DataCube {
     }
 
     /**
+     * Returns the number of components per pixel.
+     */
+    @Override
+    protected int getNumBands() {
+        return samplesPerPixel;
+    }
+
+    /**
      * Returns the size of tiles. This is also the size of the image sample model.
      * The number of dimensions is always 2 for {@code ImageFileDirectory}.
      *
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
index c2804a3..d502e8c 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
@@ -168,7 +168,7 @@ public abstract class AbstractGridResource extends AbstractResource implements G
                 previous = r;
             }
         }
-        return new RangeArgument(packed);
+        return new RangeArgument(packed, packed.length == numSampleDimensions);
     }
 
     /**
@@ -184,6 +184,11 @@ public abstract class AbstractGridResource extends AbstractResource implements G
         private final long[] packed;
 
         /**
+         * Whether the selection contains all bands of the resource, not necessarily in order.
+         */
+        public final boolean hasAllBands;
+
+        /**
          * If a {@linkplain #insertSubsampling subsampling} has been applied, indices of the first and last band
          * to read, together with the interval (stride) between bands.  Those information are computed only when
          * the {@code insertFoo(…)} methods are invoked.
@@ -201,9 +206,10 @@ public abstract class AbstractGridResource extends AbstractResource implements G
         /**
          * Encapsulates the given {@code range} argument packed in high bits.
          */
-        private RangeArgument(final long[] packed) {
-            this.packed = packed;
-            interval = 1;
+        private RangeArgument(final long[] packed, final boolean hasAllBands) {
+            this.packed      = packed;
+            this.hasAllBands = hasAllBands;
+            this.interval    = 1;
         }
 
         /**
@@ -213,7 +219,9 @@ public abstract class AbstractGridResource extends AbstractResource implements G
          * @return whether user specified all bands in increasing order without subsampling inserted.
          */
         public boolean isIdentity() {
-            if (interval != 1) return false;
+            if (!hasAllBands || interval != 1) {
+                return false;
+            }
             for (int i=0; i<packed.length; i++) {
                 if (packed[i] != ((((long) i) << Integer.SIZE) | i)) {
                     return false;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
index f9abba0..296e6a9 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridCoverage.java
@@ -119,6 +119,7 @@ public abstract class TiledGridCoverage extends GridCoverage {
      *
      * This transform maps {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CORNER pixel corners}.
      *
+     * @see #getSubsampling(int)
      * @see #toFullResolution(long, int)
      */
     private final int[] subsampling, subsamplingOffsets;
@@ -235,6 +236,16 @@ public abstract class TiledGridCoverage extends GridCoverage {
     }
 
     /**
+     * Returns the subsampling in the given dimension.
+     *
+     * @param  dimension  dimension for which to get subsampling.
+     * @return subsampling as a value ≥ 1.
+     */
+    protected final int getSubsampling(final int dimension) {
+        return subsampling[dimension];
+    }
+
+    /**
      * Converts a cell coordinate from this {@code TiledGridCoverage} coordinate space to full resolution.
      * This method removes the subsampling effect. Note that since this {@code TiledGridCoverage} uses the
      * same coordinate space than {@link TiledGridResource}, the converted coordinates should be valid in
@@ -581,6 +592,10 @@ public abstract class TiledGridCoverage extends GridCoverage {
          * if the enclosing {@link TiledGridCoverage} contains only one (potentially big) tile.
          * In that case, the reading process is more like untiled image reading.
          *
+         * <p>The {@link TiledGridCoverage} subsampling is provided for convenience,
+         * but is constant for all tiles regardless the subregion to read.
+         * The same values can be obtained by {@link #getSubsampling(int)}.</p>
+         *
          * @param  lower        a pre-allocated array where to store relative coordinates of the first pixel.
          * @param  upper        a pre-allocated array where to store relative coordinates after the last pixel.
          * @param  subsampling  a pre-allocated array where to store subsampling.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
index cdd4e59..73a662e 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/TiledGridResource.java
@@ -259,7 +259,8 @@ public abstract class TiledGridResource extends AbstractGridResource {
                     selectedBands[i] = rangeIndices.getSourceIndex(i);
                 }
                 assert ArraysExt.isSorted(selectedBands, true);
-                if (ArraysExt.isRange(0, selectedBands)) {
+                if (rangeIndices.hasAllBands) {
+                    assert ArraysExt.isRange(0, selectedBands);
                     selectedBands = null;
                 }
             }
@@ -285,5 +286,16 @@ public abstract class TiledGridResource extends AbstractGridResource {
         public boolean hasSubsampling(final int dimension) {
             return subsampling[dimension] != 1;
         }
+
+        /**
+         * Returns {@code true} if the user requested a subset of bands, or {@code false}
+         * if all bands shall be read in order. For this method, a change of band order
+         * is considered as a "subset".
+         *
+         * @return {@code true} if only a subset of bands will be read or if bands will be read out of order.
+         */
+        public boolean hasBandSubset() {
+            return selectedBands != null;
+        }
     }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/HyperRectangleReader.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/HyperRectangleReader.java
index f2a93d2..122336c 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/HyperRectangleReader.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/HyperRectangleReader.java
@@ -28,6 +28,17 @@ import org.apache.sis.storage.DataStoreContentException;
  * Helper methods for reading a rectangular area, a cube or a hyper-cube from a channel.
  * The data can be stored in an existing array, or a new array can be created.
  * This class does not handle compression; it is rather designed for efficient reading of uncompressed data.
+ * It tries to read the largest possible contiguous blocks of data with single
+ * {@link java.nio.channels.ReadableByteChannel#read(ByteBuffer)} and
+ * {@link ByteBuffer#get(byte[], int, int)} method calls.
+ *
+ * <p>This reader supports subsampling in any dimension. However subsampling in the first dimension
+ * (the one with fastest varying index) is generally not efficient because it forces a large amount
+ * of seek operations. This class makes no special case for making that specific subsampling faster.
+ * It is generally not worth because subsampling in the first dimension is a special case anyway.
+ * It is the "dimension" of bands in an image using the pixel interleaved sample model, so the caller
+ * often needs to process subsampling in the first dimension in a different way than other dimensions
+ * anyway.</p>
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
index a3f5286..b9665e2 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/storage/CoverageReadConsistency.java
@@ -28,8 +28,10 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.internal.util.StandardDateFormat;
+import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.image.PixelIterator;
 import org.apache.sis.math.Statistics;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.TestCase;
@@ -73,6 +75,23 @@ public strictfp class CoverageReadConsistency extends TestCase {
     private final GridCoverage full;
 
     /**
+     * Whether to allow random sub-regions to start elsewhere than (0,0).
+     */
+    private boolean allowOffsets;
+
+    /**
+     * Whether to use random subsamplings.
+     */
+    private boolean allowSubsamplings;
+
+    /**
+     * Whether to use random selection of bands.
+     *
+     * @see #randomRange()
+     */
+    private boolean allowBandSubset;
+
+    /**
      * The random number generator to use.
      */
     private final Random random;
@@ -133,7 +152,7 @@ public strictfp class CoverageReadConsistency extends TestCase {
      */
     @Test
     public void testSubRegionAtOrigin() throws DataStoreException {
-        readAndCompareRandomRegions(false, false);
+        readAndCompareRandomRegions();
     }
 
     /**
@@ -145,7 +164,8 @@ public strictfp class CoverageReadConsistency extends TestCase {
     @Test
     @DependsOnMethod("testSubRegionAtOrigin")
     public void testSubRegionsAnywhere() throws DataStoreException {
-        readAndCompareRandomRegions(true, false);
+        allowOffsets = true;
+        readAndCompareRandomRegions();
     }
 
     /**
@@ -157,7 +177,8 @@ public strictfp class CoverageReadConsistency extends TestCase {
     @Test
     @DependsOnMethod("testSubRegionAtOrigin")
     public void testSubsamplingAtOrigin() throws DataStoreException {
-        readAndCompareRandomRegions(false, true);
+        allowSubsamplings = true;
+        readAndCompareRandomRegions();
     }
 
     /**
@@ -169,26 +190,114 @@ public strictfp class CoverageReadConsistency extends TestCase {
     @Test
     @DependsOnMethod({"testSubsamplingAtOrigin", "testSubRegionsAnywhere"})
     public void testSubsamplingAnywhere() throws DataStoreException {
-        readAndCompareRandomRegions(true, true);
+        allowOffsets      = true;
+        allowSubsamplings = true;
+        readAndCompareRandomRegions();
+    }
+
+    /**
+     * Tests reading random subset of bands in random sub-region starting at coordinates (0,0).
+     *
+     * @throws DataStoreException if an error occurred while using the resource.
+     */
+    @Test
+    @DependsOnMethod("testSubRegionAtOrigin")
+    public void testBandSubsetAtOrigin() throws DataStoreException {
+        allowBandSubset = true;
+        readAndCompareRandomRegions();
+    }
+
+    /**
+     * Tests reading random subset of bands in random sub-regions starting at random offsets.
+     *
+     * @throws DataStoreException if an error occurred while using the resource.
+     */
+    @Test
+    @DependsOnMethod({"testBandSubsetAtOrigin", "testSubRegionsAnywhere"})
+    public void testBandSubsetAnywhere() throws DataStoreException {
+        allowOffsets    = true;
+        allowBandSubset = true;
+        readAndCompareRandomRegions();
+    }
+
+    /**
+     * Tests reading random subset of bands in random sub-regions starting at random offsets
+     * with random subsampling applied.
+     *
+     * @throws DataStoreException if an error occurred while using the resource.
+     */
+    @Test
+    @DependsOnMethod({"testBandSubsetAnywhere", "testSubsamplingAnywhere"})
+    public void testAllAnywhere() throws DataStoreException {
+        allowOffsets      = true;
+        allowBandSubset   = true;
+        allowSubsamplings = true;
+        readAndCompareRandomRegions();
+    }
+
+    /**
+     * Creates a random domain to be used as a query on the {@link #resource} to test.
+     * All arrays given to this method will have their values overwritten.
+     *
+     * @param  gg           value of {@link GridCoverage#getGridGeometry()} on the resource to test.
+     * @param  low          pre-allocated array where to write the lower grid coordinates, inclusive.
+     * @param  high         pre-allocated array where to write the upper grid coordinates, inclusive.
+     * @param  subsampling  pre-allocated array where to write the subsampling.
+     */
+    private GridGeometry randomDomain(final GridGeometry gg, final long[] low, final long[] high, final int[] subsampling) {
+        final GridExtent fullExtent = gg.getExtent();
+        final int dimension = fullExtent.getDimension();
+        for (int d=0; d<dimension; d++) {
+            final int span = StrictMath.toIntExact(fullExtent.getSize(d));
+            final int rs = random.nextInt(span);                            // Span of the sub-region - 1.
+            if (allowOffsets) {
+                low[d] = random.nextInt(span - rs);                         // Note: (span - rs) > 0.
+            }
+            high[d] = low[d] + rs;
+            subsampling[d] = 1;
+            if (allowSubsamplings) {
+                subsampling[d] += random.nextInt(StrictMath.max(rs / 16, 1));
+            }
+        }
+        return gg.derive().subgrid(new GridExtent(null, low, high, true), subsampling).build();
+    }
+
+    /**
+     * Returns the subset of bands to use for testing. This method never return {@code null},
+     * but the set of bands is random only if {@link #allowBandSubset} is {@code true}.
+     */
+    private int[] randomRange(final int numBands) {
+        if (!allowBandSubset) {
+            return ArraysExt.range(0, numBands);
+        }
+        final int[] selectedBands = new int[numBands];
+        for (int i=0; i<numBands; i++) {
+            selectedBands[i] = random.nextInt(numBands);
+        }
+        // Remove duplicated elements.
+        long included = 0;
+        int count = 0;
+        for (final int b : selectedBands) {
+            if (included != (included |= Numerics.bitmask(b))) {
+                selectedBands[count++] = b;
+            }
+        }
+        return ArraysExt.resize(selectedBands, count);
     }
 
     /**
      * Implementation of methods testing reading in random sub-regions with random sub-samplings.
      *
-     * @param  allowOffsets       whether to allow sub-regions to start elsewhere than (0,0).
-     * @param  allowSubsamplings  whether to use random subsamplings.
      * @throws DataStoreException if an error occurred while using the resource.
      */
-    private void readAndCompareRandomRegions(final boolean allowOffsets, final boolean allowSubsamplings)
-            throws DataStoreException
-    {
+    private void readAndCompareRandomRegions() throws DataStoreException {
         final GridGeometry gg = resource.getGridGeometry();
-        final GridExtent fullExtent = gg.getExtent();
-        final int    dimension   = fullExtent.getDimension();
+        final int    dimension   = gg.getDimension();
         final long[] low         = new long[dimension];
         final long[] high        = new long[dimension];
         final int [] subsampling = new int [dimension];
         final int [] subOffsets  = new int [dimension];
+        final int    numBands    = resource.getSampleDimensions().size();
         /*
          * We will collect statistics on execution time only if the
          * test is executed in a more verbose mode than the default.
@@ -196,30 +305,15 @@ public strictfp class CoverageReadConsistency extends TestCase {
         final Statistics durations = (VERBOSE || !failOnMismatch) ? new Statistics("time (ms)") : null;
         int failuresCount = 0;
         for (int it=0; it < numIterations; it++) {
-            /*
-             * Create a random domain to be used as a query on the `GridCoverageResource`.
-             */
-            for (int d=0; d<dimension; d++) {
-                final int span = StrictMath.toIntExact(fullExtent.getSize(d));
-                final int rs = random.nextInt(span);                    // Span of the sub-region - 1.
-                if (allowOffsets) {
-                    low[d] = random.nextInt(span - rs);                 // Note: (span - rs) > 0.
-                }
-                high[d] = low[d] + rs;
-                subsampling[d] = 1;
-                if (allowSubsamplings) {
-                    subsampling[d] += random.nextInt(StrictMath.max(rs / 16, 1));
-                }
-            }
-            final GridGeometry domain = gg.derive()
-                    .subgrid(new GridExtent(null, low, high, true), subsampling).build();
+            final GridGeometry domain = randomDomain(gg, low, high, subsampling);
+            final int[] selectedBands = randomRange(numBands);
             /*
              * Read a coverage containing the requested sub-domain. Note that the reader is free to read
              * more data than requested. The extent actually read is `actualReadExtent`. It shall contain
              * fully the requested `domain`.
              */
             final long startTime = System.nanoTime();
-            final GridCoverage subset = resource.read(domain, null);
+            final GridCoverage subset = resource.read(domain, selectedBands);
             final GridExtent actualReadExtent = subset.getGridGeometry().getExtent();
             if (failOnMismatch) {
                 assertEquals("Unexpected number of dimensions.", dimension, actualReadExtent.getDimension());
@@ -251,16 +345,17 @@ nextSlice:  for (;;) {
                 System.arraycopy(sliceMin, BIDIMENSIONAL, sliceMax, BIDIMENSIONAL, dimension - BIDIMENSIONAL);
                 final PixelIterator itr = iterator(full,   sliceMin, sliceMax, subsampling, subOffsets, allowSubsamplings);
                 final PixelIterator itc = iterator(subset, sliceMin, sliceMax, subsampling, subOffsets, false);
-                if (durations != null) {
-                    durations.accept((System.nanoTime() - startTime) / (double) StandardDateFormat.NANOS_PER_MILLISECOND);
-                }
                 if (itr != null) {
                     assertEquals(itr.getDomain().getSize(), itc.getDomain().getSize());
-                    double[] expected = null, actual = null;
+                    final double[] expected = new double[selectedBands.length];
+                    double[] reference = null, actual = null;
                     while (itr.next()) {
                         assertTrue(itc.next());
-                        expected = itr.getPixel(expected);
-                        actual   = itc.getPixel(actual);
+                        reference = itr.getPixel(reference);
+                        actual    = itc.getPixel(actual);
+                        for (int i=0; i<selectedBands.length; i++) {
+                            expected[i] = reference[selectedBands[i]];
+                        }
                         if (!Arrays.equals(expected, actual)) {
                             failuresCount++;
                             if (!failOnMismatch) break;
@@ -269,7 +364,7 @@ nextSlice:  for (;;) {
                             final StringBuilder message = new StringBuilder(100).append("Mismatch at position (")
                                     .append(pr.x).append(", ").append(pr.y).append(") in full image and (")
                                     .append(pc.x).append(", ").append(pc.y).append(") in tested sub-image");
-                            findMatchPosition(itr, pr, expected, message);
+                            findMatchPosition(itr, pr, selectedBands, actual, message);
                             assertArrayEquals(message.toString(), expected, actual, STRICT);
                         }
                     }
@@ -291,6 +386,9 @@ nextSlice:  for (;;) {
                 }
                 break;
             }
+            if (durations != null) {
+                durations.accept((System.nanoTime() - startTime) / (double) StandardDateFormat.NANOS_PER_MILLISECOND);
+            }
         }
         /*
          * Show statistics only if the test are executed with the `VERBOSE` flag set,
@@ -373,8 +471,11 @@ nextSlice:  for (;;) {
      * Explores pixel values around the given position in search for a pixel having the expected values.
      * If a match is found, the error message is completed with information about the match position.
      */
-    private static void findMatchPosition(final PixelIterator ir, final Point pr, final double[] expected, final StringBuilder message) {
-        double[] actual = null;
+    private static void findMatchPosition(final PixelIterator ir, final Point pr, final int[] selectedBands,
+                                          final double[] actual, final StringBuilder message)
+    {
+        final double[] expected = new double[actual.length];
+        double[] reference = null;
         for (int dy=0; dy<10; dy++) {
             for (int dx=0; dx<10; dx++) {
                 if ((dx | dy) != 0) {
@@ -386,7 +487,10 @@ nextSlice:  for (;;) {
                         } catch (IndexOutOfBoundsException e) {
                             continue;
                         }
-                        actual = ir.getPixel(actual);
+                        reference = ir.getPixel(reference);
+                        for (int i=0; i<selectedBands.length; i++) {
+                            expected[i] = reference[selectedBands[i]];
+                        }
                         if (Arrays.equals(expected, actual)) {
                             message.append(" (note: found a match at offset (").append(x).append(", ").append(y)
                                    .append(") in full image)");

[sis] 05/07: Add AbstractGridCoverage.RangeArgument.select(SampleModel)` method with capability to "compress" the sample model for skipping bands at reading time (instead of ignoring them after they are in memory).

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 d06a9b19086bfc5b9ddaffda68d8a577e3375ccc
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Jul 27 19:51:25 2021 +0200

    Add AbstractGridCoverage.RangeArgument.select(SampleModel)` method
    with capability to "compress" the sample model for skipping bands
    at reading time (instead of ignoring them after they are in memory).
---
 .../internal/coverage/j2d/SampleModelFactory.java  | 295 +++++++++++++++++++++
 .../coverage/j2d/SampleModelFactoryTest.java       | 148 +++++++++++
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 .../sis/storage/geotiff/ImageFileDirectory.java    |  41 +--
 .../sis/internal/storage/AbstractGridResource.java | 114 +++++++-
 5 files changed, 552 insertions(+), 47 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/SampleModelFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/SampleModelFactory.java
new file mode 100644
index 0000000..02f16d8
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/SampleModelFactory.java
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.util.Arrays;
+import java.awt.image.DataBuffer;
+import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.ComponentSampleModel;
+import java.awt.image.PixelInterleavedSampleModel;
+import java.awt.image.MultiPixelPackedSampleModel;
+import java.awt.image.SinglePixelPackedSampleModel;
+import java.awt.image.RasterFormatException;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.image.DataType;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * A factory for {@link SampleModel} instances. This class provides a convenient way to get the properties
+ * of an existing sample model, modify them, then create a new sample model with the modified properties.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @see SampleModel#createCompatibleSampleModel(int, int)
+ * @see SampleModel#createSubsetSampleModel(int[])
+ *
+ * @since 1.1
+ * @module
+ */
+public final class SampleModelFactory {
+    /**
+     * Type of sample values as one of the {@link DataBuffer} constants.
+     */
+    private int dataType;
+
+    /**
+     * Width and height in pixels of tiles using the sample model.
+     *
+     * <p><b>Warning:</b> changing sample model size has non-obvious implications in {@link BandedSampleModel}
+     * case when all data are in a single bank. The {@link SampleModel#createCompatibleSampleModel(int, int)}
+     * method should be preferred instead.</p>
+     *
+     * @see SampleModel#createCompatibleSampleModel(int, int)
+     */
+    private final int width, height;
+
+    /**
+     * Number of bands of the image data.
+     * Note: do not confuse <em>bands</em> and <em>banks</em>.
+     */
+    private int numBands;
+
+    /**
+     * Index for each bank storing a band of image data, or {@code null} if not applicable.
+     */
+    private int[] bankIndices;
+
+    /**
+     * Offsets for all bands in data array elements, or {@code null} if not applicable.
+     */
+    private int[] bandOffsets;
+
+    /**
+     * The bit masks for all bands, or {@code null} if not applicable.
+     */
+    private int[] bitMasks;
+
+    /**
+     * The bit offset into the data array where the first pixel begins.
+     */
+    private int dataBitOffset;
+
+    /**
+     * Number of bits per pixel, or 0 if not applicable.
+     * Also known as "pixel bit stride".
+     */
+    private int numberOfBits;
+
+    /**
+     * Number of sample values to advance for moving to next pixel.
+     * This is 0 when {@link #numberOfBits} should be used instead.
+     */
+    private int pixelStride;
+
+    /**
+     * Number of sample values to advance for moving to next line.
+     */
+    private int scanlineStride;
+
+    /**
+     * Creates a factory initialized to the given values.
+     *
+     * @param  type           type of sample values.
+     * @param  width          tile width in pixels.
+     * @param  height         tile height in pixels.
+     * @param  numBands       number of bands.
+     * @param  bitsPerSample  number of bits per sample values.
+     * @param  isBanded       {@code true} if each band is stored in a separated bank.
+     * @throws RasterFormatException if the arguments imply a sample model of unsupported type.
+     */
+    public SampleModelFactory(final DataType type, final int width, final int height,
+            final int numBands, final int bitsPerSample, final boolean isBanded)
+    {
+        this.dataType  = type.toDataBufferType();
+        this.width     = width;
+        this.height    = height;
+        this.numBands  = numBands;
+        scanlineStride = width;
+        pixelStride    = 1;
+        if (bitsPerSample != type.size()) {
+            if (numBands == 1) {
+                // MultiPixelPackedSampleModel
+                pixelStride    = 0;
+                numberOfBits   = bitsPerSample;
+                scanlineStride = Numerics.ceilDiv(Math.multiplyExact(width, numberOfBits), type.size());
+            } else if (!isBanded) {
+                // SinglePixelPackedSampleModel
+                bitMasks = new int[numBands];
+                bitMasks[0] = (1 << bitsPerSample) - 1;
+                for (int i=1; i < bitMasks.length; i++) {
+                    bitMasks[i] = bitMasks[i-1] << bitsPerSample;
+                }
+            } else {
+                // TODO: we can support that with a little bit more work.
+                throw new RasterFormatException(Errors.format(Errors.Keys.UnsupportedType_1, "bitsPerSample=" + type.size()));
+            }
+        } else if (isBanded) {
+            // BandedSampleModel
+            bankIndices = ArraysExt.range(0, numBands);
+            bandOffsets = new int[numBands];
+        } else {
+            // PixelInterleavedSampleModel
+            bandOffsets    = ArraysExt.range(0, numBands);
+            scanlineStride = Math.multiplyExact(numBands, width);
+            pixelStride    = numBands;
+        }
+    }
+
+    /**
+     * Creates a new factory initialized to the value of given sample model.
+     *
+     * @param  model  the sample model from which to copy values.
+     * @throws RasterFormatException if the type of the given sample model is not supported.
+     */
+    public SampleModelFactory(final SampleModel model) {
+        width    = model.getWidth();
+        height   = model.getHeight();
+        numBands = model.getNumBands();
+        dataType = model.getDataType();
+        if (model instanceof ComponentSampleModel) {
+            final ComponentSampleModel cm = (ComponentSampleModel) model;
+            bankIndices    = cm.getBankIndices();
+            bandOffsets    = cm.getBandOffsets();
+            scanlineStride = cm.getScanlineStride();
+            pixelStride    = cm.getPixelStride();
+            for (int i=0; i<bankIndices.length; i++) {
+                if (bankIndices[i] != 0) return;
+            }
+            bankIndices = null;     // PixelInterleavedSampleModel
+        } else if (model instanceof SinglePixelPackedSampleModel) {
+            final SinglePixelPackedSampleModel cm = (SinglePixelPackedSampleModel) model;
+            bitMasks       = cm.getBitMasks();
+            scanlineStride = cm.getScanlineStride();
+            pixelStride    = 1;
+        } else if (model instanceof MultiPixelPackedSampleModel) {
+            final MultiPixelPackedSampleModel cm = (MultiPixelPackedSampleModel) model;
+            numberOfBits   = cm.getPixelBitStride();
+            dataBitOffset  = cm.getDataBitOffset();
+            scanlineStride = cm.getScanlineStride();
+        } else {
+            throw new RasterFormatException(Errors.format(Errors.Keys.UnsupportedType_1, model.getClass()));
+        }
+    }
+
+    /**
+     * Requests a sample model with only a subset of the bands of the original sample model.
+     * Special cases:
+     *
+     * <ul>
+     *   <li>For {@link SinglePixelPackedSampleModel}, this method may change the data type
+     *       if decreasing the number of bands make possible to store pixels in smaller integers.</li>
+     * </ul>
+     *
+     * <h4>Comparison with standard method</h4>
+     * The standard {@link SampleModel#createSubsetSampleModel(int[])} method also selects a subset of the bands.
+     * However that standard method creates a sample model accepting the same {@link java.awt.image.DataBuffer}
+     * than the original {@link SampleModel}, which is useful for creating a <em>view</em> of image data that are
+     * already in memory. By contrast, this {@code BandSelector} <em>compresses</em> bank indices or pixel masks
+     * for leaving no empty space between them. This is useful when done <em>before</em> loading data from a file
+     * in order to avoid consuming space for bands that were not requested.
+     *
+     * @param  bands  bands to keep.
+     *
+     * @see SampleModel#createSubsetSampleModel(int[])
+     */
+    public void subsetAndCompress(final int[] bands) {
+        ArgumentChecks.ensureSizeBetween("bands", 1, numBands, bands.length);
+        if (bankIndices != null) bankIndices = subset(bankIndices, bands, true);
+        if (bandOffsets != null) bandOffsets = subset(bandOffsets, bands, bankIndices == null);
+        if (bitMasks    != null) {
+            final int[] shifts = new int[bitMasks.length];      // Number of bits to "remove".
+            for (int i=0; i<shifts.length; i++) shifts[i] = bitCount(bitMasks[i]);
+            for (int i=0; i< bands.length; i++) shifts[bands[i]] = 0;
+            for (int i=1; i<shifts.length; i++) shifts[i] += shifts[i-1];
+            final int[] masks = new int[bands.length];
+            int allMasks = 0;
+            for (int i=0; i<bands.length; i++) {
+                final int b = bands[i];
+                allMasks |= (masks[i] = bitMasks[b] >>> shifts[b]);
+            }
+            bitMasks = masks;
+            if (dataType == DataBuffer.TYPE_INT    && (allMasks & ~0xFFFF) == 0) dataType = DataBuffer.TYPE_USHORT;
+            if (dataType == DataBuffer.TYPE_USHORT && (allMasks & ~0x00FF) == 0) dataType = DataBuffer.TYPE_BYTE;
+        }
+        if (pixelStride > 1) {
+            final int s    = scanlineStride / pixelStride;
+            final int r    = scanlineStride % pixelStride;
+            pixelStride   -= (numBands - bands.length);
+            scanlineStride = pixelStride * s + r;
+        }
+        numBands = bands.length;
+    }
+
+    /**
+     * Returns the number of bits in the given mask. All bits between the first and last bits
+     * are considered set to 1. This is intentional, because masks shall not overlap.
+     *
+     * @see Integer#bitCount(int)
+     */
+    private static int bitCount(final int mask) {
+        return Integer.SIZE - Integer.numberOfLeadingZeros(mask) - Integer.numberOfTrailingZeros(mask);
+    }
+
+    /**
+     * Creates a subset of the {@code data} arrays for the specified bands.
+     * If {@code compress} is {@code true}, then sequences such as {1, 3, 4, 6, …}
+     * are replaced by {0, 1, 2, 3, …} (not necessarily in increasing order).
+     */
+    private static int[] subset(final int[] data, final int[] bands, final boolean compress) {
+        final int[] subset = new int[bands.length];
+        for (int i=0; i<bands.length; i++) {
+            subset[i] = data[bands[i]];
+        }
+        if (!compress) {
+            return subset;
+        }
+        Arrays.sort(subset);
+        final int[] indices = new int[subset.length];
+        for (int i=0; i<bands.length; i++) {
+            indices[i] = Arrays.binarySearch(subset, data[bands[i]]);
+            assert indices[i] >= 0;
+        }
+        return indices;
+    }
+
+    /**
+     * Builds a sample model based on current factory configuration.
+     * The factory is still valid after this method call.
+     *
+     * @return the sample model built from current factory configuration.
+     * @throws IllegalArgumentException if an error occurred while building a sample model.
+     */
+    public SampleModel build() {
+        if (pixelStride == 1) {
+            if (bankIndices != null) {
+                return new BandedSampleModel(dataType, width, height, scanlineStride, bankIndices, bandOffsets);
+            } else if (bitMasks != null) {
+                return new SinglePixelPackedSampleModel(dataType, width, height, scanlineStride, bitMasks);
+            }
+        }
+        if (numberOfBits != 0) {
+            return new MultiPixelPackedSampleModel(dataType, width, height, numberOfBits, scanlineStride, dataBitOffset);
+        } else {
+            return new PixelInterleavedSampleModel(dataType, width, height, pixelStride, scanlineStride, bandOffsets);
+        }
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/SampleModelFactoryTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/SampleModelFactoryTest.java
new file mode 100644
index 0000000..9b8baf8
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/SampleModelFactoryTest.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.awt.image.DataBuffer;
+import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.PixelInterleavedSampleModel;
+import java.awt.image.SinglePixelPackedSampleModel;
+import java.awt.image.MultiPixelPackedSampleModel;
+import org.apache.sis.image.DataType;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.opengis.test.Assert.assertInstanceOf;
+
+
+/**
+ * Tests {@link SampleModelFactory}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final strictfp class SampleModelFactoryTest extends TestCase {
+    /**
+     * Arbitrary width, height and number of bands for the sample models to create.
+     * Size does not matter because we will not create data buffer.
+     */
+    private static final int WIDTH = 200, HEIGHT = 300, NUM_BANDS = 6;
+
+    /**
+     * Tests the creation and modification of a {@link BandedSampleModel}.
+     */
+    @Test
+    public void testBanded() {
+        final BandedSampleModel model = test(BandedSampleModel.class,
+                new SampleModelFactory(DataType.FLOAT, WIDTH, HEIGHT, NUM_BANDS, Float.SIZE, true));
+
+        assertArrayEquals("bankIndices", new int[] {1, 0, 2}, model.getBankIndices());
+        assertArrayEquals("bandOffsets", new int[] {0, 0, 0}, model.getBandOffsets());
+        assertEquals("pixelStride",        1, model.getPixelStride());
+        assertEquals("scanlineStride", WIDTH, model.getScanlineStride());
+        assertEquals("dataType", DataBuffer.TYPE_FLOAT, model.getDataType());
+    }
+
+    /**
+     * Tests the creation and modification of a {@link PixelInterleavedSampleModel}.
+     */
+    @Test
+    public void testPixelInterleaved() {
+        final PixelInterleavedSampleModel model = test(PixelInterleavedSampleModel.class,
+                new SampleModelFactory(DataType.BYTE, WIDTH, HEIGHT, NUM_BANDS, Byte.SIZE, false));
+
+        assertArrayEquals("bankIndices", new int[] {0, 0, 0}, model.getBankIndices());
+        assertArrayEquals("bandOffsets", new int[] {1, 0, 2}, model.getBandOffsets());
+        assertEquals("pixelStride",          3, model.getPixelStride());
+        assertEquals("scanlineStride", WIDTH*3, model.getScanlineStride());
+        assertEquals("dataType", DataBuffer.TYPE_BYTE, model.getDataType());
+    }
+
+    /**
+     * Tests the creation and modification of a {@link SinglePixelPackedSampleModel}.
+     * Note that for this kind of sample model, the type changes from 32 bits to 16 bits integer
+     * because the decrease in number of bands make possible to store a pixel in smaller integers.
+     */
+    @Test
+    public void testSinglePixelPacked() {
+        final SinglePixelPackedSampleModel model = test(SinglePixelPackedSampleModel.class,
+                new SampleModelFactory(DataType.INT, WIDTH, HEIGHT, NUM_BANDS, 5, false));
+
+        final int[] expected = {
+            0b1111100000,           // Band 2 specified, 1 after compression.
+            0b11111,                // Band 1 specified, 0 after compression.
+            0b111110000000000       // Band 4 specified, 2 after compression.
+        };
+        assertArrayEquals("bitMasks", expected, model.getBitMasks());
+        assertEquals("scanlineStride", WIDTH, model.getScanlineStride());
+        assertEquals("dataType", DataBuffer.TYPE_USHORT, model.getDataType());
+    }
+
+    /**
+     * Tests the creation and modification of a {@link MultiPixelPackedSampleModel}.
+     * We can not test band sub-setting for this kind of sample model because it can
+     * only have a single band.
+     */
+    @Test
+    public void testPixelMultiPixelPacked() {
+        final int bitsPerSample = 4;
+        SampleModelFactory factory = new SampleModelFactory(DataType.INT, WIDTH, HEIGHT, 1, bitsPerSample, false);
+        final MultiPixelPackedSampleModel model = (MultiPixelPackedSampleModel) factory.build();
+
+        assertEquals("pixelBitStride", bitsPerSample, model.getPixelBitStride());
+        assertEquals("scanlineStride", WIDTH / (Integer.SIZE / bitsPerSample), model.getScanlineStride());
+        assertEquals("dataType", DataBuffer.TYPE_INT, model.getDataType());
+
+        factory = new SampleModelFactory(model);
+        assertEquals(model, factory.build());
+    }
+
+    /**
+     * Builds a sample model using the given factory, tests basic properties, then applies a band subset.
+     * The band subset is exactly: {2, 1, 4}.
+     */
+    private static <T extends SampleModel> T test(final Class<T> modelType, SampleModelFactory factory) {
+        final SampleModel model = factory.build();
+        assertEquals(WIDTH,     model.getWidth());
+        assertEquals(HEIGHT,    model.getHeight());
+        assertEquals(NUM_BANDS, model.getNumBands());
+        assertInstanceOf("build", modelType, model);
+        /*
+         * Select a subset of the bands and verify again.
+         * The subset is fixed by this method's contract.
+         */
+        final int[] bands = {2, 1, 4};
+        factory.subsetAndCompress(bands);
+        final SampleModel subset = factory.build();
+        assertEquals(WIDTH,  subset.getWidth());
+        assertEquals(HEIGHT, subset.getHeight());
+        assertEquals(3,      subset.getNumBands());
+        assertInstanceOf("subsetAndCompress", modelType, subset);
+        /*
+         * Repeat the same operations on a factory created using a sample
+         * model as a template, and verify that we get the same results.
+         */
+        factory = new SampleModelFactory(model);
+        assertEquals(model, factory.build());
+        factory.subsetAndCompress(bands);
+        assertEquals(subset, factory.build());
+        return modelType.cast(subset);
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index ed26d95..b3cc18f 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -80,6 +80,7 @@ import org.junit.runners.Suite;
     org.apache.sis.internal.coverage.j2d.ImageLayoutTest.class,
     org.apache.sis.internal.coverage.j2d.ScaledColorSpaceTest.class,
     org.apache.sis.internal.coverage.j2d.ColorizerTest.class,
+    org.apache.sis.internal.coverage.j2d.SampleModelFactoryTest.class,
     org.apache.sis.internal.processing.image.IsolinesTest.class,
     org.apache.sis.image.DataTypeTest.class,
     org.apache.sis.image.PlanarImageTest.class,
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index a71ee3a..a9fbe5c 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -28,10 +28,8 @@ import java.nio.charset.Charset;
 import java.awt.Color;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
-import java.awt.image.BandedSampleModel;
-import java.awt.image.MultiPixelPackedSampleModel;
-import java.awt.image.PixelInterleavedSampleModel;
 import java.awt.image.SinglePixelPackedSampleModel;
+import java.awt.image.RasterFormatException;
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
 import org.opengis.metadata.citation.DateType;
@@ -42,6 +40,7 @@ import org.apache.sis.internal.geotiff.Resources;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.internal.coverage.j2d.SampleModelFactory;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.storage.DataStoreException;
@@ -1352,38 +1351,10 @@ final class ImageFileDirectory extends DataCube {
      */
     @Override
     protected SampleModel getSampleModel() throws DataStoreContentException {
-        if (sampleModel == null) {
-            final DataType type = getDataType();
-            try {
-                final int bt = type.toDataBufferType();
-                if (bitsPerSample != type.size()) {
-                    /*
-                     * Supported types for both sample models are TYPE_BYTE, TYPE_USHORT, and TYPE_INT.
-                     * Note that if the TIFF data are signed bytes, then we have TYPE_SHORT which will
-                     * cause an exception to be thrown be sample model constructor.
-                     */
-                    if (samplesPerPixel == 1) {
-                        sampleModel = new MultiPixelPackedSampleModel(bt, tileWidth, tileHeight, bitsPerSample);
-                    } else if (!isPlanar) {
-                        final int[] bitMasks = new int[samplesPerPixel];
-                        bitMasks[0] = (1 << bitsPerSample) - 1;
-                        for (int i=1; i<bitMasks.length; i++) {
-                            bitMasks[i] = bitMasks[i-1] << bitsPerSample;
-                        }
-                        sampleModel = new SinglePixelPackedSampleModel(bt, tileWidth, tileHeight, bitMasks);
-                    } else {
-                        // TODO: we can support that with a little bit more work.
-                        throw new DataStoreContentException(Errors.format(Errors.Keys.UnsupportedType_1, type));
-                    }
-                } else if (isPlanar) {
-                    sampleModel = new BandedSampleModel(bt, tileWidth, tileHeight, samplesPerPixel);
-                } else {
-                    sampleModel = new PixelInterleavedSampleModel(bt, tileWidth, tileHeight, samplesPerPixel,
-                            Math.multiplyExact(samplesPerPixel, tileWidth), ArraysExt.range(0, samplesPerPixel));
-                }
-            } catch (IllegalArgumentException e) {
-                throw new DataStoreContentException(Errors.format(Errors.Keys.UnsupportedType_1, type), e);
-            }
+        if (sampleModel == null) try {
+            sampleModel = new SampleModelFactory(getDataType(), tileWidth, tileHeight, samplesPerPixel, bitsPerSample, isPlanar).build();
+        } catch (IllegalArgumentException | RasterFormatException e) {
+            throw new DataStoreContentException(Errors.format(Errors.Keys.UnsupportedType_1, getDataType()), e);
         }
         return sampleModel;
     }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
index 31d0f68..c2804a3 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
@@ -25,6 +25,9 @@ import java.util.logging.Logger;
 import java.util.logging.LogRecord;
 import java.util.concurrent.TimeUnit;
 import java.math.RoundingMode;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import java.awt.image.RasterFormatException;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.referencing.operation.TransformException;
@@ -43,6 +46,8 @@ import org.apache.sis.measure.AngleFormat;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.PerformanceLevel;
+import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.internal.coverage.j2d.SampleModelFactory;
 import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.internal.jdk9.JDK9;
@@ -171,11 +176,6 @@ public abstract class AbstractGridResource extends AbstractResource implements G
      */
     protected static final class RangeArgument {
         /**
-         * Name of the extent dimension for bands.
-         */
-        private static final DimensionNameType BAND = DimensionNameType.valueOf("BAND");
-
-        /**
          * The range indices specified by user in high bits, together (in the low bits)
          * with the position in the {@code ranges} array where each index was specified.
          * This packing is used for making easier to sort this array in increasing order
@@ -187,6 +187,9 @@ public abstract class AbstractGridResource extends AbstractResource implements G
          * If a {@linkplain #insertSubsampling subsampling} has been applied, indices of the first and last band
          * to read, together with the interval (stride) between bands.  Those information are computed only when
          * the {@code insertFoo(…)} methods are invoked.
+         *
+         * @see #insertBandDimension(GridExtent, int)
+         * @see #insertSubsampling(int[], int)
          */
         private int first, last, interval;
 
@@ -204,9 +207,10 @@ public abstract class AbstractGridResource extends AbstractResource implements G
         }
 
         /**
-         * Returns {@code true} if user specified all bands in increasing order and no subsampling is applied.
+         * Returns {@code true} if user specified all bands in increasing order.
+         * This method always return {@code false} if {@link #insertSubsampling(int[], int)} has been invoked.
          *
-         * @return whether user specified all bands in increasing order without subsampling.
+         * @return whether user specified all bands in increasing order without subsampling inserted.
          */
         public boolean isIdentity() {
             if (interval != 1) return false;
@@ -219,15 +223,31 @@ public abstract class AbstractGridResource extends AbstractResource implements G
         }
 
         /**
-         * Returns the number of sample dimensions. This is the length of the range array supplied by user.
+         * Returns the number of sample dimensions. This is the length of the range array supplied by user,
+         * or the number of bands in the source coverage if the {@code range} array was null or empty.
          *
-         * @return the number of sample dimensions.
+         * @return the number of sample dimensions selected by user.
          */
         public int getNumBands() {
             return packed.length;
         }
 
         /**
+         * Returns the indices of bands selected by the user.
+         * This is a copy of the {@code range} argument specified by the user, in same order.
+         * Note that this is not necessarily increasing order.
+         *
+         * @return a copy of the {@code range} argument specified by the user.
+         */
+        public int[] getSelectedBands() {
+            final int[] bands = new int[getNumBands()];
+            for (int i=0; i<bands.length; i++) {
+                bands[getTargetIndex(i)] = getSourceIndex(i);
+            }
+            return bands;
+        }
+
+        /**
          * Returns the value of the first index specified by the user. This is not necessarily equal to
          * {@code getSourceIndex(0)} if the user specified the bands out of order.
          *
@@ -278,7 +298,7 @@ public abstract class AbstractGridResource extends AbstractResource implements G
          *     }
          * }
          *
-         * If the {@code insert} methods have never been invoked, then this method is equivalent to {@link #getSourceIndex(int)}.
+         * If the {@code insertXXX(…)} methods have never been invoked, then this method is equivalent to {@link #getSourceIndex(int)}.
          *
          * @param  i  index of the range index to get, from 0 inclusive to {@link #getNumBands()} exclusive.
          * @return index of the i<sup>th</sup> band to read from the resource, after subsampling.
@@ -289,7 +309,7 @@ public abstract class AbstractGridResource extends AbstractResource implements G
 
         /**
          * Returns the increment to apply on index for moving to the same band of the next pixel.
-         * If the {@code insert} methods have never been invoked, then this method returns 1.
+         * If the {@code insertXXX(…)} methods have never been invoked, then this method returns 1.
          *
          * @return the increment to apply on index for moving to the next pixel in the same band.
          *
@@ -304,6 +324,15 @@ public abstract class AbstractGridResource extends AbstractResource implements G
          * will range from the minimum {@code range} value to the maximum {@code range} value inclusive.
          * This method should be used together with {@link #insertSubsampling(int[], int)}.
          *
+         * <h4>Use case</h4>
+         * This method is useful for reading a <var>n</var>-dimensional data cube with values stored in a
+         * {@link java.awt.image.PixelInterleavedSampleModel} fashion (except if {@code bandDimension} is
+         * after all existing {@code areaOfInterest} dimensions, in which case data become organized in a
+         * {@link java.awt.image.BandedSampleModel} fashion). This method converts the specified domain
+         * (decomposed in {@code areaOfInterest} and {@code subsamplings} parameters) into a larger domain
+         * encompassing band dimension as if it was an ordinary space or time dimension. It makes possible
+         * to use this domain with {@link org.apache.sis.internal.storage.io.HyperRectangleReader} for example.
+         *
          * @param  areaOfInterest  the extent to which to add a new dimension for bands.
          * @param  bandDimension   index of the band dimension.
          * @return a new extent with the same values than the given extent plus one dimension for bands.
@@ -311,13 +340,21 @@ public abstract class AbstractGridResource extends AbstractResource implements G
         public GridExtent insertBandDimension(final GridExtent areaOfInterest, final int bandDimension) {
             first = getSourceIndex(0);
             last  = getSourceIndex(packed.length - 1);
-            return areaOfInterest.insert(bandDimension, BAND, first, last, true);
+            return areaOfInterest.insert(bandDimension, DimensionNameType.valueOf("BAND"), first, last, true);
         }
 
         /**
          * Returns the given subsampling with a new dimension added for the bands. The subsampling in the new
          * dimension will be the greatest common divisor of the difference between all user-specified values.
          * This method should be used together with {@link #insertBandDimension(GridExtent, int)}.
+         * See that method for more information.
+         *
+         * <p>Invoking this method changes the values returned by following methods:</p>
+         * <ul>
+         *   <li>{@link #isIdentity()}</li>
+         *   <li>{@link #getSubsampledIndex(int)}</li>
+         *   <li>{@link #getPixelStride()}</li>
+         * </ul>
          *
          * @param  subsamplings   the subsampling to which to add a new dimension for bands.
          * @param  bandDimension  index of the band dimension.
@@ -351,6 +388,59 @@ public abstract class AbstractGridResource extends AbstractResource implements G
         }
 
         /**
+         * Returns a sample model for the bands specified by the user.
+         * The model created by this method can be a "view" or can be "compressed":
+         *
+         * <ul class="verbose">
+         *   <li>If {@code view} is {@code true}, the sample model returned by this method will expect the
+         *       same {@link java.awt.image.DataBuffer} than the one expected by the original {@code model}.
+         *       Bands enumerated in the {@code range} argument will be used and other bands will be ignored.
+         *       This mode is efficient if the data are already in memory and we want to avoid copying them.
+         *       An inconvenient is that all bands, including the ignored ones, are retained in memory.</li>
+         *   <li>If {@code view} is {@code false}, then this method will "compress" bank indices and bit masks
+         *       for making them consecutive. For example if the {@code range} argument specifies that the bands
+         *       to read are {1, 3, 4, 6, …}, then "compressed" sample model will use bands {0, 1, 2, 3, …}.
+         *       This mode is efficient if the data are not yet in memory and the reader is capable to skip
+         *       the bands to ignore. In such case, this mode save memory.</li>
+         * </ul>
+         *
+         * @param  model  the original sample model with all bands. Can be {@code null}.
+         * @param  view   whether the band subset shall be a view over the full band set.
+         * @return the sample model for a subset of bands, or {@code null} if the given sample model was null.
+         * @throws RasterFormatException if the given sample model is not recognized.
+         * @throws IllegalArgumentException if an error occurred when constructing the new sample model.
+         *
+         * @see SampleModel#createSubsetSampleModel(int[])
+         * @see SampleModelFactory#subsetAndCompress(int[])
+         */
+        public SampleModel select(final SampleModel model, final boolean view) {
+            if (model == null || isIdentity()) {
+                return model;
+            }
+            final int[] bands = getSelectedBands();
+            if (view) {
+                return model.createSubsetSampleModel(bands);
+            } else {
+                final SampleModelFactory factory = new SampleModelFactory(model);
+                factory.subsetAndCompress(bands);
+                return factory.build();
+            }
+        }
+
+        /**
+         * Returns a color model for the bands specified by the user.
+         *
+         * @param  colors  the original color model with all bands. Can be {@code null}.
+         * @return the color model for a subset of bands, or {@code null} if the given color model was null.
+         */
+        public ColorModel select(final ColorModel colors) {
+            if (colors == null || isIdentity()) {
+                return colors;
+            }
+            return ColorModelFactory.createSubset(colors, getSelectedBands());
+        }
+
+        /**
          * Returns a builder for sample dimensions. This method recycles the same builder on every calls.
          * If the builder has been returned by a previous call to this method,
          * then it is {@linkplain SampleDimension.Builder#clear() cleared} before to be returned again.