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 9c514b4f87b529794374580972be724c4e4d7c55
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Mar 26 15:42:08 2022 +0100

    Move `RangeInternal` to a separated class.
    The intent is to keep it internal while moving `AbstractGridResource` to a 
public package.
---
 .../apache/sis/internal/netcdf/RasterResource.java |   3 +-
 .../sis/internal/storage/AbstractGridResource.java | 368 +-------------------
 .../sis/internal/storage/MemoryGridResource.java   |   2 +-
 .../apache/sis/internal/storage/RangeArgument.java | 386 +++++++++++++++++++++
 .../sis/internal/storage/TiledGridResource.java    |   2 +-
 .../internal/storage/MemoryGridResourceTest.java   |   2 +-
 ...ridResourceTest.java => RangeArgumentTest.java} |  20 +-
 .../apache/sis/test/suite/StorageTestSuite.java    |   2 +-
 8 files changed, 414 insertions(+), 371 deletions(-)

diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 499f55e..9ba27aa 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -55,6 +55,7 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.jdk9.JDK9;
 import org.apache.sis.internal.storage.MetadataBuilder;
+import org.apache.sis.internal.storage.RangeArgument;
 
 
 /**
@@ -623,7 +624,7 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
     @Override
     public GridCoverage read(final GridGeometry domain, final int... range) 
throws DataStoreException {
         final long startTime = System.nanoTime();
-        final RangeArgument rangeIndices = 
validateRangeArgument(ranges.length, range);
+        final RangeArgument rangeIndices = 
RangeArgument.validate(ranges.length, range, listeners);
         final Variable first = data[bandDimension >= 0 ? 0 : 
rangeIndices.getFirstSpecified()];
         final DataType dataType = first.getDataType();
         if (bandDimension < 0) {
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 dca3626..9f8aa04 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
@@ -16,8 +16,6 @@
  */
 package org.apache.sis.internal.storage;
 
-import java.util.List;
-import java.util.Arrays;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.logging.Level;
@@ -25,12 +23,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.Metadata;
-import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.storage.DataStoreException;
@@ -42,16 +37,10 @@ import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.DisjointExtentException;
-import org.apache.sis.coverage.SampleDimension;
-import org.apache.sis.math.MathFunctions;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Longitude;
 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;
