sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Refactor GridGeometry.Modifier as methods provided directly in GridDerivation.
Date Sun, 13 Jan 2019 19:23:49 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

commit bd65feded38b31a7f0f7d6bd724c62487d2f462d
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sun Jan 13 17:38:39 2019 +0100

    Refactor GridGeometry.Modifier as methods provided directly in GridDerivation.
---
 .../org/apache/sis/coverage/grid/GridCoverage.java |   4 +-
 .../apache/sis/coverage/grid/GridDerivation.java   | 455 +++++++++++++++------
 .../org/apache/sis/coverage/grid/GridExtent.java   |  22 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java | 241 +----------
 .../org/apache/sis/internal/raster/Resources.java  |   5 +
 .../sis/internal/raster/Resources.properties       |   1 +
 .../sis/internal/raster/Resources_fr.properties    |   1 +
 ...idGeometryTest.java => GridDerivationTest.java} | 216 +---------
 .../apache/sis/coverage/grid/GridGeometryTest.java | 195 +--------
 .../org/apache/sis/test/suite/RasterTestSuite.java |   1 +
 10 files changed, 379 insertions(+), 762 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index b794f2b..7d19398 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -147,8 +147,8 @@ public abstract class GridCoverage {
      * Then:
      *
      * <blockquote><code>sliceExtent = {@linkplain #getGridGeometry()}.{@link GridGeometry#derive()
-     * derive()}.{@linkplain GridGeometry.Modifier#slice(DirectPosition)
-     * slice}(slicePoint).{@linkplain GridGeometry.Modifier#build() build()};</code></blockquote>
+     * derive()}.{@linkplain GridDerivation#slice(DirectPosition)
+     * slice}(slicePoint).{@linkplain GridDerivation#build() build()};</code></blockquote>
      *
      * If the {@code slicePoint} CRS is different than this grid coverage CRS (except for the number of dimensions),
      * a coordinate transformation will be applied as needed.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
index 961311b..51c18b5 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
@@ -40,26 +40,54 @@ import org.opengis.coverage.PointOutsideCoverageException;
 
 
 /**
- * Helper class for computing the grid extent of a sub-area of a given grid geometry.
- * This class provides the {@link MathTransform} converting source grid coordinates to target grid coordinates,
- * where the source is the given {@link GridGeometry} instance and the target is the subsampled grid.
+ * Creates a new grid geometry derived from a base grid geometry with different extent or resolution.
+ * {@code GridDerivation} are created by calls to {@link GridGeometry#derive()}.
+ * Properties of the desired grid geometry can be specified by calls to the following methods:
+ *
+ * <ul>
+ *   <li>{@link #rounding(GridRoundingMode)} — if invoked, should be first.</li>
+ *   <li>At most one of the following methods:<ul>
+ *     <li>{@link #subgrid(Envelope, double...)}</li>
+ *     <li>{@link #slice(DirectPosition)}</li>
+ *   </ul></li>
+ *   <li>{@link #reduce(int...)}</li>
+ * </ul>
+ *
+ * Then the grid geometry is created by a call to {@link #build()}.
+ * Alternatively, {@link #extent()} can be invoked if only the {@link GridExtent} is desired
+ * instead than the full {@link GridGeometry}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
- * @since   1.0
+ *
+ * @see GridGeometry#derive()
+ *
+ * @since 1.0
  * @module
  */
-final class GridDerivation {
+public class GridDerivation {
+    /**
+     * The base grid geometry from which to derive a new grid geometry.
+     */
+    protected final GridGeometry base;
+
+    /**
+     * If {@link #slice(DirectPosition)} or {@link #subgrid(Envelope, double...)} has been invoked, the method name.
+     * This is used for preventing those methods to be invoked twice, which is currently not supported.
+     */
+    private String subGridSetter;
+
     /**
-     * The sub-extent computed by the constructor.
+     * The sub-extent computed by a {@link #slice(DirectPosition)} or {@link #subgrid(Envelope, double...)} methods,
+     * or {@code null} if no slice or sub-grid has been requested.
      */
-    GridExtent extent;
+    private GridExtent subExtent;
 
     /**
      * The conversion from the original grid to the subsampled grid, or {@code null} if no subsampling is applied.
-     * This is computed by the constructor.
+     * This is computed by {@link #subgrid(Envelope, double...)}.
      */
-    MathTransform toSubsampled;
+    private MathTransform toSubsampled;
 
     /**
      * List of grid dimensions that are modified by the {@code cornerToCRS} transform, or null for all dimensions.
@@ -69,23 +97,104 @@ final class GridDerivation {
     private int[] modifiedDimensions;
 
     /**
-     * Computes the sub-grid for a slice at the given slice point. The given position can be given in any CRS.
-     * The position should not define a coordinate for all dimensions, otherwise the sub-grid would degenerate
-     * to a single point. Dimensions can be left unspecified either by assigning to the position a CRS without
-     * those dimensions, or by assigning the NaN value to some coordinates.
+     * The grid dimension to keep, or {@code null} if no filtering is applied.
+     * Those values are set by {@link #reduce(int...)}.
+     */
+    private int[] selectedDimensions;
+
+    /**
+     * Controls behavior of rounding from floating point values to integers.
+     */
+    private GridRoundingMode rounding;
+
+    /**
+     * Creates a new builder for deriving a grid geometry from the specified base.
+     *
+     * @param  base  the base to use as a template for deriving a new grid geometry.
+     *
+     * @see GridGeometry#derive()
+     */
+    protected GridDerivation(final GridGeometry base) {
+        ArgumentChecks.ensureNonNull("base", base);
+        this.base = base;
+        rounding = GridRoundingMode.NEAREST;
+    }
+
+    /**
+     * Controls behavior of rounding from floating point values to integers.
+     * This setting modifies computations performed by the following methods
+     * (it has no effect on other methods in this {@code GridDerivation} class):
+     * <ul>
+     *   <li>{@link #slice(DirectPosition)}</li>
+     *   <li>{@link #subgrid(Envelope, double...)}</li>
+     * </ul>
+     *
+     * If this method is never invoked, the default value is {@link GridRoundingMode#NEAREST}.
+     * If this method is invoked too late, an {@link IllegalStateException} is thrown.
+     *
+     * @param  mode  the new rounding mode.
+     * @return {@code this} for method call chaining.
+     * @throws IllegalStateException if {@link #slice(DirectPosition)} or
+     *         {@link #subgrid(Envelope, double...)} has already been invoked.
+     */
+    public GridDerivation rounding(final GridRoundingMode mode) {
+        ArgumentChecks.ensureNonNull("mode", mode);
+        ensureSubgridNotSet();
+        rounding = mode;
+        return this;
+    }
+
+    /**
+     * Verifies that a sub-grid has not yet been defined.
+     */
+    private void ensureSubgridNotSet() {
+        if (subGridSetter != null) {
+            throw new IllegalStateException(Resources.format(Resources.Keys.CanNotSetDerivedGridProperty_1, subGridSetter));
+        }
+    }
+
+    /**
+     * Requests a grid geometry for a slice at the given point.
+     * The given position can be expressed in any coordinate reference system (CRS).
+     * The position should not define a coordinate for all dimensions, otherwise the slice would degenerate
+     * to a single point. Dimensions can be left unspecified either by assigning to {@code slicePoint} a CRS
+     * without those dimensions, or by assigning the NaN value to some coordinates.
+     *
+     * <div class="note"><b>Example:</b>
+     * if the {@linkplain GridGeometry#getCoordinateReferenceSystem() coordinate reference system} of base grid geometry has
+     * (<var>longitude</var>, <var>latitude</var>, <var>time</var>) axes, then a (<var>longitude</var>, <var>latitude</var>)
+     * slice at time <var>t</var> can be created with one of the following two positions:
+     * <ul>
+     *   <li>A three-dimensional position with ({@link Double#NaN}, {@link Double#NaN}, <var>t</var>) coordinates.</li>
+     *   <li>A one-dimensional position with (<var>t</var>) coordinate and the coordinate reference system set to
+     *       {@linkplain org.apache.sis.referencing.CRS#getTemporalComponent(CoordinateReferenceSystem) the temporal component}
+     *       of the grid geometry CRS.</li>
+     * </ul></div>
+     *
+     * <p>Notes:</p>
+     * <ul>
+     *   <li>This method can be invoked only once.</li>
+     *   <li>This method can not be used together with {@link #subgrid(Envelope, double...)}.</li>
+     *   <li>If a non-default rounding mode is desired, it should be {@linkplain #rounding(GridRoundingMode) specified}
+     *       before to invoke this method.</li>
+     *   <li>This method does not reduce the number of dimensions of the grid geometry.
+     *       For dimensionality reduction, see {@link #reduce(int...)}.</li>
+     * </ul>
      *
-     * @param  grid         the enclosing grid geometry (mandatory).
-     * @param  cornerToCRS  the transform from cell corners to grid CRS (mandatory).
      * @param  slicePoint   the coordinates where to get a slice.
-     * @throws TransformException if an error occurred while converting the envelope coordinates to grid coordinates.
+     * @return {@code this} for method call chaining.
+     * @throws IncompleteGridGeometryException if the base grid geometry has no extent or no "grid to CRS" transform.
+     * @throws IllegalGridGeometryException if an error occurred while converting the point coordinates to grid coordinates.
      * @throws PointOutsideCoverageException if the given point is outside the grid extent.
      */
-    GridDerivation(final GridGeometry grid, MathTransform cornerToCRS, final DirectPosition slicePoint)
-            throws TransformException
-    {
+    public GridDerivation slice(final DirectPosition slicePoint) {
+        ArgumentChecks.ensureNonNull("slicePoint", slicePoint);
+        ensureSubgridNotSet();
+        MathTransform cornerToCRS = base.requireGridToCRS();
+        subGridSetter = "slice";
         try {
-            if (grid.envelope != null) {
-                final CoordinateReferenceSystem sourceCRS = grid.envelope.getCoordinateReferenceSystem();
+            if (base.envelope != null) {
+                final CoordinateReferenceSystem sourceCRS = base.envelope.getCoordinateReferenceSystem();
                 if (sourceCRS != null) {
                     final CoordinateReferenceSystem targetCRS = slicePoint.getCoordinateReferenceSystem();
                     if (targetCRS != null) {
@@ -97,114 +206,141 @@ final class GridDerivation {
             final int dimension = cornerToCRS.getTargetDimensions();
             ArgumentChecks.ensureDimensionMatches("slicePoint", dimension, slicePoint);
             cornerToCRS = dropUnusedDimensions(cornerToCRS, dimension);
+            subExtent = base.extent.slice(cornerToCRS.inverse().transform(slicePoint, null), modifiedDimensions);
         } catch (FactoryException e) {
-            throw new TransformException(Resources.format(Resources.Keys.CanNotMapToGridDimensions), e);
+            throw new IllegalGridGeometryException(Resources.format(Resources.Keys.CanNotMapToGridDimensions), e);
+        } catch (TransformException e) {
+            throw new IllegalGridGeometryException(e, "slicePoint");
         }
-        extent = grid.extent.slice(cornerToCRS.inverse().transform(slicePoint, null), modifiedDimensions);
+        return this;
     }
 
     /**
-     * Computes the sub-grid over the given area of interest with the given resolution.
-     * At least one of {@code areaOfInterest} and {@code resolution} shall be non-null.
-     * It is caller's responsibility to ensure that {@link GridGeometry#extent} is non-null.
+     * Requests a grid geometry over a sub-region of the base grid geometry and optionally with subsampling.
+     * The given envelope does not need to be expressed in the same coordinate reference system (CRS)
+     * than {@linkplain GridGeometry#getCoordinateReferenceSystem() the CRS of the base grid geometry};
+     * coordinate conversions or transformations will be applied as needed.
+     * That envelope CRS may have fewer dimensions than the base grid geometry CRS,
+     * in which case grid dimensions not mapped to envelope dimensions will be returned unchanged.
+     * The target resolution, if provided, shall be in same units and same order than the given envelope axes.
+     * If the length of {@code resolution} array is less than the number of dimensions of {@code areaOfInterest},
+     * then no subsampling will be applied on the missing dimensions.
+     *
+     * <p>Notes:</p>
+     * <ul>
+     *   <li>This method can be invoked only once.</li>
+     *   <li>This method can not be used together with {@link #slice(DirectPosition)}.</li>
+     *   <li>If a non-default rounding mode is desired, it should be {@linkplain #rounding(GridRoundingMode) specified}
+     *       before to invoke this method.</li>
+     *   <li>This method does not reduce the number of dimensions of the grid geometry.
+     *       For dimensionality reduction, see {@link #reduce(int...)}.</li>
+     * </ul>
+     *
+     * @param  areaOfInterest    the desired spatiotemporal region in any CRS (transformations will be applied as needed),
+     *                           or {@code null} for not restricting the sub-grid to a sub-area.
+     * @param  resolution        the desired resolution in the same units and order than the axes of the given envelope,
+     *                           or {@code null} or an empty array if no subsampling is desired.
+     * @return {@code this} for method call chaining.
+     * @throws IncompleteGridGeometryException if the base grid geometry has no extent or no "grid to CRS" transform.
+     * @throws IllegalGridGeometryException if an error occurred while converting the envelope coordinates to grid coordinates.
      *
-     * @param  grid            the enclosing grid geometry (mandatory).
-     * @param  cornerToCRS     the transform from cell corners to grid CRS (mandatory).
-     * @param  areaOfInterest  the desired spatiotemporal region in any CRS, or {@code null} for the whole area.
-     * @param  resolution      the desired resolution in the same units and order than the axes of the AOI envelope,
-     *                         or {@code null} or an empty array if no subsampling is desired.
-     * @param  rounding        controls behavior of rounding from floating point values to integers.
-     * @throws TransformException if an error occurred while converting the envelope coordinates to grid coordinates.
+     * @see GridExtent#subsample(int[])
      */
-    GridDerivation(final GridGeometry grid, MathTransform cornerToCRS, final Envelope areaOfInterest,
-            double[] resolution, final GridRoundingMode rounding) throws TransformException, FactoryException
-    {
-        /*
-         * If the envelope CRS is different than the expected CRS, concatenate the envelope transformation
-         * to the 'gridToCRS' transform.  We should not transform the envelope here - only concatenate the
-         * transforms - because transforming envelopes twice would add errors.
-         */
-        final CoordinateOperation operation = Envelopes.findOperation(grid.envelope, areaOfInterest);
-        if (operation != null) {
-            cornerToCRS = MathTransforms.concatenate(cornerToCRS, operation.getMathTransform());
-        }
-        /*
-         * If the envelope dimensions does not encompass all grid dimensions, the envelope is probably non-invertible.
-         * We need to reduce the number of grid dimensions in the transform for having a one-to-one relationship.
-         */
-        int dimension = cornerToCRS.getTargetDimensions();
-        ArgumentChecks.ensureDimensionMatches("areaOfInterest", dimension, areaOfInterest);
-        cornerToCRS = dropUnusedDimensions(cornerToCRS, dimension);
-        /*
-         * Compute the sub-extent for the given Area Of Interest (AOI), ignoring for now the subsampling.
-         * If no area of interest has been specified, or if the result is identical to the original extent,
-         * then we will keep the reference to the original GridExtent (i.e. we share existing instances).
-         */
-        extent = grid.extent;
-        dimension = extent.getDimension();
-        GeneralEnvelope indices = null;
-        if (areaOfInterest != null) {
-            indices = Envelopes.transform(cornerToCRS.inverse(), areaOfInterest);
-            setExtent(indices, extent, rounding);
-        }
-        if (indices == null || indices.getDimension() != dimension) {
-            indices = new GeneralEnvelope(dimension);
-        }
-        for (int i=0; i<dimension; i++) {
-            indices.setRange(i, extent.getLow(i), extent.getHigh(i) + 1.0);
-        }
-        /*
-         * Convert the target resolutions to grid cell subsamplings and adjust the extent consequently.
-         * We perform this conversion by handling the resolution has a small translation vector located
-         * at the point of interest, and converting it to a translation vector in grid coordinates. The
-         * conversion is done by a multiplication with the "CRS to grid" derivative at that point.
-         *
-         * The subsampling will be rounded in such a way that the difference in grid size is less than
-         * one half of cell. Demonstration:
-         *
-         *    e = Math.getExponent(span)     →    2^e ≦ span
-         *    a = e+1                        →    2^a > span     →    1/2^a < 1/span
-         *   Δs = (s - round(s)) / 2^a
-         *   (s - round(s)) ≦ 0.5            →    Δs  ≦  0.5/2^a  <  0.5/span
-         *   Δs < 0.5/span                   →    Δs⋅span < 0.5 cell.
-         */
-        if (resolution != null && resolution.length != 0) {
-            resolution = ArraysExt.resize(resolution, cornerToCRS.getTargetDimensions());
-            final int[] modifiedDimensions = this.modifiedDimensions;                     // Will not change anymore.
-            Matrix m = cornerToCRS.derivative(new DirectPositionView.Double(getPointOfInterest(modifiedDimensions)));
-            resolution = Matrices.inverse(m).multiply(resolution);
-            boolean modified = false;
-            for (int k=0; k<resolution.length; k++) {
-                double s = Math.abs(resolution[k]);
-                if (s > 1) {                                // Also for skipping NaN values.
-                    final int i = (modifiedDimensions != null) ? modifiedDimensions[k] : k;
-                    final int accuracy = Math.max(0, Math.getExponent(indices.getSpan(i))) + 1;         // Power of 2.
-                    s = Math.scalb(Math.rint(Math.scalb(s, accuracy)), -accuracy);
-                    indices.setRange(i, indices.getLower(i) / s, indices.getUpper(i) / s);
-                    modified = true;
-                }
-                resolution[k] = s;
+    public GridDerivation subgrid(final Envelope areaOfInterest, double... resolution) {
+        ensureSubgridNotSet();
+        MathTransform cornerToCRS = base.requireGridToCRS();
+        subGridSetter = "subgrid";
+        try {
+            /*
+             * If the envelope CRS is different than the expected CRS, concatenate the envelope transformation
+             * to the 'gridToCRS' transform.  We should not transform the envelope here - only concatenate the
+             * transforms - because transforming envelopes twice would add errors.
+             */
+            final CoordinateOperation operation = Envelopes.findOperation(base.envelope, areaOfInterest);
+            if (operation != null) {
+                cornerToCRS = MathTransforms.concatenate(cornerToCRS, operation.getMathTransform());
+            }
+            /*
+             * If the envelope dimensions does not encompass all grid dimensions, the envelope is probably non-invertible.
+             * We need to reduce the number of grid dimensions in the transform for having a one-to-one relationship.
+             */
+            int dimension = cornerToCRS.getTargetDimensions();
+            ArgumentChecks.ensureDimensionMatches("areaOfInterest", dimension, areaOfInterest);
+            cornerToCRS = dropUnusedDimensions(cornerToCRS, dimension);
+            /*
+             * Compute the sub-extent for the given Area Of Interest (AOI), ignoring for now the subsampling.
+             * If no area of interest has been specified, or if the result is identical to the original extent,
+             * then we will keep the reference to the original GridExtent (i.e. we share existing instances).
+             */
+            subExtent = base.extent;
+            dimension = subExtent.getDimension();
+            GeneralEnvelope indices = null;
+            if (areaOfInterest != null) {
+                indices = Envelopes.transform(cornerToCRS.inverse(), areaOfInterest);
+                setSubExtent(indices, subExtent);
+            }
+            if (indices == null || indices.getDimension() != dimension) {
+                indices = new GeneralEnvelope(dimension);
+            }
+            for (int i=0; i<dimension; i++) {
+                indices.setRange(i, subExtent.getLow(i), subExtent.getHigh(i) + 1.0);
             }
             /*
-             * If at least one subsampling is effective, build a scale from the old grid coordinates to the new
-             * grid coordinates. If we had no rounding, the conversion would be only a scale. But because of rounding,
-             * we need a small translation for the difference between the "real" coordinate and the integer coordinate.
+             * Convert the target resolutions to grid cell subsamplings and adjust the extent consequently.
+             * We perform this conversion by handling the resolutions as a small translation vector located
+             * at the point of interest, and converting it to a translation vector in grid coordinates. The
+             * conversion is done by a multiplication with the "CRS to grid" derivative at that point.
+             *
+             * The subsampling will be rounded in such a way that the difference in grid size is less than
+             * one half of cell. Demonstration:
+             *
+             *    e = Math.getExponent(span)     →    2^e ≦ span
+             *    a = e+1                        →    2^a > span     →    1/2^a < 1/span
+             *   Δs = (s - round(s)) / 2^a
+             *   (s - round(s)) ≦ 0.5            →    Δs  ≦  0.5/2^a  <  0.5/span
+             *   Δs < 0.5/span                   →    Δs⋅span < 0.5 cell.
              */
-            if (modified) {
-                final GridExtent unscaled = extent;
-                setExtent(indices, null, rounding);
-                m = Matrices.createIdentity(dimension + 1);
+            if (resolution != null && resolution.length != 0) {
+                resolution = ArraysExt.resize(resolution, cornerToCRS.getTargetDimensions());
+                Matrix m = cornerToCRS.derivative(new DirectPositionView.Double(getPointOfInterest()));
+                resolution = Matrices.inverse(m).multiply(resolution);
+                final int[] modifiedDimensions = this.modifiedDimensions;                     // Will not change anymore.
+                boolean modified = false;
                 for (int k=0; k<resolution.length; k++) {
-                    final double s = resolution[k];
-                    if (s > 1) {                            // Also for skipping NaN values.
+                    double s = Math.abs(resolution[k]);
+                    if (s > 1) {                                // Also for skipping NaN values.
                         final int i = (modifiedDimensions != null) ? modifiedDimensions[k] : k;
-                        m.setElement(i, i, s);
-                        m.setElement(i, dimension, unscaled.getLow(i) - extent.getLow(i) * s);
+                        final int accuracy = Math.max(0, Math.getExponent(indices.getSpan(i))) + 1;         // Power of 2.
+                        s = Math.scalb(Math.rint(Math.scalb(s, accuracy)), -accuracy);
+                        indices.setRange(i, indices.getLower(i) / s, indices.getUpper(i) / s);
+                        modified = true;
                     }
+                    resolution[k] = s;
+                }
+                /*
+                 * If at least one subsampling is effective, build a scale from the old grid coordinates to the new
+                 * grid coordinates. If we had no rounding, the conversion would be only a scale. But because of rounding,
+                 * we need a small translation for the difference between the "real" coordinate and the integer coordinate.
+                 */
+                if (modified) {
+                    final GridExtent unscaled = subExtent;
+                    setSubExtent(indices, null);
+                    m = Matrices.createIdentity(dimension + 1);
+                    for (int k=0; k<resolution.length; k++) {
+                        final double s = resolution[k];
+                        if (s > 1) {                            // Also for skipping NaN values.
+                            final int i = (modifiedDimensions != null) ? modifiedDimensions[k] : k;
+                            m.setElement(i, i, s);
+                            m.setElement(i, dimension, unscaled.getLow(i) - subExtent.getLow(i) * s);
+                        }
+                    }
+                    toSubsampled = MathTransforms.linear(m);
                 }
-                toSubsampled = MathTransforms.linear(m);
             }
+        } catch (FactoryException | TransformException e) {
+            throw new IllegalGridGeometryException(e, "areaOfInterest");
         }
+        return this;
     }
 
     /**
@@ -231,31 +367,96 @@ final class GridDerivation {
     }
 
     /**
-     * Sets {@link #extent} to the given envelope.
+     * Returns the point of interest of current {@link #subExtent}, keeping only the remaining
+     * dimensions after {@link #dropUnusedDimensions(MathTransform, int)} execution.
+     */
+    private double[] getPointOfInterest() {
+        final double[] pointOfInterest = subExtent.getPointOfInterest();
+        if (modifiedDimensions == null) {
+            return pointOfInterest;
+        }
+        final double[] filtered = new double[modifiedDimensions.length];
+        for (int i=0; i<filtered.length; i++) {
+            filtered[i] = pointOfInterest[modifiedDimensions[i]];
+        }
+        return filtered;
+    }
+
+    /**
+     * Sets {@link #subExtent} to the given envelope.
      *
      * @param  indices    the envelope to use for setting the grid extent.
      * @param  enclosing  the enclosing grid extent if a subsampling is not yet applied, {@code null} otherwise.
-     * @param  rounding   controls behavior of rounding from floating point values to integers.
      */
-    private void setExtent(final GeneralEnvelope indices, final GridExtent enclosing, final GridRoundingMode rounding) {
+    private void setSubExtent(final GeneralEnvelope indices, final GridExtent enclosing) {
         final GridExtent sub = new GridExtent(indices, rounding, null, enclosing, modifiedDimensions);
-        if (!sub.equals(extent)) {
-            extent = sub;
+        if (!sub.equals(subExtent)) {
+            subExtent = sub;
         }
     }
 
     /**
-     * Returns the point of interest of current {@link #extent}.
+     * Requests a grid geometry that encompass only some dimensions of the grid extent.
+     * The specified dimensions will be copied into a new grid geometry.
+     * The selection is applied on {@linkplain GridGeometry#getExtent() grid extent} dimensions;
+     * they are not necessarily the same than the {@linkplain GridGeometry#getEnvelope() envelope} dimensions.
+     * The given dimensions must be in strictly ascending order without duplicated values.
+     * The number of dimensions of the sub grid geometry will be {@code dimensions.length}.
+     *
+     * <p>This method performs a <cite>dimensionality reduction</cite>.
+     * This method can not be used for changing dimension order.</p>
+     *
+     * @param  dimensions  the grid (not CRS) dimensions to select, in strictly increasing order.
+     * @return {@code this} for method call chaining.
+     * @throws IndexOutOfBoundsException if an index is out of bounds.
+     *
+     * @see GridExtent#getSubspaceDimensions(int)
+     * @see GridExtent#reduce(int...)
+     * @see org.apache.sis.referencing.CRS#reduce(CoordinateReferenceSystem, int...)
      */
-    private double[] getPointOfInterest(final int[] modifiedDimensions) {
-        final double[] pointOfInterest = extent.getPointOfInterest();
-        if (modifiedDimensions == null) {
-            return pointOfInterest;
+    public GridDerivation reduce(final int... dimensions) {
+        if (selectedDimensions != null) {
+            throw new IllegalStateException(Resources.format(Resources.Keys.CanNotSetDerivedGridProperty_1, "reduce"));
         }
-        final double[] filtered = new double[modifiedDimensions.length];
-        for (int i=0; i<filtered.length; i++) {
-            filtered[i] = pointOfInterest[modifiedDimensions[i]];
+        selectedDimensions = GridExtent.verifyDimensions(dimensions, base.getDimension());
+        return this;
+    }
+
+    /**
+     * Returns the extent of the modified grid geometry. This method is more efficient than
+     * {@link #build()} if only the grid extent is desired instead than the full grid geometry.
+     *
+     * @return the modified grid geometry extent.
+     */
+    public GridExtent extent() {
+        GridExtent extent = (subExtent != null) ? subExtent : base.getExtent();
+        if (selectedDimensions != null) {
+            extent = extent.reduce(selectedDimensions);
         }
-        return filtered;
+        return extent;
+    }
+
+    /**
+     * Builds a grid geometry with the configuration specified by the other methods in this {@code GridDerivation} class.
+     *
+     * @return the modified grid geometry. May be the {@linkplain #base} grid geometry if no change apply.
+     */
+    public GridGeometry build() {
+        GridGeometry grid = base;
+        GridExtent extent = (subExtent != null) ? subExtent : base.extent;
+        String cause = null;
+        try {
+            if (toSubsampled != null || extent != base.extent) {
+                cause = "subgrid";
+                grid = new GridGeometry(grid, extent, toSubsampled);
+            }
+            if (selectedDimensions != null) {
+                cause = "dimensions";
+                grid = new GridGeometry(grid, selectedDimensions);
+            }
+        } catch (FactoryException | TransformException e) {
+            throw new IllegalGridGeometryException(e, cause);
+        }
+        return grid;
     }
 }
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 c1d7230..0ada60f 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
@@ -856,14 +856,18 @@ public class GridExtent implements Serializable {
      *
      * @param  dimensions  the user-supplied argument to validate.
      * @param  limit       maximal number of dimensions, exclusive.
-     * @return {@code true} if the caller can return {@code this}.
+     * @return a clone of the given array, or {@code null} if the caller can return {@code this}.
      */
-    static boolean verifyDimensions(final int[] dimensions, final int limit) {
+    static int[] verifyDimensions(int[] dimensions, final int limit) {
         ArgumentChecks.ensureNonNull("dimensions", dimensions);
         final int n = dimensions.length;
         if (n == 0) {
             throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "dimensions"));
         }
+        if (n > limit) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.TooManyOccurrences_2, limit, "dimension"));
+        }
+        dimensions = dimensions.clone();
         if (!ArraysExt.isSorted(dimensions, true)) {
             throw new IllegalArgumentException(Resources.format(Resources.Keys.NotStrictlyOrderedDimensions));
         }
@@ -871,7 +875,7 @@ public class GridExtent implements Serializable {
         if (d >= 0) {
             d = dimensions[n - 1];
             if (d < limit) {
-                return n == limit;
+                return (n != limit) ? dimensions : null;
             }
         }
         throw new IndexOutOfBoundsException(Errors.format(Errors.Keys.IndexOutOfBounds_1, d));
@@ -891,11 +895,12 @@ public class GridExtent implements Serializable {
      * @return the sub-envelope, or {@code this} if the given array contains all dimensions of this grid extent.
      * @throws IndexOutOfBoundsException if an index is out of bounds.
      *
-     * @see GridGeometry.Modifier#reduce(int...)
+     * @see GridDerivation#reduce(int...)
      */
-    public GridExtent reduce(final int... dimensions) {
+    public GridExtent reduce(int... dimensions) {
         final int sd = getDimension();
-        if (verifyDimensions(dimensions, sd)) {
+        dimensions = verifyDimensions(dimensions, sd);
+        if (dimensions == null) {
             return this;
         }
         final int td = dimensions.length;
@@ -935,7 +940,7 @@ public class GridExtent implements Serializable {
      * @return the subsampled extent, or {@code this} is subsampling results in the same extent.
      * @throws IllegalArgumentException if a period is not greater than zero.
      *
-     * @see GridGeometry.Modifier#subgrid(Envelope, double...)
+     * @see GridDerivation#subgrid(Envelope, double...)
      */
     public GridExtent subsample(final int... periods) {
         ArgumentChecks.ensureNonNull("periods", periods);
@@ -964,8 +969,7 @@ public class GridExtent implements Serializable {
      * Creates a new grid extent which represent a slice of this grid at the given point.
      * The given point may have less dimensions than this grid extent, in which case the
      * dimensions must be specified in the {@code modifiedDimensions} array. Coordinates
-     * in the given point will be rounded to nearest integer. This method does not reduce
-     * the number of dimensions of the grid extent.
+     * in the given point will be rounded to nearest integer.
      *
      * <p>This method does not reduce the number of dimensions of the grid extent.
      * For dimensionality reduction, see {@link #reduce(int...)}.</p>
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 73d7d25..a33d55e 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
@@ -26,7 +26,6 @@ import java.awt.image.RenderedImage;            // For javadoc only.
 import org.opengis.util.FactoryException;
 import org.opengis.metadata.Identifier;
 import org.opengis.geometry.Envelope;
-import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.Matrix;
@@ -60,9 +59,6 @@ import org.apache.sis.util.Classes;
 import org.apache.sis.util.Debug;
 import org.apache.sis.io.TableAppender;
 
-// Branch-dependent imports
-import org.opengis.coverage.PointOutsideCoverageException;
-
 
 /**
  * Valid extent of grid coordinates together with the transform from those grid coordinates
@@ -242,7 +238,7 @@ public class GridGeometry implements Serializable {
     /**
      * Creates a new grid geometry derived from the given grid geometry with a new extent and a modified transform.
      * This constructor is used for creating a grid geometry over a subregion (for example with the grid extent
-     * computed by {@link Modifier#subgrid(Envelope, double...)}) or grid geometry for a subsampled raster.
+     * computed by {@link GridDerivation#subgrid(Envelope, double...)}) or grid geometry for a subsampled raster.
      *
      * <p>If {@code toOther} is non-null, it should be a transform from the given {@code extent} coordinates to the
      * {@code other} grid coordinates. That transform should be merely a {@linkplain MathTransforms#scale(double...)
@@ -261,7 +257,7 @@ public class GridGeometry implements Serializable {
      * @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 Modifier#subgrid(Envelope, double...)
+     * @see GridDerivation#subgrid(Envelope, double...)
      */
     GridGeometry(final GridGeometry other, final GridExtent extent, final MathTransform toOther) throws TransformException {
         final int dimension = other.getDimension();
@@ -509,9 +505,9 @@ public class GridGeometry implements Serializable {
      * @param  dimensions  the dimensions to select, in strictly increasing order (this may not be verified).
      * @throws FactoryException if an error occurred while separating the "grid to CRS" transform.
      *
-     * @see Modifier#reduce(int...)
+     * @see GridDerivation#reduce(int...)
      */
-    private GridGeometry(final GridGeometry other, int[] dimensions) throws FactoryException {
+    GridGeometry(final GridGeometry other, int[] dimensions) throws FactoryException {
         extent = (other.extent != null) ? other.extent.reduce(dimensions) : null;
         final int n = dimensions.length;
         if (other.gridToCRS != null) {
@@ -630,7 +626,7 @@ public class GridGeometry implements Serializable {
      * {@linkplain #getGridToCRS(PixelInCell) transformed} to the "real world" coordinate system.
      * The initial envelope encompasses all cell surfaces, from the left border of leftmost cell
      * to the right border of the rightmost cell and similarly along other axes.
-     * If this grid geometry is a {@linkplain Modifier#subgrid(Envelope, double...) subgrid}, then the envelope is also
+     * If this grid geometry is a {@linkplain GridDerivation#subgrid(Envelope, double...) subgrid}, then the envelope is also
      * {@linkplain GeneralEnvelope#intersect(Envelope) clipped} to the envelope of the original (non subsampled) grid geometry.
      *
      * @return the bounding box in "real world" coordinates (never {@code null}).
@@ -936,14 +932,17 @@ public class GridGeometry implements Serializable {
     /**
      * Verifies that this grid geometry defines an {@linkplain #extent} and a {@link #cornerToCRS} transform.
      * They are the information required for mapping the grid to a spatiotemporal envelope.
+     *
+     * @return {@link #cornerToCRS}.
      */
-    private void requireGridToCRS() throws IncompleteGridGeometryException {
+    final MathTransform requireGridToCRS() throws IncompleteGridGeometryException {
         if (extent == null) {
             throw incomplete(EXTENT, Resources.Keys.UnspecifiedGridExtent);
         }
         if (cornerToCRS == null) {
             throw incomplete(GRID_TO_CRS, Resources.Keys.UnspecifiedTransform);
         }
+        return cornerToCRS;
     }
 
     /**
@@ -977,9 +976,9 @@ public class GridGeometry implements Serializable {
 
     /**
      * Returns an object that can be used for creating a new grid geometry derived from this grid geometry.
-     * Despite its name, {@code Modifier} does not change the state of this {@code GridGeometry} but instead
-     * creates new instances as needed. Examples of modifications include clipping to a sub-area, applying a
-     * sub-sampling, or selecting some grid dimensions.
+     * Despite its name, {@code GridDerivation} does not change the state of this {@code GridGeometry} but
+     * instead creates new instances as needed. Examples of modifications include clipping to a sub-area,
+     * applying a sub-sampling, or selecting some grid dimensions.
      *
      * <div class="note"><b>Example:</b>
      * for clipping this grid geometry to a sub-area, one can use:
@@ -992,222 +991,12 @@ public class GridGeometry implements Serializable {
      * }
      * </div>
      *
-     * Each {@code Modifier} instance can be used only once.
+     * Each {@code GridDerivation} instance can be used only once and should be used in a single thread.
      *
      * @return an object for deriving a grid geometry from {@code this}.
      */
-    public Modifier derive() {
-        return new Modifier();
-    }
-
-    /**
-     * Creates a new grid geometry derived from the enclosing grid geometry with different extent or resolution.
-     * {@code Modifier} are created by calls to {@link GridGeometry#derive()}. Properties of the desired grid
-     * geometry can be specified by {@link #rounding rounding}, {@link #subgrid subgrid}, {@link #slice slice}
-     * or {@link #reduce reduce} methods, and the grid geometry is created by {@link #build()}.
-     *
-     * @author  Martin Desruisseaux (Geomatys)
-     * @version 1.0
-     *
-     * @see GridGeometry#derive()
-     *
-     * @since 1.0
-     * @module
-     */
-    public class Modifier {
-        /**
-         * Builder of grid geometry based on a slice of enclosing grid geometry, or {@code null} if none.
-         */
-        private GridDerivation subgrid;
-
-        /**
-         * The grid dimension to keep, or {@code null} if no filtering is applied.
-         */
-        private int[] dimensions;
-
-        /**
-         * Controls behavior of rounding from floating point values to integers.
-         */
-        private GridRoundingMode rounding;
-
-        /**
-         * Creates a new modifier based on the enclosing grid geometry.
-         * This constructor is for subclasses only.
-         *
-         * @see GridGeometry#derive()
-         */
-        protected Modifier() {
-            rounding = GridRoundingMode.NEAREST;
-        }
-
-        /**
-         * Verifies that a sub-grid has not yet been defined.
-         */
-        private void ensureBeforeSubgrid() {
-            if (subgrid != null) {
-                throw new IllegalStateException();
-            }
-        }
-
-        /**
-         * Controls behavior of rounding from floating point values to integers.
-         * This method can be invoked before any method expecting an {@link Envelope} argument.
-         * If this method is never invoked, the default value is {@link GridRoundingMode#NEAREST}.
-         * If this method is invoked too late, an {@link IllegalStateException} is thrown.
-         *
-         * @param  mode  the new rounding mode.
-         * @return {@code this} for method call chaining.
-         * @throws IllegalStateException if {@link #subgrid(Envelope, double...)} has already been invoked.
-         */
-        public Modifier rounding(final GridRoundingMode mode) {
-            ArgumentChecks.ensureNonNull("mode", mode);
-            ensureBeforeSubgrid();
-            rounding = mode;
-            return this;
-        }
-
-        /**
-         * Returns a grid geometry over a sub-region of this grid geometry and optionally with subsampling.
-         * The given envelope does not need to be expressed in the same coordinate reference system (CRS)
-         * than {@linkplain #getCoordinateReferenceSystem() the CRS of this grid geometry};
-         * coordinate conversions or transformations will be applied as needed.
-         * That envelope CRS may have fewer dimensions than this grid geometry CRS,
-         * in which case grid dimensions not mapped to envelope dimensions will be returned unchanged.
-         * The target resolution, if provided, shall be in same units and same order than the given envelope axes.
-         * If the length of {@code targetResolution} array is less than the number of dimensions of {@code areaOfInterest},
-         * then no subsampling will be applied on the missing dimensions.
-         *
-         * <p>This method does not reduce the number of dimensions of this grid geometry.
-         * For dimensionality reduction, see {@link #reduce(int...)}.</p>
-         *
-         * @param  areaOfInterest    the desired spatiotemporal region in any CRS (transformations will be applied as needed),
-         *                           or {@code null} for not restricting the sub-grid to a sub-area.
-         * @param  targetResolution  the desired resolution in the same units and order than the axes of the given envelope,
-         *                           or {@code null} or an empty array if no subsampling is desired.
-         * @return {@code this} for method call chaining.
-         * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
-         * @throws IllegalGridGeometryException if an error occurred while converting the envelope coordinates to grid coordinates.
-         *
-         * @see GridExtent#subsample(int[])
-         */
-        public Modifier subgrid(final Envelope areaOfInterest, double... targetResolution) {
-            ensureBeforeSubgrid();
-            requireGridToCRS();
-            try {
-                subgrid = new GridDerivation(GridGeometry.this, cornerToCRS, areaOfInterest, targetResolution, rounding);
-            } catch (FactoryException | TransformException e) {
-                throw new IllegalGridGeometryException(e, "areaOfInterest");
-            }
-            return this;
-        }
-
-        /**
-         * Requests a grid geometry for a slice at the given point.
-         * The given position can be expressed in any coordinate reference system (CRS).
-         * The position should not define a coordinate for all dimensions, otherwise the slice would degenerate
-         * to a single point. Dimensions can be left unspecified either by assigning to {@code slicePoint} a CRS
-         * without those dimensions, or by assigning the NaN value to some coordinates.
-         *
-         * <div class="note"><b>Example:</b>
-         * if the {@linkplain #getCoordinateReferenceSystem() coordinate reference system} of current grid geometry has
-         * (<var>longitude</var>, <var>latitude</var>, <var>time</var>) axes, then a (<var>longitude</var>, <var>latitude</var>)
-         * slice at time <var>t</var> can be created with one of the following two positions:
-         * <ul>
-         *   <li>A three-dimensional position with ({@link Double#NaN}, {@link Double#NaN}, <var>t</var>) coordinates.</li>
-         *   <li>A one-dimensional position with (<var>t</var>) coordinate and the coordinate reference system set to
-         *       {@linkplain org.apache.sis.referencing.CRS#getTemporalComponent(CoordinateReferenceSystem) the temporal component}
-         *       of the grid geometry CRS.</li>
-         * </ul></div>
-         *
-         * This method does not reduce the number of dimensions of this grid geometry.
-         * For dimensionality reduction, see {@link #reduce(int...)}.
-         *
-         * @param  slicePoint   the coordinates where to get a slice.
-         * @return {@code this} for method call chaining.
-         * @throws IncompleteGridGeometryException if this grid geometry has no extent or no "grid to CRS" transform.
-         * @throws IllegalGridGeometryException if an error occurred while converting the point coordinates to grid coordinates.
-         * @throws PointOutsideCoverageException if the given point is outside the grid extent.
-         */
-        public Modifier slice(final DirectPosition slicePoint) {
-            ArgumentChecks.ensureNonNull("slicePoint", slicePoint);
-            ensureBeforeSubgrid();
-            requireGridToCRS();
-            try {
-                subgrid = new GridDerivation(GridGeometry.this, cornerToCRS, slicePoint);
-            } catch (TransformException e) {
-                throw new IllegalGridGeometryException(e, "slicePoint");
-            }
-            return this;
-        }
-
-        /**
-         * Requests a grid geometry that encompass only some dimensions of the grid extent.
-         * The specified dimensions will be copied into a new grid geometry.
-         * The selection is applied on {@linkplain #getExtent() grid extent} dimensions;
-         * they are not necessarily the same than the {@linkplain #getEnvelope() envelope} dimensions.
-         * The given dimensions must be in strictly ascending order without duplicated values.
-         * The number of dimensions of the sub grid geometry will be {@code dimensions.length}.
-         *
-         * <p>This method performs a <cite>dimensionality reduction</cite>.
-         * This method can not be used for changing dimension order.</p>
-         *
-         * @param  dimensions  the grid (not CRS) dimensions to select, in strictly increasing order.
-         * @return {@code this} for method call chaining.
-         * @throws IndexOutOfBoundsException if an index is out of bounds.
-         *
-         * @see GridExtent#getSubspaceDimensions(int)
-         * @see GridExtent#reduce(int...)
-         * @see org.apache.sis.referencing.CRS#reduce(CoordinateReferenceSystem, int...)
-         */
-        public Modifier reduce(int... dimensions) {
-            if (GridExtent.verifyDimensions(dimensions, getDimension())) {
-                this.dimensions = null;
-            } else {
-                this.dimensions = dimensions.clone();
-            }
-            return this;
-        }
-
-        /**
-         * Returns the extent of the modified grid geometry. This method is more efficient than
-         * {@link #build()} if only the grid extent is desired instead than the full grid geometry.
-         *
-         * @return the modified grid geometry extent.
-         */
-        public GridExtent extent() {
-            GridExtent e = (subgrid != null) ? subgrid.extent : getExtent();
-            if (dimensions != null) {
-                e = e.reduce(dimensions);
-            }
-            return e;
-        }
-
-        /**
-         * Builds a grid geometry with the configuration specified by the other methods in this {@code Modifier} class.
-         *
-         * @return the modified grid geometry. May be the enclosing geometry if no change apply.
-         */
-        public GridGeometry build() {
-            GridGeometry gg = GridGeometry.this;
-            String cause = null;
-            try {
-                cause = "subgrid";
-                if (subgrid != null) {
-                    final GridExtent    slice        = subgrid.extent;
-                    final MathTransform toSubsampled = subgrid.toSubsampled;
-                    if (toSubsampled != null || slice != extent) {
-                        gg = new GridGeometry(gg, slice, toSubsampled);
-                    }
-                }
-                cause = "dimensions";
-                if (dimensions != null) {
-                    gg = new GridGeometry(gg, dimensions);
-                }
-            } catch (FactoryException | TransformException e) {
-                throw new IllegalGridGeometryException(e, cause);
-            }
-            return gg;
-        }
+    public GridDerivation derive() {
+        return new GridDerivation(this);
     }
 
     /**
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java
index ffc6eb3..36da41b 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java
@@ -69,6 +69,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CanNotMapToGridDimensions = 12;
 
         /**
+         * Can not set this derived grid property after a call to “{0}” method.
+         */
+        public static final short CanNotSetDerivedGridProperty_1 = 32;
+
+        /**
          * Can not simplify transfer function of sample dimension “{0}”.
          */
         public static final short CanNotSimplifyTransferFunction_1 = 19;
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties
index 157b261..b2e3fc3 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties
@@ -21,6 +21,7 @@
 #
 CanNotEnumerateValuesInRange_1    = Can not enumerate values in the {0} range.
 CanNotMapToGridDimensions         = Some envelope dimensions can not be mapped to grid dimensions.
+CanNotSetDerivedGridProperty_1    = Can not set this derived grid property after a call to \u201c{0}\u201d method.
 CanNotSimplifyTransferFunction_1  = Can not simplify transfer function of sample dimension \u201c{0}\u201d.
 CategoryRangeOverlap_4            = The two categories \u201c{0}\u201d and \u201c{2}\u201d have overlapping ranges: {1} and {3} respectively.
 GridCoordinateOutsideCoverage_4   = Indices ({3}) are outside grid coverage. The value at dimension {0} shall be between {1,number} and {2,number} inclusive.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties
index ba0f80c..1fcfe9b 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties
@@ -26,6 +26,7 @@
 #
 CanNotEnumerateValuesInRange_1    = Ne peut pas \u00e9num\u00e9rer les valeurs dans la plage {0}.
 CanNotMapToGridDimensions         = Certaines dimensions de l\u2019enveloppe ne correspondent pas \u00e0 des dimensions de la grille.
+CanNotSetDerivedGridProperty_1    = Ne peut pas d\u00e9finir cette propri\u00e9t\u00e9 de la grille d\u00e9riv\u00e9e apr\u00e8s un appel \u00e0 la m\u00e9thode \u00ab\u202f{0}\u202f\u00bb.
 CanNotSimplifyTransferFunction_1  = Ne peut pas simplifier la fonction de transfert de la dimension d\u2019\u00e9chantillonnage \u00ab\u202f{0}\u202f\u00bb.
 CategoryRangeOverlap_4            = Les deux cat\u00e9gories \u00ab\u202f{0}\u202f\u00bb et \u00ab\u202f{2}\u202f\u00bb ont des plages de valeurs qui se chevauchent\u2008: {1} et {3} respectivement.
 GridCoordinateOutsideCoverage_4   = Les indices ({3}) sont en dehors du domaine de la grille. La valeur \u00e0 la dimension {0} doit \u00eatre entre {1,number} et {2,number} inclusivement.
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
similarity index 51%
copy from core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
copy to core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
index 7c51585..109d334 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
@@ -23,7 +23,6 @@ import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.matrix.Matrix2;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.operation.matrix.Matrix4;
-import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 import org.apache.sis.geometry.DirectPosition2D;
@@ -35,214 +34,23 @@ import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
 import static org.apache.sis.test.ReferencingAssert.*;
+import static org.apache.sis.coverage.grid.GridGeometryTest.assertExtentEquals;
 
 
 /**
- * Tests the {@link GridGeometry} implementation.
+ * Tests the {@link GridDerivation} implementation.
  *
- * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
  */
-@DependsOn(GridExtentTest.class)
-public final strictfp class GridGeometryTest extends TestCase {
+@DependsOn(GridGeometryTest.class)
+public final strictfp class GridDerivationTest extends TestCase {
     /**
-     * Verifies grid extent coordinates.
-     */
-    private static void assertExtentEquals(final long[] low, final long[] high, final GridExtent extent) {
-        assertArrayEquals("extent.low",  low,  extent.getLow() .getCoordinateValues());
-        assertArrayEquals("extent.high", high, extent.getHigh().getCoordinateValues());
-    }
-
-    /**
-     * Tests construction with an identity transform mapping pixel corner.
-     */
-    @Test
-    public void testFromPixelCorner() {
-        final long[]         low     = new long[] {100, 300, 3, 6};
-        final long[]         high    = new long[] {200, 400, 4, 7};
-        final GridExtent    extent   = new GridExtent(null, low, high, true);
-        final MathTransform identity = MathTransforms.identity(4);
-        final GridGeometry  grid     = new GridGeometry(extent, PixelInCell.CELL_CORNER, identity, null);
-        /*
-         * Verify properties that should be stored "as-is".
-         */
-        final MathTransform trCorner = grid.getGridToCRS(PixelInCell.CELL_CORNER);
-        assertSame("gridToCRS", identity, trCorner);
-        assertExtentEquals(low, high, grid.getExtent());
-        /*
-         * Verify computed math transform.
-         */
-        final MathTransform trCenter = grid.getGridToCRS(PixelInCell.CELL_CENTER);
-        assertNotSame(trCenter, trCorner);
-        assertFalse ("gridToCRS.isIdentity",          trCenter.isIdentity());
-        assertEquals("gridToCRS.sourceDimensions", 4, trCenter.getSourceDimensions());
-        assertEquals("gridToCRS.targetDimensions", 4, trCenter.getTargetDimensions());
-        assertMatrixEquals("gridToCRS", Matrices.create(5, 5, new double[] {
-                1, 0, 0, 0, 0.5,
-                0, 1, 0, 0, 0.5,
-                0, 0, 1, 0, 0.5,
-                0, 0, 0, 1, 0.5,
-                0, 0, 0, 0, 1}), MathTransforms.getMatrix(trCenter), STRICT);
-        /*
-         * Verify the envelope, which should have been computed using the given math transform as-is.
-         */
-        assertEnvelopeEquals(new GeneralEnvelope(
-                new double[] {100, 300, 3, 6},
-                new double[] {201, 401, 5, 8}), grid.getEnvelope(), STRICT);
-        /*
-         * Verify other computed properties.
-         */
-        assertArrayEquals("resolution", new double[] {1, 1, 1, 1}, grid.getResolution(false), STRICT);
-        assertTrue("isConversionLinear", grid.isConversionLinear(0, 1, 2, 3));
-    }
-
-    /**
-     * Tests construction with an identity transform mapping pixel center.
-     * This results a 0.5 pixel shifts in the "real world" envelope.
-     */
-    @Test
-    public void testFromPixelCenter() {
-        final long[]        low      = new long[] { 0,   0, 2};
-        final long[]        high     = new long[] {99, 199, 4};
-        final GridExtent    extent   = new GridExtent(null, low, high, true);
-        final MathTransform identity = MathTransforms.identity(3);
-        final GridGeometry  grid     = new GridGeometry(extent, PixelInCell.CELL_CENTER, identity, null);
-        /*
-         * Verify properties that should be stored "as-is".
-         */
-        final MathTransform trCenter = grid.getGridToCRS(PixelInCell.CELL_CENTER);
-        assertSame("gridToCRS", identity, trCenter);
-        assertExtentEquals(low, high, grid.getExtent());
-        /*
-         * Verify computed math transform.
-         */
-        final MathTransform trCorner = grid.getGridToCRS(PixelInCell.CELL_CORNER);
-        assertNotSame(trCenter, trCorner);
-        assertFalse ("gridToCRS.isIdentity",          trCorner.isIdentity());
-        assertEquals("gridToCRS.sourceDimensions", 3, trCorner.getSourceDimensions());
-        assertEquals("gridToCRS.targetDimensions", 3, trCorner.getTargetDimensions());
-        assertMatrixEquals("gridToCRS", new Matrix4(
-                1, 0, 0, -0.5,
-                0, 1, 0, -0.5,
-                0, 0, 1, -0.5,
-                0, 0, 0,  1), MathTransforms.getMatrix(trCorner), STRICT);
-        /*
-         * Verify the envelope, which should have been computed using the math transform shifted by 0.5.
-         */
-        assertEnvelopeEquals(new GeneralEnvelope(
-                new double[] {-0.5,  -0.5, 1.5},
-                new double[] {99.5, 199.5, 4.5}), grid.getEnvelope(), STRICT);
-        /*
-         * Verify other computed properties.
-         */
-        assertArrayEquals("resolution", new double[] {1, 1, 1}, grid.getResolution(false), STRICT);
-        assertTrue("isConversionLinear", grid.isConversionLinear(0, 1, 2));
-    }
-
-    /**
-     * Tests the {@link GridGeometry#GridGeometry(GridGeometry, GridExtent, MathTransform)} constructor.
-     *
-     * @throws TransformException if an error occurred while using the "grid to CRS" transform.
-     */
-    @Test
-    public void testFromOther() throws TransformException {
-        long[]        low       = new long[] {  1,   3, 2};
-        long[]        high      = new long[] {101, 203, 4};
-        GridExtent    extent    = new GridExtent(null, low, high, false);
-        MathTransform gridToCRS = MathTransforms.translation(5, 7, 8);
-        GridGeometry  grid      = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS, null);
-
-        low    = new long[] { 11,  35, 20};
-        high   = new long[] {120, 250, 39};
-        extent = new GridExtent(null, low, high, false);
-        grid   = new GridGeometry(grid, extent, MathTransforms.scale(2, 1, 3));
-        assertSame(extent, grid.getExtent());
-        assertMatrixEquals("gridToCRS", new Matrix4(
-                2, 0, 0, 5,
-                0, 1, 0, 7,     // Combination of above scales (diagonal) and translation (last column).
-                0, 0, 3, 8,
-                0, 0, 0, 1), MathTransforms.getMatrix(grid.getGridToCRS(PixelInCell.CELL_CENTER)), STRICT);
-    }
-
-    /**
-     * Tests construction from a <cite>grid to CRS</cite> having a 0.5 pixel translation.
-     * This translation happens in transform mapping <cite>pixel center</cite> when the
-     * corresponding <cite>pixel corner</cite> transformation is identity.
-     */
-    @Test
-    public void testShifted() {
-        final long[]        low      = new long[] {100, 300};
-        final long[]        high     = new long[] {200, 400};
-        final GridExtent    extent   = new GridExtent(null, low, high, true);
-        final MathTransform identity = MathTransforms.linear(new Matrix3(
-                1, 0, 0.5,
-                0, 1, 0.5,
-                0, 0, 1));
-        final GridGeometry grid = new GridGeometry(extent, PixelInCell.CELL_CENTER, identity, null);
-        assertTrue("gridToCRS.isIdentity", grid.getGridToCRS(PixelInCell.CELL_CORNER).isIdentity());
-    }
-
-    /**
-     * Tests construction with a non-linear component in the transform.
-     */
-    @Test
-    public void testNonLinear() {
-        final GridExtent extent = new GridExtent(
-                new DimensionNameType[] {
-                    DimensionNameType.COLUMN,
-                    DimensionNameType.ROW,
-                    DimensionNameType.VERTICAL,
-                    DimensionNameType.TIME
-                },
-                new long[] {  0,   0, 2, 6},
-                new long[] {100, 200, 3, 9}, false);
-        final MathTransform horizontal = MathTransforms.linear(new Matrix3(
-                0.5, 0,    12,
-                0,   0.25, -2,
-                0,   0,     1));
-        final MathTransform vertical  = MathTransforms.interpolate(null, new double[] {1, 2, 4, 10});
-        final MathTransform temporal  = MathTransforms.linear(3600, 60);
-        final MathTransform gridToCRS = MathTransforms.compound(horizontal, vertical, temporal);
-        final GridGeometry  grid      = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS, null);
-        assertArrayEquals("resolution", new double[] {0.5, 0.25,        6.0, 3600}, grid.getResolution(true),  STRICT);
-        assertArrayEquals("resolution", new double[] {0.5, 0.25, Double.NaN, 3600}, grid.getResolution(false), STRICT);
-        assertFalse("isConversionLinear", grid.isConversionLinear(0, 1, 2, 3));
-        assertTrue ("isConversionLinear", grid.isConversionLinear(0, 1,    3));
-    }
-
-    /**
-     * Tests the construction from a geospatial envelope.
-     */
-    @Test
-    public void testFromGeospatialEnvelope() {
-        final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84_φλ);
-        envelope.setRange(0, -70.001, +80.002);
-        envelope.setRange(1,   4.997,  15.003);
-        final MathTransform gridToCRS = MathTransforms.linear(new Matrix3(
-            0,   0.5, -90,
-            0.5, 0,  -180,
-            0,   0,     1));
-        final GridGeometry grid = new GridGeometry(PixelInCell.CELL_CORNER, gridToCRS, envelope, GridRoundingMode.NEAREST);
-        assertExtentEquals(
-                new long[] {370, 40},
-                new long[] {389, 339}, grid.getExtent());
-        assertEnvelopeEquals(new GeneralEnvelope(
-                new double[] {-70,  5},
-                new double[] {+80, 15}), grid.getEnvelope(), STRICT);
-        assertArrayEquals("resolution", new double[] {0.5, 0.5}, grid.getResolution(false), STRICT);
-        assertMatrixEquals("gridToCRS", new Matrix3(
-                0,   0.5, -89.75,
-                0.5, 0,  -179.75,
-                0,   0,     1), MathTransforms.getMatrix(grid.getGridToCRS(PixelInCell.CELL_CENTER)), STRICT);
-    }
-
-    /**
-     * Tests {@link GridGeometry.Modifier#subgrid(Envelope, double...)}.
+     * Tests {@link GridDerivation#subgrid(Envelope, double...)}.
      */
     @Test
-    @DependsOnMethod("testFromGeospatialEnvelope")
     public void testSubExtent() {
         GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84_3D);
         envelope.setRange(0, -80, 120);
@@ -269,10 +77,10 @@ public final strictfp class GridGeometryTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridGeometry.Modifier#subgrid(Envelope, double...)} with a non-linear "grid to CRS" transform.
+     * Tests {@link GridDerivation#subgrid(Envelope, double...)} with a non-linear "grid to CRS" transform.
      */
     @Test
-    @DependsOnMethod({"testNonLinear", "testSubExtent"})
+    @DependsOnMethod("testSubExtent")
     public void testSubExtentNonLinear() {
         final GridExtent extent = new GridExtent(
                 new DimensionNameType[] {
@@ -306,12 +114,12 @@ public final strictfp class GridGeometryTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridGeometry.Modifier#subgrid(Envelope, double...)}.
+     * Tests {@link GridDerivation#subgrid(Envelope, double...)}.
      *
      * @throws TransformException if an error occurred during computation.
      */
     @Test
-    @DependsOnMethod({"testFromGeospatialEnvelope", "testSubExtent"})
+    @DependsOnMethod("testSubExtent")
     public void testSubgrid() throws TransformException {
         final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84_φλ);
         envelope.setRange(0, -70, +80);
@@ -358,7 +166,7 @@ public final strictfp class GridGeometryTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridGeometry.Modifier#slice(DirectPosition)}.
+     * Tests {@link GridDerivation#slice(DirectPosition)}.
      */
     @Test
     public void testSlice() {
@@ -391,7 +199,7 @@ public final strictfp class GridGeometryTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridGeometry.Modifier#reduce(int...)}.
+     * Tests {@link GridDerivation#reduce(int...)}.
      */
     @Test
     public void testReduce() {
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
index 7c51585..7907a22 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
@@ -20,16 +20,12 @@ import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.referencing.operation.matrix.Matrix2;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.operation.matrix.Matrix4;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.crs.HardCodedCRS;
-import org.apache.sis.geometry.DirectPosition2D;
-import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.geometry.GeneralEnvelope;
-import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
@@ -50,7 +46,7 @@ public final strictfp class GridGeometryTest extends TestCase {
     /**
      * Verifies grid extent coordinates.
      */
-    private static void assertExtentEquals(final long[] low, final long[] high, final GridExtent extent) {
+    static void assertExtentEquals(final long[] low, final long[] high, final GridExtent extent) {
         assertArrayEquals("extent.low",  low,  extent.getLow() .getCoordinateValues());
         assertArrayEquals("extent.high", high, extent.getHigh().getCoordinateValues());
     }
@@ -237,193 +233,4 @@ public final strictfp class GridGeometryTest extends TestCase {
                 0.5, 0,  -179.75,
                 0,   0,     1), MathTransforms.getMatrix(grid.getGridToCRS(PixelInCell.CELL_CENTER)), STRICT);
     }
-
-    /**
-     * Tests {@link GridGeometry.Modifier#subgrid(Envelope, double...)}.
-     */
-    @Test
-    @DependsOnMethod("testFromGeospatialEnvelope")
-    public void testSubExtent() {
-        GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84_3D);
-        envelope.setRange(0, -80, 120);
-        envelope.setRange(1, -12,  21);
-        envelope.setRange(2,  10,  25);
-        final MathTransform gridToCRS = MathTransforms.linear(new Matrix4(
-            0,   0.5, 0,  -90,
-            0.5, 0,   0, -180,
-            0,   0,   2,    3,
-            0,   0,   0,    1));
-        final GridGeometry grid = new GridGeometry(PixelInCell.CELL_CORNER, gridToCRS, envelope, GridRoundingMode.NEAREST);
-        assertExtentEquals(
-                new long[] {336,  20,  4},
-                new long[] {401, 419, 10}, grid.getExtent());
-        /*
-         * Set the region of interest as a two-dimensional envelope. The vertical dimension is omitted.
-         * The result should be that all grid indices in the vertical dimension are kept unchanged.
-         */
-        envelope = new GeneralEnvelope(HardCodedCRS.WGS84);
-        envelope.setRange(0, -70.001, +80.002);
-        envelope.setRange(1,   4.997,  15.003);
-        assertExtentEquals(new long[] {370,  40,  4},
-                           new long[] {389, 339, 10}, grid.derive().subgrid(envelope).extent());
-    }
-
-    /**
-     * Tests {@link GridGeometry.Modifier#subgrid(Envelope, double...)} with a non-linear "grid to CRS" transform.
-     */
-    @Test
-    @DependsOnMethod({"testNonLinear", "testSubExtent"})
-    public void testSubExtentNonLinear() {
-        final GridExtent extent = new GridExtent(
-                new DimensionNameType[] {
-                    DimensionNameType.COLUMN,
-                    DimensionNameType.ROW,
-                    DimensionNameType.VERTICAL
-                },
-                new long[] {  0,  0, 2},
-                new long[] {180, 90, 5}, false);
-        final MathTransform linear = MathTransforms.linear(new Matrix4(
-                2, 0, 0, -180,
-                0, 2, 0,  -90,
-                0, 0, 5,   10,
-                0, 0, 0,    1));
-        final MathTransform latitude  = MathTransforms.interpolate(new double[] {0, 20, 50, 70, 90}, new double[] {-90, -45, 0, 45, 90});
-        final MathTransform gridToCRS = MathTransforms.concatenate(linear, MathTransforms.passThrough(1, latitude, 1));
-        final GridGeometry  grid      = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS, HardCodedCRS.WGS84_3D);
-        /*
-         * Following tests is similar to the one executed in testSubExtent(). Expected values are only
-         * anti-regression values, except the vertical range which is expected to cover all cells. The
-         * main purpose of this test is to verify that TransformSeparator has been able to extract the
-         * two-dimensional transform despite its non-linear component.
-         */
-        final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84);
-        envelope.setRange(0, -70.001, +80.002);
-        envelope.setRange(1,  -4.997,  15.003);
-        final GridExtent actual = grid.derive().subgrid(envelope).extent();
-        assertEquals(extent.getAxisType(0), actual.getAxisType(0));
-        assertExtentEquals(new long[] { 56, 69, 2},
-                           new long[] {130, 73, 4}, actual);
-    }
-
-    /**
-     * Tests {@link GridGeometry.Modifier#subgrid(Envelope, double...)}.
-     *
-     * @throws TransformException if an error occurred during computation.
-     */
-    @Test
-    @DependsOnMethod({"testFromGeospatialEnvelope", "testSubExtent"})
-    public void testSubgrid() throws TransformException {
-        final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84_φλ);
-        envelope.setRange(0, -70, +80);
-        envelope.setRange(1,   5,  15);
-        final MathTransform gridToCRS = MathTransforms.linear(new Matrix3(
-            0,   0.5, -90,
-            0.5, 0,  -180,
-            0,   0,     1));
-        GridGeometry grid = new GridGeometry(PixelInCell.CELL_CORNER, gridToCRS, envelope, GridRoundingMode.NEAREST);
-        assertExtentEquals(new long[] {370, 40}, new long[] {389, 339}, grid.getExtent());
-        assertEnvelopeEquals(envelope, grid.getEnvelope(), STRICT);
-        /*
-         * Set a sub-region. The grid extent and "grid to CRS" transform shall be adjusted
-         * in such a way that envelope computed from the new grid geometry is the same.
-         */
-        envelope.setRange(0, -50, +30);
-        envelope.setRange(1,   8,  12);
-        grid = grid.derive().subgrid(envelope, 1, 2).build();
-        assertExtentEquals(new long[] {94, 40}, new long[] {95, 119}, grid.getExtent());
-        assertEnvelopeEquals(envelope, grid.getEnvelope(), STRICT);
-        assertMatrixEquals("gridToCRS", new Matrix3(
-                  0, 1,  -90,
-                  2, 0, -180,
-                  0, 0,    1), MathTransforms.getMatrix(grid.getGridToCRS(PixelInCell.CELL_CORNER)), STRICT);
-        /*
-         * A sub-region again but with a requested resolution which is not a divisor of the actual resolution.
-         * It will force GridGeometry to adjust the translation term to compensate. We verify that the adustment
-         * is correct by verifying that we still get the same envelope.
-         */
-        grid = grid.derive().subgrid(envelope, 3, 2).build();
-        assertExtentEquals(new long[] {94, 13}, new long[] {95, 39}, grid.getExtent());
-        assertEnvelopeEquals(envelope, grid.getEnvelope(), STRICT);
-        MathTransform cornerToCRS = grid.getGridToCRS(PixelInCell.CELL_CORNER);
-        assertMatrixEquals("gridToCRS", new Matrix3(
-                  0, 3,  -89,
-                  2, 0, -180,
-                  0, 0,    1), MathTransforms.getMatrix(cornerToCRS), STRICT);
-
-        DirectPosition2D src = new DirectPosition2D();
-        DirectPosition2D tgt = new DirectPosition2D();
-        DirectPosition2D exp = new DirectPosition2D();
-        src.x = 94; src.y = 13; exp.x = -50; exp.y =  8; assertEquals("Lower corner", exp, cornerToCRS.transform(src, tgt));
-        src.x = 96; src.y = 40; exp.x = +31; exp.y = 12; assertEquals("Upper corner", exp, cornerToCRS.transform(src, tgt));
-    }
-
-    /**
-     * Tests {@link GridGeometry.Modifier#slice(DirectPosition)}.
-     */
-    @Test
-    public void testSlice() {
-        final GridGeometry grid = new GridGeometry(
-                new GridExtent(null, new long[] {336, 20, 4}, new long[] {401, 419, 10}, true),
-                PixelInCell.CELL_CORNER, MathTransforms.linear(new Matrix4(
-                        0,   0.5, 0,  -90,
-                        0.5, 0,   0, -180,
-                        0,   0,   2,    3,
-                        0,   0,   0,    1)), HardCodedCRS.WGS84_3D);
-        /*
-         * There is two ways to ask for a slice. The first way is to set some coordinates to NaN.
-         */
-        GridGeometry slice = grid.derive().slice(new GeneralDirectPosition(Double.NaN, Double.NaN, 15)).build();
-        assertNotSame(grid, slice);
-        assertSame("gridToCRS", grid.gridToCRS, slice.gridToCRS);
-        final long[] expectedLow  = {336,  20, 6};
-        final long[] expectedHigh = {401, 419, 6};
-        assertExtentEquals(expectedLow, expectedHigh, slice.getExtent());
-        /*
-         * Same test, but using a one-dimensional slice point instead than NaN values.
-         * Opportunistically use different units for testing conversions.
-         */
-        GeneralDirectPosition p = new GeneralDirectPosition(HardCodedCRS.ELLIPSOIDAL_HEIGHT_cm);
-        p.setOrdinate(0, 1500);
-        slice = grid.derive().slice(p).build();
-        assertNotSame(grid, slice);
-        assertSame("gridToCRS", grid.gridToCRS, slice.gridToCRS);
-        assertExtentEquals(expectedLow, expectedHigh, slice.getExtent());
-    }
-
-    /**
-     * Tests {@link GridGeometry.Modifier#reduce(int...)}.
-     */
-    @Test
-    public void testReduce() {
-        final GridGeometry grid = new GridGeometry(
-                new GridExtent(null, new long[] {336, 20, 4}, new long[] {401, 419, 10}, true),
-                PixelInCell.CELL_CORNER, MathTransforms.linear(new Matrix4(
-                        0,   0.5, 0,  -90,
-                        0.5, 0,   0, -180,
-                        0,   0,   2,    3,
-                        0,   0,   0,    1)), HardCodedCRS.GEOID_3D);
-        /*
-         * Tests on the two first dimensions.
-         */
-        GridGeometry reduced = grid.derive().reduce(0, 1).build();
-        assertNotSame(grid, reduced);
-        assertExtentEquals(new long[] {336, 20}, new long[] {401, 419}, reduced.getExtent());
-        assertSame("CRS", HardCodedCRS.WGS84, reduced.getCoordinateReferenceSystem());
-        assertArrayEquals("resolution", new double[] {0.5, 0.5}, reduced.getResolution(false), STRICT);
-        assertMatrixEquals("gridToCRS", new Matrix3(
-                  0, 0.5,  -90,
-                  0.5, 0, -180,
-                  0,   0,    1), MathTransforms.getMatrix(reduced.getGridToCRS(PixelInCell.CELL_CORNER)), STRICT);
-        /*
-         * Tests on the last dimension.
-         */
-        reduced = grid.derive().reduce(2).build();
-        assertNotSame(grid, reduced);
-        assertExtentEquals(new long[] {4}, new long[] {10}, reduced.getExtent());
-        assertSame("CRS", HardCodedCRS.GRAVITY_RELATED_HEIGHT, reduced.getCoordinateReferenceSystem());
-        assertArrayEquals("resolution", new double[] {2}, reduced.getResolution(false), STRICT);
-        assertMatrixEquals("gridToCRS", new Matrix2(
-                  2, 3,
-                  0, 1), MathTransforms.getMatrix(reduced.getGridToCRS(PixelInCell.CELL_CORNER)), STRICT);
-    }
 }
diff --git a/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java b/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
index 1aac8e2..2a8b4ee 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
@@ -34,6 +34,7 @@ import org.junit.BeforeClass;
     org.apache.sis.coverage.grid.PixelTranslationTest.class,
     org.apache.sis.coverage.grid.GridExtentTest.class,
     org.apache.sis.coverage.grid.GridGeometryTest.class,
+    org.apache.sis.coverage.grid.GridDerivationTest.class,
     org.apache.sis.coverage.grid.GridChangeTest.class,
     org.apache.sis.coverage.CategoryTest.class,
     org.apache.sis.coverage.CategoryListTest.class,


Mime
View raw message