sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: First draft of a netCDF reader implementation taking in account the sub-sampling and sub-set of bands specified in the read(GridGeometry domain, int... range) arguments.
Date Thu, 27 Dec 2018 19:41:24 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new b3b819d  First draft of a netCDF reader implementation taking in account the sub-sampling and sub-set of bands specified in the read(GridGeometry domain, int... range) arguments.
b3b819d is described below

commit b3b819db6d79a494b9eddcdeb30c09d4b216a99e
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Dec 27 20:40:17 2018 +0100

    First draft of a netCDF reader implementation taking in account the sub-sampling and sub-set of bands specified in the read(GridGeometry domain, int... range) arguments.
---
 .../sis/internal/metadata/VerticalDatumTypes.java  |   2 -
 .../org/apache/sis/coverage/grid/GridChange.java   | 107 ++++++++++++++++-----
 .../org/apache/sis/coverage/grid/GridExtent.java   |  45 ++++++++-
 .../org/apache/sis/coverage/grid/GridGeometry.java |  95 ++++++++++--------
 .../referencing/provider/GeographicOffsets.java    |  12 +--
 .../referencing/provider/VerticalOffset.java       |   7 +-
 .../transform/AbstractLinearTransform.java         |   2 +-
 .../operation/transform/MathTransformProvider.java |   2 +-
 .../operation/transform/MathTransforms.java        |  23 ++++-
 .../operation/transform/ScaleTransform.java        |  16 +--
 pom.xml                                            |   2 +-
 .../apache/sis/storage/netcdf/GridResource.java    | 101 ++++++++++++-------
 .../sis/internal/storage/AbstractGridResource.java |  27 ++++++
 .../org/apache/sis/internal/storage/Resources.java |   5 +
 .../sis/internal/storage/Resources.properties      |   1 +
 .../sis/internal/storage/Resources_fr.properties   |   1 +
 .../apache/sis/storage/GridCoverageResource.java   |   2 +-
 17 files changed, 323 insertions(+), 127 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/VerticalDatumTypes.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/VerticalDatumTypes.java
index 7b720a7..852c75d 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/VerticalDatumTypes.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/VerticalDatumTypes.java
@@ -48,8 +48,6 @@ public final class VerticalDatumTypes implements CodeList.Filter {
      * normal to the ellipsoid used in the definition of horizontal datum.
      *
      * <p>Identifier: {@code CS_DatumType.CS_VD_Ellipsoidal}</p>
-     *
-     * @see <a href="http://jira.codehaus.org/browse/GEO-133">GEO-133</a>
      */
     public static final VerticalDatumType ELLIPSOIDAL = VerticalDatumType.valueOf("ELLIPSOIDAL");
 
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java
index 1046fe9..4b42eb4 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java
@@ -27,6 +27,7 @@ import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
@@ -50,7 +51,7 @@ import org.apache.sis.util.Classes;
  *         &#64;Override
  *         public GridCoverage read(GridGeometry domain, int... range) throws DataStoreException {
  *             GridChange change = new GridChange(domain, getGridGeometry());
- *             GridExtent toRead = change.getTargetRange();
+ *             GridExtent toRead = change.getTargetExtent();
  *             int[] subsampling = change.getTargetStrides());
  *             // Do reading here.
  *         }