@@ -71,7 +60,7 @@ import org.apache.sis.internal.jdk9.JDK9;
  * of the {@link #read(GridGeometry, int...) read(…)} method in subclasses:
  *
  * <ul>
- *   <li>{@link #validateRangeArgument(int, int[])} for validation of the 
{@code range} argument.</li>
+ *   <li>{@link #canNotRead(String, GridGeometry, Throwable)} for reporting a 
failure to read operation.</li>
  *   <li>{@link #logReadOperation(Object, GridGeometry, long)} for logging a 
notice about a read operation.</li>
  * </ul>
  *
@@ -93,7 +82,7 @@ public abstract class AbstractGridResource extends 
AbstractResource implements G
 
     /**
      * Returns the grid geometry envelope if known.
-     * This implementation fetches the envelope from the grid geometry instead 
of from metadata.
+     * This implementation fetches the envelope from the grid geometry.
      * The envelope is absent if the grid geometry does not provide this 
information.
      *
      * @return the grid geometry envelope.
@@ -112,6 +101,7 @@ public abstract class AbstractGridResource extends 
AbstractResource implements G
      * Invoked in a synchronized block the first time that {@code 
getMetadata()} is invoked.
      * The default implementation populates metadata based on information 
provided by
      * {@link #getIdentifier()       getIdentifier()},
+     * {@link #getEnvelope()         getEnvelope()},
      * {@link #getGridGeometry()     getGridGeometry()} and
      * {@link #getSampleDimensions() getSampleDimensions()}.
      * Subclasses should override if they can provide more information.
@@ -127,352 +117,6 @@ public abstract class AbstractGridResource extends 
AbstractResource implements G
     }
 
     /**
-     * Validate the {@code range} argument given to {@link #read(GridGeometry, 
int...)}.
-     * This method verifies that all indices are between 0 and {@code 
numSampleDimensions}
-     * and that there is no duplicated index.
-     *
-     * @param  numSampleDimensions  number of sample dimensions in the 
resource.
-     *         Equal to <code>{@linkplain 
#getSampleDimensions()}.size()</code>.
-     * @param  range  the {@code range} argument given by the user. May be 
null or empty.
-     * @return the {@code range} argument encapsulated with a set of 
convenience tools.
-     * @throws IllegalArgumentException if a range index is invalid.
-     */
-    protected final RangeArgument validateRangeArgument(final int 
numSampleDimensions, final int[] range) {
-        ArgumentChecks.ensureStrictlyPositive("numSampleDimensions", 
numSampleDimensions);
-        final long[] packed;
-        if (range == null || range.length == 0) {
-            packed = new long[numSampleDimensions];
-            for (int i=1; i<numSampleDimensions; i++) {
-                packed[i] = (((long) i) << Integer.SIZE) | i;
-            }
-        } else {
-            /*
-             * Pattern: [specified `range` value | index in `range` where the 
value was specified]
-             */
-            packed = new long[range.length];
-            for (int i=0; i<range.length; i++) {
-                final int r = range[i];
-                if (r < 0 || r >= numSampleDimensions) {
-                    throw new 
IllegalArgumentException(Resources.forLocale(listeners.getLocale()).getString(
-                            Resources.Keys.InvalidSampleDimensionIndex_2, 
numSampleDimensions - 1, r));
-                }
-                packed[i] = (((long) r) << Integer.SIZE) | i;
-            }
-            /*
-             * Sort by increasing `range` value, but keep together with index 
in `range` where each
-             * value was specified. After sorting, it become easy to check for 
duplicated values.
-             */
-            Arrays.sort(packed);
-            int previous = -1;
-            for (int i=0; i<packed.length; i++) {
-                // Never negative because of check in previous loop.
-                final int r = (int) (packed[i] >>> Integer.SIZE);
-                if (r == previous) {
-                    throw new 
IllegalArgumentException(Resources.forLocale(listeners.getLocale()).getString(
-                            Resources.Keys.DuplicatedSampleDimensionIndex_1, 
r));
-                }
-                previous = r;
-            }
-        }
-        return new RangeArgument(packed, packed.length == numSampleDimensions);
-    }
-
-    /**
-     * The user-provided {@code range} argument, together with a set of 
convenience tools.
-     */
-    protected static final class RangeArgument {
-        /**
-         * 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
-         * of user-specified range index.
-         */
-        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.
-         *
-         * @see #insertBandDimension(GridExtent, int)
-         * @see #insertSubsampling(int[], int)
-         */
-        private int first, last, interval;
-
-        /**
-         * A builder for sample dimensions, created when first needed.
-         */
-        private SampleDimension.Builder builder;
-
-        /**
-         * Encapsulates the given {@code range} argument packed in high bits.
-         */
-        private RangeArgument(final long[] packed, final boolean hasAllBands) {
-            this.packed      = packed;
-            this.hasAllBands = hasAllBands;
-            this.interval    = 1;
-        }
-
-        /**
-         * 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 inserted.
-         */
-        public boolean isIdentity() {
-            if (!hasAllBands || interval != 1) {
-                return false;
-            }
-            for (int i=0; i<packed.length; i++) {
-                if (packed[i] != ((((long) i) << Integer.SIZE) | i)) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        /**
-         * 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 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.
-         *
-         * @return index of the first value in the user-specified {@code 
range} array.
-         */
-        public int getFirstSpecified() {
-            for (final long p : packed) {
-                if (((int) p) == 0) {
-                    return (int) (p >>> Integer.SIZE);
-                }
-            }
-            throw new IllegalStateException();              // Should never 
happen.
-        }
-
-        /**
-         * Returns the i<sup>th</sup> index of the band to read from the 
resource.
-         * Indices are returned in strictly increasing order.
-         *
-         * @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.
-         */
-        public int getSourceIndex(final int i) {
-            return (int) (packed[i] >>> Integer.SIZE);
-        }
-
-        /**
-         * Returns the i<sup>th</sup> band position. This is the index in the 
user-supplied {@code range} array
-         * where the {@code getSourceIndex(i)} value was specified.
-         *
-         * @param  i  index of the range index to get, from 0 inclusive to 
{@link #getNumBands()} exclusive.
-         * @return index in user-supplied {@code range} array where was 
specified the {@code getSourceIndex(i)} value.
-         */
-        public int getTargetIndex(final int i) {
-            return (int) packed[i];
-        }
-
-        /**
-         * Returns the i<sup>th</sup> index of the band to read from the 
resource, after subsampling has been applied.
-         * The subsampling results from calls to {@link 
#insertBandDimension(GridExtent, int)} and
-         * {@link #insertSubsampling(int[], int)} methods.
-         *
-         * {@preformat java
-         *     areaOfInterest = 
rangeIndices.insertBandDimension(areaOfInterest, bandDimension);
-         *     subsampling    = rangeIndices.insertSubsampling  (subsampling,  
  bandDimension);
-         *     data = myReadMethod(areaOfInterest, subsampling);
-         *     for (int i=0; i<numBands; i++) {
-         *         int bandIndexInTheDataWeJustRead = 
rangeIndices.getSubsampledIndex(i);
-         *     }
-         * }
-         *
-         * 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.
-         */
-        public int getSubsampledIndex(final int i) {
-            return (getSourceIndex(i) - first) / interval;
-        }
-
-        /**
-         * Returns the increment to apply on index for moving to the same band 
of the next pixel.
-         * 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.
-         *
-         * @see java.awt.image.PixelInterleavedSampleModel#getPixelStride()
-         */
-        public int getPixelStride() {
-            return (last - first) / interval + 1;
-        }
-
-        /**
-         * Returns the given extent with a new dimension added for the bands. 
The extent in the new dimension
-         * 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 subsampling} 
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.
-         */
-        public GridExtent insertBandDimension(final GridExtent areaOfInterest, 
final int bandDimension) {
-            first = getSourceIndex(0);
-            last  = getSourceIndex(packed.length - 1);
-            return areaOfInterest.insertDimension(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  subsampling    the subsampling to which to add a new 
dimension for bands.
-         * @param  bandDimension  index of the band dimension.
-         * @return a new subsampling array with the same values than the given 
array plus one dimension for bands.
-         */
-        public int[] insertSubsampling(int[] subsampling, final int 
bandDimension) {
-            final int[] delta = new int[packed.length - 1];
-            for (int i=0; i<delta.length; i++) {
-                delta[i] = getSourceIndex(i+1) - getSourceIndex(i);
-            }
-            final int[] divisors = MathFunctions.commonDivisors(delta);
-            interval = (divisors.length != 0) ? divisors[divisors.length - 1] 
: 1;
-            subsampling = ArraysExt.insert(subsampling, bandDimension, 1);
-            subsampling[bandDimension] = interval;
-            return subsampling;
-        }
-
-        /**
-         * Returns sample dimensions selected by the user. This is a 
convenience method for situations where
-         * sample dimensions are already in memory and there is no advantage 
to read them in "physical" order.
-         *
-         * @param  sourceBands  bands in the source coverage.
-         * @return bands selected by user, in user-specified order.
-         */
-        public SampleDimension[] select(final List<? extends SampleDimension> 
sourceBands) {
-            final SampleDimension[] bands = new SampleDimension[getNumBands()];
-            for (int i=0; i<bands.length; i++) {
-                bands[getTargetIndex(i)] = sourceBands.get(getSourceIndex(i));
-            }
-            return bands;
-        }
-
-        /**
-         * 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())
-                    .orElse(null);
-        }
-
-        /**
-         * 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.
-         *
-         * @return a recycled builder for sample dimensions.
-         */
-        public SampleDimension.Builder builder() {
-            if (builder == null) {
-                builder = new SampleDimension.Builder();
-            } else {
-                builder.clear();
-            }
-            return builder;
-        }
-    }
-
-    /**
      * Creates an exception for a failure to load data. If the failure may be 
caused by an envelope
      * outside the resource domain, that envelope will be inferred from the 
{@code request} argument.
      *
@@ -526,9 +170,9 @@ public abstract class AbstractGridResource extends 
AbstractResource implements G
      * The log level will be {@link Level#FINE} if the operation was quick 
enough,
      * or {@link PerformanceLevel#SLOW} or higher level otherwise.
      *
-     * @param  file        the file that was opened, or {@code null} for 
{@link #getSourceName()}.
-     * @param  domain      domain of the created grid coverage.
-     * @param  startTime   value of {@link System#nanoTime()} when the loading 
process started.
+     * @param  file       the file that was opened, or {@code null} for {@link 
#getSourceName()}.
+     * @param  domain     domain of the created grid coverage.
+     * @param  startTime  value of {@link System#nanoTime()} when the loading 
process started.
      */
     protected final void logReadOperation(final Object file, final 
GridGeometry domain, final long startTime) {
         final Logger logger = listeners.getLogger();
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
index 706680e..7b85d75 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryGridResource.java
@@ -94,7 +94,7 @@ public class MemoryGridResource extends AbstractGridResource {
     @Override
     public GridCoverage read(GridGeometry domain, final int... range) {
         List<SampleDimension> bands = coverage.getSampleDimensions();
-        final RangeArgument rangeIndices = validateRangeArgument(bands.size(), 
range);
+        final RangeArgument rangeIndices = 
RangeArgument.validate(bands.size(), range, listeners);
         /*
          * The given `domain` may use arbitrary `gridToCRS` and `CRS` 
properties.
          * For this simple implementation we need the same `gridToCRS` and 
`CRS`
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/RangeArgument.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/RangeArgument.java
new file mode 100644
index 0000000..4e84330
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/RangeArgument.java
@@ -0,0 +1,386 @@
+/*
+ * 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.storage;
+
+import java.util.List;
+import java.util.Arrays;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import java.awt.image.RasterFormatException;
+import org.opengis.metadata.spatial.DimensionNameType;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.internal.coverage.j2d.SampleModelFactory;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.math.MathFunctions;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Localized;
+
+
+/**
+ * The user-provided {@code range} argument together with a set of convenience 
tools.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   0.8
+ * @module
+ */
+public final class RangeArgument {
+    /**
+     * 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
+     * of user-specified range index.
+     */
+    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.
+     *
+     * @see #insertBandDimension(GridExtent, int)
+     * @see #insertSubsampling(int[], int)
+     */
+    private int first, last, interval;
+
+    /**
+     * A builder for sample dimensions, created when first needed.
+     */
+    private SampleDimension.Builder builder;
+
+    /**
+     * Encapsulates the given {@code range} argument packed in high bits.
+     */
+    private RangeArgument(final long[] packed, final boolean hasAllBands) {
+        this.packed      = packed;
+        this.hasAllBands = hasAllBands;
+        this.interval    = 1;
+    }
+
+    /**
+     * Validate the {@code range} argument given to {@link 
GridCoverageResource#read(GridGeometry, int...)}.
+     * This method verifies that all indices are between 0 and {@code 
numSampleDimensions} and that there is
+     * no duplicated index.
+     *
+     * @param  numSampleDimensions  number of sample dimensions in the 
resource.
+     *         Equal to <code>{@linkplain 
GridCoverageResource#getSampleDimensions()}.size()</code>.
+     * @param  range  the {@code range} argument given by the user. May be 
null or empty.
+     * @param  listeners  source of locale to use if an exception must be 
thrown.
+     * @return the {@code range} argument encapsulated with a set of 
convenience tools.
+     * @throws IllegalArgumentException if a range index is invalid.
+     */
+    public static RangeArgument validate(final int numSampleDimensions, final 
int[] range, final Localized listeners) {
+        ArgumentChecks.ensureStrictlyPositive("numSampleDimensions", 
numSampleDimensions);
+        final long[] packed;
+        if (range == null || range.length == 0) {
+            packed = new long[numSampleDimensions];
+            for (int i=1; i<numSampleDimensions; i++) {
+                packed[i] = (((long) i) << Integer.SIZE) | i;
+            }
+        } else {
+            /*
+             * Pattern: [specified `range` value | index in `range` where the 
value was specified]
+             */
+            packed = new long[range.length];
+            for (int i=0; i<range.length; i++) {
+                final int r = range[i];
+                if (r < 0 || r >= numSampleDimensions) {
+                    throw new 
IllegalArgumentException(Resources.forLocale(listeners.getLocale()).getString(
+                            Resources.Keys.InvalidSampleDimensionIndex_2, 
numSampleDimensions - 1, r));
+                }
+                packed[i] = (((long) r) << Integer.SIZE) | i;
+            }
+            /*
+             * Sort by increasing `range` value, but keep together with index 
in `range` where each
+             * value was specified. After sorting, it become easy to check for 
duplicated values.
+             */
+            Arrays.sort(packed);
+            int previous = -1;
+            for (int i=0; i<packed.length; i++) {
+                // Never negative because of check in previous loop.
+                final int r = (int) (packed[i] >>> Integer.SIZE);
+                if (r == previous) {
+                    throw new 
IllegalArgumentException(Resources.forLocale(listeners.getLocale()).getString(
+                            Resources.Keys.DuplicatedSampleDimensionIndex_1, 
r));
+                }
+                previous = r;
+            }
+        }
+        return new RangeArgument(packed, packed.length == numSampleDimensions);
+    }
+
+    /**
+     * 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 inserted.
+     */
+    public boolean isIdentity() {
+        if (!hasAllBands || interval != 1) {
+            return false;
+        }
+        for (int i=0; i<packed.length; i++) {
+            if (packed[i] != ((((long) i) << Integer.SIZE) | i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 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 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.
+     *
+     * @return index of the first value in the user-specified {@code range} 
array.
+     */
+    public int getFirstSpecified() {
+        for (final long p : packed) {
+            if (((int) p) == 0) {
+                return (int) (p >>> Integer.SIZE);
+            }
+        }
+        throw new IllegalStateException();              // Should never happen.
+    }
+
+    /**
+     * Returns the i<sup>th</sup> index of the band to read from the resource.
+     * Indices are returned in strictly increasing order.
+     *
+     * @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.
+     */
+    public int getSourceIndex(final int i) {
+        return (int) (packed[i] >>> Integer.SIZE);
+    }
+
+    /**
+     * Returns the i<sup>th</sup> band position. This is the index in the 
user-supplied {@code range} array
+     * where the {@code getSourceIndex(i)} value was specified.
+     *
+     * @param  i  index of the range index to get, from 0 inclusive to {@link 
#getNumBands()} exclusive.
+     * @return index in user-supplied {@code range} array where was specified 
the {@code getSourceIndex(i)} value.
+     */
+    public int getTargetIndex(final int i) {
+        return (int) packed[i];
+    }
+
+    /**
+     * Returns the i<sup>th</sup> index of the band to read from the resource, 
after subsampling has been applied.
+     * The subsampling results from calls to {@link 
#insertBandDimension(GridExtent, int)} and
+     * {@link #insertSubsampling(int[], int)} methods.
+     *
+     * {@preformat java
+     *     areaOfInterest = rangeIndices.insertBandDimension(areaOfInterest, 
bandDimension);
+     *     subsampling    = rangeIndices.insertSubsampling  (subsampling,    
bandDimension);
+     *     data = myReadMethod(areaOfInterest, subsampling);
+     *     for (int i=0; i<numBands; i++) {
+     *         int bandIndexInTheDataWeJustRead = 
rangeIndices.getSubsampledIndex(i);
+     *     }
+     * }
+     *
+     * 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.
+     */
+    public int getSubsampledIndex(final int i) {
+        return (getSourceIndex(i) - first) / interval;
+    }
+
+    /**
+     * Returns the increment to apply on index for moving to the same band of 
the next pixel.
+     * 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.
+     *
+     * @see java.awt.image.PixelInterleavedSampleModel#getPixelStride()
+     */
+    public int getPixelStride() {
+        return (last - first) / interval + 1;
+    }
+
+    /**
+     * Returns the given extent with a new dimension added for the bands. The 
extent in the new dimension
+     * 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 subsampling} 
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.
+     */
+    public GridExtent insertBandDimension(final GridExtent areaOfInterest, 
final int bandDimension) {
+        first = getSourceIndex(0);
+        last  = getSourceIndex(packed.length - 1);
+        return areaOfInterest.insertDimension(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  subsampling    the subsampling to which to add a new dimension 
for bands.
+     * @param  bandDimension  index of the band dimension.
+     * @return a new subsampling array with the same values than the given 
array plus one dimension for bands.
+     */
+    public int[] insertSubsampling(int[] subsampling, final int bandDimension) 
{
+        final int[] delta = new int[packed.length - 1];
+        for (int i=0; i<delta.length; i++) {
+            delta[i] = getSourceIndex(i+1) - getSourceIndex(i);
+        }
+        final int[] divisors = MathFunctions.commonDivisors(delta);
+        interval = (divisors.length != 0) ? divisors[divisors.length - 1] : 1;
+        subsampling = ArraysExt.insert(subsampling, bandDimension, 1);
+        subsampling[bandDimension] = interval;
+        return subsampling;
+    }
+
+    /**
+     * Returns sample dimensions selected by the user. This is a convenience 
method for situations where
+     * sample dimensions are already in memory and there is no advantage to 
read them in "physical" order.
+     *
+     * @param  sourceBands  bands in the source coverage.
+     * @return bands selected by user, in user-specified order.
+     */
+    public SampleDimension[] select(final List<? extends SampleDimension> 
sourceBands) {
+        final SampleDimension[] bands = new SampleDimension[getNumBands()];
+        for (int i=0; i<bands.length; i++) {
+            bands[getTargetIndex(i)] = sourceBands.get(getSourceIndex(i));
+        }
+        return bands;
+    }
+
+    /**
+     * 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())
+                .orElse(null);
+    }
+
+    /**
+     * 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.
+     *
+     * @return a recycled builder for sample dimensions.
+     */
+    public SampleDimension.Builder builder() {
+        if (builder == null) {
+            builder = new SampleDimension.Builder();
+        } else {
+            builder.clear();
+        }
+        return builder;
+    }
+}
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 9f8b5cd..0baf6b3 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
@@ -328,7 +328,7 @@ public abstract class TiledGridResource extends 
AbstractGridResource {
          */
         public Subset(GridGeometry domain, final int[] range) throws 
DataStoreException {
             List<SampleDimension> bands        = getSampleDimensions();
-            final RangeArgument   rangeIndices = 
validateRangeArgument(bands.size(), range);
+            final RangeArgument   rangeIndices = 
RangeArgument.validate(bands.size(), range, listeners);
             final GridGeometry    gridGeometry = getGridGeometry();
             sourceExtent = gridGeometry.getExtent();
             tileSize = getTileSize();
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MemoryGridResourceTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MemoryGridResourceTest.java
index 2a6ab36..d8c7ff0 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MemoryGridResourceTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/MemoryGridResourceTest.java
@@ -40,7 +40,7 @@ import static org.apache.sis.test.Assert.*;
  * @since   1.1
  * @module
  */
-@DependsOn(AbstractGridResourceTest.class)
+@DependsOn(RangeArgumentTest.class)
 public final strictfp class MemoryGridResourceTest extends TestCase {
     /**
      * Arbitrary size for the grid to test.
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java
similarity index 88%
rename from 
storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java
rename to 
storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java
index 11b85cd..5cc7437 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/RangeArgumentTest.java
@@ -17,25 +17,27 @@
 package org.apache.sis.internal.storage;
 
 import java.util.List;
+import java.util.Locale;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.test.TestCase;
+import org.apache.sis.util.Localized;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
 
 
 /**
- * Tests {@link AbstractGridResource} and {@link 
AbstractGridResource.RangeArgument}.
+ * Tests {@link RangeArgument}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
  * @since   1.0
  * @module
  */
-public final strictfp class AbstractGridResourceTest  extends TestCase {
+public final strictfp class RangeArgumentTest extends TestCase implements 
Localized {
     /**
      * A resource performing no operation.
      */
@@ -46,12 +48,22 @@ public final strictfp class AbstractGridResourceTest  
extends TestCase {
     };
 
     /**
+     * Returns a fixed locale for testing purpose.
+     *
+     * @return a fixed locale.
+     */
+    @Override
+    public Locale getLocale() {
+        return Locale.US;
+    }
+
+    /**
      * Tests {@link AbstractGridResource.RangeArgument} for data organized in 
a banded sample model.
      * This is the state when no {@code insert} method is invoked.
      */
     @Test
     public void testRangeArgumentForBandedModel() {
-        final AbstractGridResource.RangeArgument r = 
resource.validateRangeArgument(7, new int[] {4, 6, 2});
+        final RangeArgument r = RangeArgument.validate(7, new int[] {4, 6, 2}, 
this);
         assertEquals("numBands",    3, r.getNumBands());
         assertEquals("first",       4, r.getFirstSpecified());
         assertEquals("source",      2, r.getSourceIndex(0));           // 
Expect sorted source indices: {2, 4, 6}.
@@ -72,7 +84,7 @@ public final strictfp class AbstractGridResourceTest  extends 
TestCase {
      */
     @Test
     public void testRangeArgumentForInterleavedModel() {
-        final AbstractGridResource.RangeArgument r = 
resource.validateRangeArgument(7, new int[] {4, 6, 2});
+        final RangeArgument r = RangeArgument.validate(7, new int[] {4, 6, 2}, 
this);
         assertEquals(3, r.insertBandDimension(new GridExtent(360, 180), 
2).getDimension());
         assertArrayEquals(new int[] {3, 1, 2}, r.insertSubsampling(new int[] 
{3, 1}, 2));
         assertEquals("numBands",    3, r.getNumBands());
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
index a638d5d..1703a23 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
@@ -41,7 +41,7 @@ import org.junit.BeforeClass;
     org.apache.sis.internal.storage.io.HyperRectangleReaderTest.class,
     org.apache.sis.internal.storage.io.RewindableLineReaderTest.class,
     org.apache.sis.internal.storage.MetadataBuilderTest.class,
-    org.apache.sis.internal.storage.AbstractGridResourceTest.class,
+    org.apache.sis.internal.storage.RangeArgumentTest.class,
     org.apache.sis.internal.storage.MemoryGridResourceTest.class,
     org.apache.sis.storage.FeatureNamingTest.class,
     org.apache.sis.storage.ProbeResultTest.class,

Reply via email to