@@ -79,9 +80,17 @@ public class GridChange implements Serializable {
      * This is the expected ranges after conversions from source grid coordinates to target
      * grid coordinates, clipped to target extent and ignoring {@linkplain #scales}.
      *
-     * @see #getTargetRange()
+     * @see #getTargetExtent()
      */
-    private final GridExtent targetRange;
+    private final GridExtent targetExtent;
+
+    /**
+     * The target grid geometry specified to the constructor. The extent of that geometry
+     * is not necessarily the {@link #targetExtent}.
+     *
+     * @see #getTargetGeometry(int...)
+     */
+    private final GridGeometry givenGeometry;
 
     /**
      * An estimation of the multiplication factors when converting cell coordinates from source
@@ -155,10 +164,11 @@ public class GridChange implements Serializable {
         ArgumentChecks.ensureNonNull("source",   source);
         ArgumentChecks.ensureNonNull("target",   target);
         ArgumentChecks.ensureNonNull("rounding", rounding);
-        if (source.equals(target)) {
+        givenGeometry = target;
+        if (target.equals(source)) {
             // Optimization for a common case.
-            mapCorners = mapCenters = MathTransforms.identity(source.getDimension());
-            targetRange = source.getExtent();
+            mapCorners = mapCenters = MathTransforms.identity(target.getDimension());
+            targetExtent = target.getExtent();                                        // May throw IncompleteGridGeometryException.
         } else {
             final CoordinateOperation crsChange;
             try {
@@ -166,22 +176,17 @@ public class GridChange implements Serializable {
             } catch (FactoryException e) {
                 throw new TransformException(Errors.format(Errors.Keys.CanNotTransformEnvelope), e);
             }
-            final GridExtent domain = source.getExtent();
+            final GridExtent domain = source.getExtent();                             // May throw IncompleteGridGeometryException.
             mapCorners  = path(source, crsChange, target, PixelInCell.CELL_CORNER);
             mapCenters  = path(source, crsChange, target, PixelInCell.CELL_CENTER);
-            targetRange = new GridExtent(domain.toCRS(mapCorners, mapCenters), rounding, margin, target.extent, null);
+            final GridExtent extent = new GridExtent(domain.toCRS(mapCorners, mapCenters), rounding, margin, target.extent, null);
+            targetExtent = extent.equals(target.extent) ? target.extent : extent.equals(domain) ? domain : extent;
             /*
              * Get an estimation of the scale factors when converting from source to target.
              * If all scale factors are 1, we will not store the array for consistency with
              * above block for identity case.
              */
-            final Matrix matrix = MathTransforms.getMatrix(mapCenters);
-            final double[] resolution;
-            if (matrix != null) {
-                resolution = GridGeometry.resolution(matrix, 1);
-            } else {
-                resolution = GridGeometry.resolution(mapCenters, domain);
-            }
+            final double[] resolution = GridGeometry.resolution(mapCenters, domain);
             for (int i=resolution.length; --i >= 0;) {
                 if (resolution[i] != 1) {
                     scales = resolution;
@@ -266,12 +271,12 @@ public class GridChange implements Serializable {
      *
      * @return intersection of grid geometry extents in units of target cells.
      */
-    public GridExtent getTargetRange() {
-        return targetRange;
+    public GridExtent getTargetExtent() {
+        return targetExtent;
     }
 
     /**
-     * Returns an <em>estimation</em> of the steps for accessing cells along each axis of target range.
+     * Returns an <em>estimation</em> of the steps for accessing cells along each axis of target grid.
      * Given a {@linkplain #getConversion(PixelInCell) conversion} from source grid coordinates
      * (<var>x</var>, <var>y</var>, <var>z</var>) to target grid coordinates
      * (<var>x′</var>, <var>y′</var>, <var>z′</var>) defined as below (generalize to as many dimensions as needed):
@@ -288,7 +293,7 @@ public class GridChange implements Serializable {
      * corresponds approximately to an iteration in target grid coordinates with a step of Δ<var>x′</var>=s₀,
      * a step Δ<var>y</var>=1 corresponds approximately to a step Δ<var>y′</var>=s₁, <i>etc.</i>
      * If the conversion changes grid axis order, then the order of elements in the returned array
-     * is the order of axes in the {@linkplain #getTargetRange() target range}.
+     * is the order of axes in the {@linkplain #getTargetExtent() target range}.
      *
      * <p>In a <em>inverse</em> conversion from target to source grid, the value returned by this
      * method would be the sub-sampling to apply while reading the target grid.</p>
@@ -298,7 +303,7 @@ public class GridChange implements Serializable {
     public int[] getTargetStrides() {
         final int[] subsamplings;
         if (scales == null) {
-            subsamplings = new int[targetRange.getDimension()];
+            subsamplings = new int[targetExtent.getDimension()];
             Arrays.fill(subsamplings, 1);
         } else {
             subsamplings = new int[scales.length];
@@ -310,6 +315,62 @@ public class GridChange implements Serializable {
     }
 
     /**
+     * Returns the grid geometry resulting from sub-sampling the target grid with the given strides.
+     * The {@code strides} argument is usually the array returned by {@link #getTargetStrides()}, but not necessarily.
+     * The {@linkplain GridGeometry#getExtent() extent} of the returned grid geometry will be derived from
+     * {@link #getTargetExtent()} as below for each dimension <var>i</var>:
+     *
+     * <ul>
+     *   <li>The {@linkplain GridExtent#getLow(int) low} coordinate is unchanged.</li>
+     *   <li>The {@linkplain GridExtent#getSize(int) size} is divided by {@code strides[i]}.</li>
+     * </ul>
+     *
+     * The {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform is scaled accordingly
+     * in order to map approximately to the same {@linkplain GridGeometry#getEnvelope() envelope}.
+     *
+     * @param  strides  the sub-sampling to apply on each grid dimension. All values shall be greater than zero.
+     *         If the array length is shorter than the number of dimensions, missing values are assumed to be 1.
+     * @return a grid geometry derived from the target geometry with the given sub-sampling.
+     * @throws TransformException if an error occurred during computation of target grid geometry.
+     */
+    public GridGeometry getTargetGeometry(final int... strides) throws TransformException {
+        GridExtent extent = getTargetExtent();
+        double[] factors = null;
+        Matrix toGiven = null;
+        if (strides != null) {
+            final int dimension = extent.getDimension();
+            for (int i = Math.min(dimension, strides.length); --i >= 0;) {
+                final int s = strides[i];
+                if (s != 1) {
+                    if (factors == null) {
+                        extent = new GridExtent(extent, strides);
+                        factors = new double[dimension];
+                        Arrays.fill(factors, 1);
+                        if (!extent.startsWithZero()) {
+                            toGiven = Matrices.createIdentity(dimension + 1);
+                        }
+                    }
+                    factors[i] = s;
+                    if (toGiven != null) {
+                        toGiven.setElement(i, i, s);
+                        toGiven.setElement(i, dimension, (1-s) * extent.getLow(i));
+                    }
+                }
+            }
+        }
+        final MathTransform mt;
+        if (factors == null) {
+            if (extent.equals(givenGeometry.extent)) {
+                return givenGeometry;
+            }
+            mt = null;
+        } else {
+            mt = (toGiven != null) ? MathTransforms.linear(toGiven) : MathTransforms.scale(factors);
+        }
+        return new GridGeometry(givenGeometry, extent, mt);
+    }
+
+    /**
      * Returns a hash code value for this grid change.
      *
      * @return hash code value.
@@ -320,7 +381,7 @@ public class GridChange implements Serializable {
          * The mapCorners is closely related to mapCenters, so we omit it.
          * The scales array is derived from mapCenters, so we omit it too.
          */
-        return mapCorners.hashCode() + 59 * targetRange.hashCode();
+        return mapCorners.hashCode() + 59 * targetExtent.hashCode();
     }
 
     /**
@@ -338,7 +399,7 @@ public class GridChange implements Serializable {
             final GridChange that = (GridChange) other;
             return mapCorners.equals(that.mapCorners) &&
                    mapCenters.equals(that.mapCenters) &&
-                   targetRange.equals(that.targetRange) &&
+                   targetExtent.equals(that.targetExtent) &&
                    Arrays.equals(scales, that.scales);
         }
         return false;
@@ -357,7 +418,7 @@ public class GridChange implements Serializable {
                 .append(Classes.getShortClassName(this)).append(lineSeparator)
                 .append("└ Scale factor ≈ ").append((float) getGlobalScale()).append(lineSeparator)
                 .append("Target range").append(lineSeparator);
-        targetRange.appendTo(buffer, Vocabulary.getResources((Locale) null));
+        targetExtent.appendTo(buffer, Vocabulary.getResources((Locale) null));
         return buffer.toString();
     }
 }
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index e565d17..f9f823c 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -122,7 +122,7 @@ public class GridExtent implements Serializable {
     /**
      * Minimum and maximum grid coordinates. The first half contains minimum coordinates (inclusive),
      * while the last half contains maximum coordinates (<strong>inclusive</strong>). Note that the
-     * later is the opposite of Java2D usage but conforms to ISO specification.
+     * later inclusiveness is the opposite of Java2D usage but conforms to ISO specification.
      */
     private final long[] coordinates;
 
@@ -211,6 +211,34 @@ public class GridExtent implements Serializable {
     }
 
     /**
+     * Creates a new grid extent with the same lower values than the given extent, and high values adjusted by dividing
+     * the {@linkplain #getSize(int) size} by the given strides.  The policy of keeping the lower coordinates unchanged
+     * is consistent with {@link GridChange#getTargetGeometry(int...)} policy of keeping lower coordinates invariant
+     * under the scales.
+     *
+     * @param  extent   the extent from which to compute a new extent.
+     * @param  strides  the strides. Length shall be equal to the number of dimension and all values shall be greater than zero.
+     * @throws IllegalArgumentException if a stride is not greater than zero.
+     */
+    GridExtent(final GridExtent extent, final int[] strides) {
+        types = extent.types;
+        coordinates = extent.coordinates.clone();
+        final int m = getDimension();
+        for (int i=0; i<m; i++) {
+            final int s = strides[i];
+            if (s <= 0) {
+                throw new IllegalArgumentException(Errors.format(Errors.Keys.ValueNotGreaterThanZero_2, "strides[" + i + ']', s));
+            }
+            final int j = i + m;
+            final long size = coordinates[j] - coordinates[i] + 1;                             // Result is an unsigned number.
+            if (size == 0) throw new ArithmeticException("long overflow");
+            long r = Long.divideUnsigned(size, s);
+            if (r*s == size) r--;                           // Make inclusive if the division did not already rounded toward 0.
+            coordinates[j] = coordinates[i] + r;
+        }
+    }
+
+    /**
      * Creates a new grid extent for an image or matrix of the given size.
      * The {@linkplain #getLow() low} grid coordinates are zeros and the axis types are
      * {@link DimensionNameType#COLUMN} and {@link DimensionNameType#ROW ROW} in that order.
@@ -461,6 +489,21 @@ public class GridExtent implements Serializable {
     }
 
     /**
+     * Returns {@code true} if all low coordinates are zero.
+     * This is a very common case since many grids start their cell numbering at zero.
+     *
+     * @return whether all low coordinates are zero.
+     */
+    public boolean startsWithZero() {
+        for (int i = getDimension(); --i >= 0;) {
+            if (coordinates[i] != 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
      * Returns the valid minimum grid coordinates, inclusive.
      * The sequence contains a minimum value for each dimension of the grid coverage.
      *
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index cccd6dc..a6adb7f 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -213,28 +213,43 @@ public class GridGeometry implements Serializable {
     }
 
     /**
-     * Creates a new grid geometry with the same values than the given grid geometry except the grid extent.
-     * This constructor can be used for creating a grid geometry over a subregion, for example with the grid
-     * extent computed by {@link #getExtent(Envelope)}.
+     * Creates a new grid geometry derived from the given grid geometry with a new extent and a modified transform.
+     * This constructor can be used for creating a grid geometry over a subregion, for example with the grid extent
+     * computed by {@link #getExtent(Envelope)}.
      *
-     * @param other   the other grid geometry to copy.
-     * @param extent  the new extent for the grid geometry to construct, or {@code null} if none.
+     * <p>If {@code toOther} is non-null, it should be a transform from the given {@code extent} coordinates to the
+     * {@code other} grid coordinates. The {@link #gridToCRS} transform of the new grid geometry will be set to the
+     * following concatenation:</p>
+     *
+     * <blockquote>{@code this.gridToCRS} = {@code toOther} → {@code other.gridToCRS}</blockquote>
+     *
+     * @param  other    the other grid geometry to copy.
+     * @param  extent   the new extent for the grid geometry to construct, or {@code null} if none.
+     * @param  toOther  transform from this grid coordinates to {@code other} grid coordinates, or {@code null} if none.
      * @throws NullPointerException if {@code extent} is {@code null} and the other grid geometry contains no other information.
      * @throws TransformException if the math transform can not compute the geospatial envelope from the grid extent.
      *
      * @see #getExtent(Envelope)
      */
-    public GridGeometry(final GridGeometry other, final GridExtent extent) throws TransformException {
+    public GridGeometry(final GridGeometry other, final GridExtent extent, final MathTransform toOther) throws TransformException {
         ArgumentChecks.ensureNonNull("other", other);
+        final int dimension = other.getDimension();
+        this.extent = extent;
         if (extent != null) {
-            ensureDimensionMatches("extent", other.getDimension(), extent.getDimension());
+            ensureDimensionMatches("extent", dimension, extent.getDimension());
         }
-        this.extent = extent;
-        gridToCRS   = other.gridToCRS;
-        cornerToCRS = other.cornerToCRS;
-        resolution  = other.resolution;
-        nonLinears  = other.nonLinears;
-        envelope    = computeEnvelope(other.gridToCRS, other.envelope != null ? other.envelope.getCoordinateReferenceSystem() : null);
+        if (toOther == null || toOther.isIdentity()) {
+            gridToCRS   = other.gridToCRS;
+            cornerToCRS = other.cornerToCRS;
+            resolution  = other.resolution;
+            nonLinears  = other.nonLinears;
+        } else {
+            gridToCRS   = MathTransforms.concatenate(toOther, other.gridToCRS);
+            cornerToCRS = MathTransforms.concatenate(toOther, other.cornerToCRS);
+            resolution  = resolution(gridToCRS, extent);
+            nonLinears  = findNonLinearTargets(gridToCRS);
+        }
+        envelope = computeEnvelope(gridToCRS, other.envelope != null ? other.envelope.getCoordinateReferenceSystem() : null);
         if (envelope == null && gridToCRS == null) {
             ArgumentChecks.ensureNonNull("extent", extent);
         }
@@ -298,26 +313,8 @@ public class GridGeometry implements Serializable {
         this.gridToCRS   = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CENTER);
         this.cornerToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CORNER);
         this.envelope    = computeEnvelope(gridToCRS, crs);     // 'gridToCRS' specified by the user, not 'this.gridToCRS'.
-        /*
-         * If the gridToCRS transform is linear, we do not even need to check the grid extent;
-         * it can be null. Otherwise (if the transform is non-linear) the extent is mandatory.
-         * The easiest way to estimate a resolution is then to ask for the derivative at some
-         * arbitrary point. For this constructor, we take the grid center.
-         *
-         * Note that for this computation, it does not matter if 'gridToCRS' is the user-specified
-         * transform or the 'this.gridToCRS' field value; both should produce equivalent results.
-         */
-        double[] resolution = null;
-        final Matrix matrix = MathTransforms.getMatrix(gridToCRS);
-        if (matrix != null) {
-            resolution = resolution(matrix, 1);
-        } else if (extent != null && gridToCRS != null) try {
-            resolution = resolution(gridToCRS, extent);
-        } catch (TransformException e) {
-            recoverableException(e);
-        }
-        this.resolution = resolution;
-        nonLinears = findNonLinearTargets(gridToCRS);
+        this.resolution  = resolution(gridToCRS, extent);       // 'gridToCRS' or 'cornerToCRS' does not matter here.
+        this.nonLinears  = findNonLinearTargets(gridToCRS);
     }
 
     /**
@@ -571,7 +568,7 @@ public class GridGeometry implements Serializable {
      * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
      * @throws TransformException if an error occurred while converting the envelope coordinates to grid coordinates.
      *
-     * @see #GridGeometry(GridGeometry, GridExtent)
+     * @see #GridGeometry(GridGeometry, GridExtent, MathTransform)
      */
     public GridExtent getExtent(final Envelope areaOfInterest) throws IncompleteGridGeometryException, TransformException {
         ArgumentChecks.ensureNonNull("areaOfInterest", areaOfInterest);
@@ -708,13 +705,34 @@ public class GridGeometry implements Serializable {
     }
 
     /**
+     * Computes the resolution for the given grid extent and transform, or returns {@code null} if unknown.
+     * If the {@code gridToCRS} transform is linear, we do not even need to check the grid extent; it can be null.
+     * Otherwise (if the transform is non-linear) the extent is necessary. The easiest way to estimate a resolution
+     * is then to ask for the derivative at some arbitrary point (the point of interest).
+     *
+     * <p>Note that for this computation, it does not matter if {@code gridToCRS} is the user-specified
+     * transform or the {@code this.gridToCRS} field value; both should produce equivalent results.</p>
+     */
+    static double[] resolution(final MathTransform gridToCRS, final GridExtent domain) throws TransformException {
+        final Matrix matrix = MathTransforms.getMatrix(gridToCRS);
+        if (matrix != null) {
+            return resolution(matrix, 1);
+        } else if (domain != null && gridToCRS != null) try {
+            return resolution(gridToCRS.derivative(new DirectPositionView.Double(domain.getPointOfInterest())), 0);
+        } catch (TransformException e) {
+            recoverableException(e);
+        }
+        return null;
+    }
+
+    /**
      * Computes the resolutions from the given matrix. This is the magnitude of each row vector.
      *
      * @param  numToIgnore  number of rows and columns to ignore at the end of the matrix.
      *         This is 0 if the matrix is a derivative (i.e. we ignore nothing), or 1 if the matrix
      *         is an affine transform (i.e. we ignore the translation column and the [0 0 … 1] row).
      */
-    static double[] resolution(final Matrix gridToCRS, final int numToIgnore) {
+    private static double[] resolution(final Matrix gridToCRS, final int numToIgnore) {
         final double[] resolution = new double[gridToCRS.getNumRow() - numToIgnore];
         final double[] buffer     = new double[gridToCRS.getNumCol() - numToIgnore];
         for (int j=0; j<resolution.length; j++) {
@@ -727,13 +745,6 @@ public class GridGeometry implements Serializable {
     }
 
     /**
-     * Returns an estimation of the resolution at the point of interest of the given extent.
-     */
-    static double[] resolution(final MathTransform gridToCRS, final GridExtent domain) throws TransformException {
-        return resolution(gridToCRS.derivative(new DirectPositionView.Double(domain.getPointOfInterest())), 0);
-    }
-
-    /**
      * Indicates whether the <cite>grid to CRS</cite> conversion is linear for all the specified CRS axes.
      * The conversion from grid coordinates to real world coordinates is often linear for some dimensions,
      * typically the horizontal ones at indices 0 and 1. But the vertical dimension (usually at index 2)
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicOffsets.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicOffsets.java
index c428915..b6e27e0 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicOffsets.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicOffsets.java
@@ -25,7 +25,6 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
-import org.apache.sis.referencing.operation.matrix.Matrix4;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.measure.Units;
 
@@ -36,7 +35,7 @@ import org.apache.sis.measure.Units;
  * but subclasses will provide different operations.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.7
  * @module
  */
@@ -115,10 +114,9 @@ public class GeographicOffsets extends GeodeticOperation {
             throws ParameterNotFoundException
     {
         final Parameters pv = Parameters.castOrWrap(values);
-        final Matrix4 t = new Matrix4();
-        t.m03 = pv.doubleValue(TX);
-        t.m13 = pv.doubleValue(TY);
-        t.m23 = pv.doubleValue(vertical());
-        return MathTransforms.linear(t);
+        return MathTransforms.translation(pv.doubleValue(TX),
+                                          pv.doubleValue(TY),
+                                          pv.doubleValue(vertical()));
+
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/VerticalOffset.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/VerticalOffset.java
index 1ba2833..455f1ea 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/VerticalOffset.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/VerticalOffset.java
@@ -26,7 +26,6 @@ import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.parameter.Parameters;
-import org.apache.sis.referencing.operation.matrix.Matrix2;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 
 
@@ -47,7 +46,7 @@ import org.apache.sis.referencing.operation.transform.MathTransforms;
  * <cite>"Vertical Offset"</cite> parameter value needs to be reversed if the target coordinate system axis is down.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.0
  * @since   0.7
  * @module
  */
@@ -87,9 +86,7 @@ public final class VerticalOffset extends GeographicOffsets {
             throws ParameterNotFoundException
     {
         final Parameters pv = Parameters.castOrWrap(values);
-        final Matrix2 t = new Matrix2();
-        t.m01 = pv.doubleValue(TZ);
-        return MathTransforms.linear(t);
+        return MathTransforms.translation(pv.doubleValue(TZ));
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
index 571ee02..d3593d7 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
@@ -116,7 +116,7 @@ abstract class AbstractLinearTransform extends AbstractMathTransform implements
      * If this method is unsure, it conservatively returns {@code null}.
      */
     @Override
-    protected MathTransform tryConcatenate(boolean applyOtherFirst, MathTransform other, MathTransformFactory factory)
+    protected final MathTransform tryConcatenate(boolean applyOtherFirst, MathTransform other, MathTransformFactory factory)
             throws FactoryException
     {
         if (other instanceof LinearTransform) {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransformProvider.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransformProvider.java
index 9fe23b2..5f464b1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransformProvider.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransformProvider.java
@@ -108,7 +108,7 @@ public interface MathTransformProvider {
      * Some math transforms may actually be implemented as a chain of operation steps, for example a
      * {@linkplain DefaultMathTransformFactory#createConcatenatedTransform(MathTransform, MathTransform)
      * concatenation} of {@linkplain DefaultMathTransformFactory#createAffineTransform affine transforms}
-     * with other kind of transforms. In such cases, implementors should use the given factory for creating
+     * with other kind of transforms. In such cases, implementations should use the given factory for creating
      * the steps.
      *
      * @param  factory     the factory to use if this constructor needs to create other math transforms.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
index dfdc469..26495d3 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@ -101,7 +101,7 @@ public final class MathTransforms extends Static {
         final LinearTransform tr;
         switch (vector.length) {
             case 0:  return IdentityTransform.create(0);
-            case 1:  tr = new LinearTransform1D(1, vector[0]); break;
+            case 1:  return LinearTransform1D.create(1, vector[0]);
             case 2:  tr = new AffineTransform2D(1, 0, 0, 1, vector[0], vector[1]); break;
             default: tr = new TranslationTransform(vector); break;
         }
@@ -109,6 +109,27 @@ public final class MathTransforms extends Static {
     }
 
     /**
+     * Creates a transform which applies the given scale.
+     * The source and target dimensions of the transform are the length of the given vector.
+     *
+     * @param  factors  the scale factors.
+     * @return a transform applying the given scale.
+     *
+     * @since 1.0
+     */
+    public static LinearTransform scale(final double... factors) {
+        ArgumentChecks.ensureNonNull("factors", factors);
+        final LinearTransform tr;
+        switch (factors.length) {
+            case 0:  return IdentityTransform.create(0);
+            case 1:  return LinearTransform1D.create(factors[0], 0);
+            case 2:  tr = new AffineTransform2D(factors[0], 0, 0, factors[1], 0, 0); break;
+            default: tr = new ScaleTransform(factors); break;
+        }
+        return tr.isIdentity() ? IdentityTransform.create(factors.length) : tr;
+    }
+
+    /**
      * Creates a one-dimensional affine transform for the given coefficients.
      * Input values <var>x</var> will be converted into output values <var>y</var> using the following equation:
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ScaleTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ScaleTransform.java
index 429c98e..fc9bb4d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ScaleTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ScaleTransform.java
@@ -34,7 +34,7 @@ import org.apache.sis.util.ArraysExt;
  * {@link org.apache.sis.internal.referencing.j2d.AffineTransform2D} should be used in such case.</div>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.0
  *
  * @see <a href="http://issues.apache.org/jira/browse/SIS-176">SIS-176</a>
  *
@@ -66,6 +66,15 @@ final class ScaleTransform extends AbstractLinearTransform implements ExtendedPr
     private final int numDroppedDimensions;
 
     /**
+     * Constructs a scale transform for the given scale factors.
+     */
+    ScaleTransform(final double[] factors) {
+        this.factors = factors.clone();
+        errors = null;
+        numDroppedDimensions = 0;
+    }
+
+    /**
      * Constructs a scale transform from a matrix having the given elements.
      * This constructors assumes that the matrix is affine and contains only
      * scale coefficients (this is not verified).
@@ -147,11 +156,6 @@ final class ScaleTransform extends AbstractLinearTransform implements ExtendedPr
 
     /**
      * Tests whether this transform does not move any points.
-     *
-     * <div class="note"><b>Note:</b> this method should always returns {@code false}, since
-     * {@code MathTransforms.linear(…)} should have created specialized implementations for identity cases.
-     * Nevertheless we perform the full check as a safety, in case someone instantiated this class directly
-     * instead than using a factory method.</div>
      */
     @Override
     public boolean isIdentity() {
diff --git a/pom.xml b/pom.xml
index d17afa0..80ec310 100644
--- a/pom.xml
+++ b/pom.xml
@@ -524,7 +524,7 @@
     <maven.compiler.source>8</maven.compiler.source>
     <maven.compiler.target>8</maven.compiler.target>
     <sis.plugin.version>${project.version}</sis.plugin.version>
-    <sis.non-free.version>1.0-M1</sis.non-free.version>
+    <sis.non-free.version>1.0-M1</sis.non-free.version>                 <!-- Used only if "non-free" profile is enabled. -->
     <geoapi.version>4.0-SNAPSHOT</geoapi.version>
   </properties>
 
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
index 88afe0b..6e94e98 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
@@ -93,22 +93,25 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
 
     /**
      * The grid geometry (size, CRS…) of the {@linkplain #data} cube.
+     * This defines the "domain" in "coverage function" terminology.
      *
      * @see #getGridGeometry()
      */
     private final GridGeometry gridGeometry;
 
     /**
-     * The netCDF variable wrapped by this resource.
+     * The sample dimension for the {@link #data} variables.
+     * This defines the "range" in "coverage function" terminology.
+     * All elements are initially {@code null} and created when first needed.
+     *
+     * @see #getSampleDimensions()
      */
-    private final Variable[] data;
+    private final SampleDimension[] ranges;
 
     /**
-     * The sample dimension for the {@link #data} variables, created when first needed.
-     *
-     * @see #getSampleDimensions()
+     * The netCDF variable wrapped by this resource.
      */
-    private List<SampleDimension> definitions;
+    private final Variable[] data;
 
     /**
      * Path to the netCDF file for information purpose, or {@code null} if unknown.
@@ -130,6 +133,7 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
     {
         super(decoder.listeners);
         this.data    = data.toArray(new Variable[data.size()]);
+        ranges       = new SampleDimension[this.data.length];
         gridGeometry = grid.getGridGeometry(decoder);
         identifier   = decoder.nameFactory.createLocalName(decoder.namespace, name);
         location     = decoder.location;
@@ -238,16 +242,15 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public List<SampleDimension> getSampleDimensions() {
-        if (definitions == null) {
-            final SampleDimension.Builder builder = new SampleDimension.Builder();
-            final SampleDimension[] dimensions = new SampleDimension[data.length];
-            for (int i=0; i<dimensions.length; i++) {
-                dimensions[i] = createSampleDimension(builder, data[i]);
+        SampleDimension.Builder builder = null;
+        for (int i=0; i<ranges.length; i++) {
+            if (ranges[i] == null) {
+                if (builder == null) builder = new SampleDimension.Builder();
+                ranges[i] = createSampleDimension(builder, data[i]);
                 builder.clear();
             }
-            definitions = UnmodifiableArrayList.wrap(dimensions);
         }
-        return definitions;
+        return UnmodifiableArrayList.wrap(ranges);
     }
 
     /**
@@ -300,12 +303,10 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
             builder.addQuantitative(data.getName(), range, mt, data.getUnit());
         }
         /*
-         * Adds the "missing value" or "fill value" as qualitative categories.
-         * If a value has both roles, use "missing value" for category name.
-         * If the values are already real values, then the "no data" values
-         * have been replaced by NaN values by Variable.replaceNaN(Object).
-         * The qualitative categories constructed below must be consistent
-         * with the NaN values used by 'replaceNaN'.
+         * Adds the "missing value" or "fill value" as qualitative categories.  If a value has both roles, use "missing value"
+         * as category name. If the sample values are already real values, then the "no data" values have been replaced by NaN
+         * values by Variable.replaceNaN(Object). The qualitative categories constructed below must be consistent with the NaN
+         * values created by 'replaceNaN'.
          */
         boolean setBackground = true;
         int ordinal = data.hasRealValues() ? 0 : -1;
@@ -342,39 +343,67 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
      * Loads a subset of the grid coverage represented by this resource.
      *
      * @param  domain  desired grid extent and resolution, or {@code null} for reading the whole domain.
-     * @param  range   0-based index of sample dimensions to read, or an empty sequence for reading all ranges.
+     * @param  range   0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
      * @return the grid coverage for the specified domain and range.
      * @throws DataStoreException if an error occurred while reading the grid coverage data.
      */
     @Override
-    public GridCoverage read(GridGeometry domain, final int... range) throws DataStoreException {
+    public GridCoverage read(GridGeometry domain, int... range) throws DataStoreException {
+        range = validateRangeArgument(data.length, range);
         if (domain == null) {
             domain = gridGeometry;
         }
-        final Buffer[] samples = new Buffer[data.length];
+        final DataType          dataType;
+        final DataBuffer        imageBuffer;
+        final SampleDimension[] selected = new SampleDimension[range.length];
         try {
-            final GridChange change = new GridChange(domain, gridGeometry);
-            final int[] strides = change.getTargetStrides();
-            for (int i=0; i<samples.length; i++) {
-                // Optional.orElseThrow() below should never fail since Variable.read(…) wraps primitive array.
-                samples[i] = data[i].read(change.getTargetRange(), strides).buffer().get();
+            final Buffer[]   samples = new Buffer[range.length];
+            final GridChange change  = new GridChange(domain, gridGeometry);
+            final int[]      strides = change.getTargetStrides();
+            SampleDimension.Builder builder = null;
+            /*
+             * Iterate over netCDF variables in the order they appear in the file, not in the order requested
+             * by the 'range' argument.  The intent is to perform sequential I/O as much as possible, without
+             * seeking backward.
+             */
+            for (int i=0; i<data.length; i++) {
+                final Variable variable = data[i];
+                SampleDimension def = ranges[i];
+                Buffer values = null;
+                for (int j=0; j<range.length; j++) {
+                    /*
+                     * Check if the current variable is a sample dimension specified in the 'range' argument.
+                     * Note that the same sample dimension may be requested an arbitrary amount of time, but
+                     * the data will be loaded at most once.
+                     */
+                    if (range[j] == i) {
+                        if (def == null) {
+                            if (builder == null) builder = new SampleDimension.Builder();
+                            ranges[i] = def = createSampleDimension(builder, variable);
+                        }
+                        if (values == null) {
+                            // Optional.orElseThrow() below should never fail since Variable.read(…) wraps primitive array.
+                            values = variable.read(change.getTargetExtent(), strides).buffer().get();
+                        }
+                        selected[j] = def;
+                        samples[j] = values;
+                    }
+                }
             }
+            dataType = data[range[0]].getDataType();
+            imageBuffer = RasterFactory.wrap(dataType.rasterDataType, samples);
+            domain = change.getTargetGeometry(strides);
         } catch (TransformException e) {
             throw new DataStoreReferencingException(e);
         } catch (IOException e) {
             throw new DataStoreException(e);
-        }
-        final DataType type = data[0].getDataType();
-        final DataBuffer buffer;
-        try {
-            buffer = RasterFactory.wrap(type.rasterDataType, samples);
-        } catch (RuntimeException e) {
+        } catch (RuntimeException e) {                  // Many exceptions thrown by RasterFactory.wrap(…).
             throw new DataStoreContentException(e);
         }
-        if (buffer == null) {
-            throw new DataStoreContentException(Errors.format(Errors.Keys.UnsupportedType_1, type.name()));
+        if (imageBuffer == null) {
+            throw new DataStoreContentException(Errors.format(Errors.Keys.UnsupportedType_1, dataType.name()));
         }
-        return new Image(domain, getSampleDimensions(), buffer);
+        return new Image(domain, UnmodifiableArrayList.wrap(selected), imageBuffer);
     }
 
     /**
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 bd06abb..a74ad5d 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
@@ -24,6 +24,8 @@ import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ArraysExt;
 
 
 /**
@@ -86,4 +88,29 @@ public abstract class AbstractGridResource extends AbstractResource implements G
             metadata.addNewBand(band);
         }
     }
+
+    /**
+     * Validate the {@code range} argument given to {@link #read(GridGeometry, int...)}.
+     * This method verifies that all indices are between 0 and {@code numSampleDimensions},
+     * but does not verify if there is duplicated indices since such duplication is allowed.
+     *
+     * @param  numSampleDimensions  number of sample dimensions.
+     * @param  range                the {@code range} argument given by the user.
+     * @return the 0-based indices of ranges to use. May be the given {@code range} argument, or a sequence
+     *         from 0 to {@code numSampleDimensions} exclusive if {@code range} was {@code null} or empty.
+     */
+    protected final int[] validateRangeArgument(final int numSampleDimensions, final int[] range) {
+        ArgumentChecks.ensureStrictlyPositive("numSampleDimensions", numSampleDimensions);
+        if (range == null || range.length == 0) {
+            return ArraysExt.sequence(0, numSampleDimensions);
+        }
+        for (int i=0; i<range.length; i++) {
+            final int r = range[i];
+            if (r < 0 || r >= numSampleDimensions) {
+                throw new IllegalArgumentException(Resources.forLocale(getLocale()).getString(
+                        Resources.Keys.InvalidSampleDimensionIndex_2, i, numSampleDimensions - 1));
+            }
+        }
+        return range;
+    }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
index eccdca4..366007f 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
@@ -219,6 +219,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short InconsistentNameComponents_2 = 10;
 
         /**
+         * Sample dimension index {1} is invalid. Expected an index from 0 to {0} inclusive.
+         */
+        public static final short InvalidSampleDimensionIndex_2 = 52;
+
+        /**
          * Resource “{0}” does not have an identifier.
          */
         public static final short MissingResourceIdentifier_1 = 42;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
index 6cf0903..616f8e9 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
@@ -50,6 +50,7 @@ IllegalFeatureType_2              = The {0} data store does not accept features
 IllegalInputTypeForReader_2       = The {0} reader does not accept inputs of type \u2018{1}\u2019.
 IllegalOutputTypeForWriter_2      = The {0} writer does not accept outputs of type \u2018{1}\u2019.
 InconsistentNameComponents_2      = Components of the \u201c{1}\u201d name are inconsistent with those of the name previously binded in \u201c{0}\u201d data store.
+InvalidSampleDimensionIndex_2     = Sample dimension index {1} is invalid. Expected an index from 0 to {0} inclusive.
 MissingResourceIdentifier_1       = Resource \u201c{0}\u201d does not have an identifier.
 MissingSchemeInURI_1              = Missing scheme in \u201c{0}\u201d URI.
 NoSuchResourceDirectory_1         = No directory of resources found at \u201c{0}\u201d.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
index 8edfd0a..f5f3739 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
@@ -54,6 +54,7 @@ FoliationRepresentation           = Indique s\u2019il faut assembler les fragmen
 IllegalFeatureType_2              = Le format {0} ne stocke pas de donn\u00e9es de type \u00ab\u202f{1}\u202f\u00bb.
 IllegalInputTypeForReader_2       = Le lecteur {0} n\u2019accepte pas des entr\u00e9s de type \u2018{1}\u2019.
 IllegalOutputTypeForWriter_2      = L\u2019encodeur {0} n\u2019accepte pas des sorties de type \u2018{1}\u2019.
+InvalidSampleDimensionIndex_2     = L\u2019index de dimension d\u2019\u00e9chantillonnage {1} est invalide. On attendait un index de 0 \u00e0 {0} inclusif.
 InconsistentNameComponents_2      = Les \u00e9l\u00e9ments qui composent le nom \u00ab\u202f{1}\u202f\u00bb ne sont pas coh\u00e9rents avec ceux du nom qui avait \u00e9t\u00e9 pr\u00e9c\u00e9demment li\u00e9 dans les donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb.
 MissingResourceIdentifier_1       = La ressource \u00ab\u202f{0}\u202f\u00bb n\u2019a pas d\u2019identifiant.
 MissingSchemeInURI_1              = Il manque le sch\u00e9ma dans l\u2019URI \u00ab\u202f{0}\u202f\u00bb.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java
index 6e9bd6e..6f76c27 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/GridCoverageResource.java
@@ -84,7 +84,7 @@ public interface GridCoverageResource extends DataSet {
      * at a later stage.</p>
      *
      * @param  domain  desired grid extent and resolution, or {@code null} for reading the whole domain.
-     * @param  range   0-based index of sample dimensions to read, or an empty sequence for reading all ranges.
+     * @param  range   0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
      * @return the grid coverage for the specified domain and range.
      * @throws DataStoreException if an error occurred while reading the grid coverage data.
      */


Mime
View raw message