sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 07/31: First draft of a port of GridGeometry class, for grid coverage (raster) support.
Date Mon, 18 Jun 2018 09:44:18 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 20423a32ff2c50dbd652047791d29c3df88e7e13
Author: Martin Desruisseaux <desruisseaux@apache.org>
AuthorDate: Tue May 29 16:58:56 2018 +0000

    First draft of a port of GridGeometry class, for grid coverage (raster) support.
    
    
    git-svn-id: https://svn.apache.org/repos/asf/sis/branches/JDK8@1832460 13f79535-47bb-0310-9956-ffa450edef68
---
 core/sis-raster/pom.xml                            |   2 +-
 .../sis/coverage/grid/GridCoordinatesView.java     | 142 +++++
 .../org/apache/sis/coverage/grid/GridExtent.java   | 322 ++++++++++++
 .../org/apache/sis/coverage/grid/GridGeometry.java | 582 +++++++++++++++++++++
 .../grid/IncompleteGridGeometryException.java      |  70 +++
 .../apache/sis/coverage/grid/PixelTranslation.java | 358 +++++++++++++
 .../raster => coverage/grid}/package-info.java     |  15 +-
 .../java/org/apache/sis/image/DefaultIterator.java |   4 +-
 .../java/org/apache/sis/image/PixelIterator.java   |   4 +-
 .../java/org/apache/sis/image/TransferType.java    |   4 +-
 .../apache/sis/image/WritablePixelIterator.java    |   4 +-
 .../java/org/apache/sis/image/package-info.java    |   4 +-
 .../org/apache/sis/internal/raster/Resources.java  |  30 +-
 .../sis/internal/raster/Resources.properties       |   1 +
 .../sis/internal/raster/Resources_fr.properties    |   1 +
 .../apache/sis/internal/raster/package-info.java   |   4 +-
 16 files changed, 1522 insertions(+), 25 deletions(-)

diff --git a/core/sis-raster/pom.xml b/core/sis-raster/pom.xml
index be8cda6..cc6b8fa 100644
--- a/core/sis-raster/pom.xml
+++ b/core/sis-raster/pom.xml
@@ -101,7 +101,7 @@
   <dependencies>
     <dependency>
       <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-utility</artifactId>
+      <artifactId>sis-referencing</artifactId>
       <version>${project.version}</version>
     </dependency>
 
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
new file mode 100644
index 0000000..bd338a3
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.coverage.grid;
+
+import java.util.Arrays;
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+
+// Branch-dependent imports
+import org.opengis.coverage.grid.GridCoordinates;
+
+
+/**
+ * A view over the low or high grid envelope coordinates.
+ * This is not a general-purpose grid coordinates since it assumes a {@link GridExtent} coordinates layout.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class GridCoordinatesView implements GridCoordinates {
+    /**
+     * A reference to the coordinate array of the enclosing grid envelope.
+     */
+    private final int[] ordinates;
+
+    /**
+     * Index of the first value in the {@link #ordinates} array.
+     * This is 0 for low values, or {@link #getDimension()} for high values.
+     */
+    private final int offset;
+
+    /**
+     * Creates a new view over the low or high coordinates.
+     */
+    GridCoordinatesView(final int[] ordinates, final int offset) {
+        this.ordinates = ordinates;
+        this.offset = offset;
+    }
+
+    /**
+     * Returns the number of dimension.
+     */
+    @Override
+    public final int getDimension() {
+        return ordinates.length >>> 1;
+    }
+
+    /**
+     * Returns all coordinate values.
+     */
+    @Override
+    public final int[] getCoordinateValues() {
+        return Arrays.copyOfRange(ordinates, offset, offset + getDimension());
+    }
+
+    /**
+     * Returns the coordinate value for the specified dimension.
+     */
+    @Override
+    public final int getCoordinateValue(final int index) {
+        ArgumentChecks.ensureValidIndex(getDimension(), index);
+        return ordinates[offset + index];
+    }
+
+    /**
+     * Do not allow modification of grid coordinates since they are backed by {@link GridExtent}.
+     */
+    @Override
+    public void setCoordinateValue(final int index, int value) {
+        throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, "GridCoordinates"));
+    }
+
+    /**
+     * Returns a string representation of this grid coordinates for debugging purpose.
+     */
+    @Debug
+    @Override
+    public final String toString() {
+        return "GridCoordinates".concat(Arrays.toString(getCoordinateValues()));
+    }
+
+    /**
+     * Returns a hash code value for this object.
+     */
+    @Override
+    public final int hashCode() {
+        int code = -3;                              // Arbitrary seed for differentiating from Arrays.hashCode(int[]).
+        final int end = offset + getDimension();
+        for (int i=offset; i<end; i++) {
+            code = 31 * code + ordinates[i];
+        }
+        return code;
+    }
+
+    /**
+     * Compares this grid coordinates with the specified object for equality.
+     *
+     * @param  object  the object to compares with this grid coordinates.
+     * @return {@code true} if the given object is equal to this grid coordinates.
+     */
+    @Override
+    public final boolean equals(final Object object) {
+        if (object == this) {                           // Slight optimization.
+            return true;
+        }
+        /*
+         * We do not require the exact same class because we want to accept
+         * immutable grid coordinates as equal to mutable grid coordinates.
+         */
+        if (object instanceof GridCoordinatesView) {
+            final GridCoordinatesView that = (GridCoordinatesView) object;
+            final int dimension = getDimension();
+            if (dimension == that.getDimension()) {
+                // TODO: use Arrays.equals(...) with JDK9 instead.
+                for (int i=0; i<dimension; i++) {
+                    if (ordinates[offset + i] != that.ordinates[that.offset + i]) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+}
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
new file mode 100644
index 0000000..816a62f
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -0,0 +1,322 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.coverage.grid;
+
+import java.util.Arrays;
+import java.io.Serializable;
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.raster.Resources;
+
+// Branch-dependent imports
+import org.opengis.coverage.grid.GridEnvelope;
+import org.opengis.coverage.grid.GridCoordinates;
+
+
+/**
+ * A range of grid coverage coordinates, also known as "grid envelope".
+ * {@code GridExtent} instances are unmodifiable,
+ * so they can be shared between different {@link GridGeometry} instances.
+ *
+ * <p><b>CAUTION:</b>
+ * ISO 19123 defines {@linkplain #getHigh high} coordinates as <strong>inclusive</strong>.
+ * We follow this specification for all getters methods, but developer should keep in mind
+ * that this is the opposite of Java2D usage where {@link java.awt.Rectangle} maximal values are exclusive.
+ * When the context is ambiguous, an explicit {@code isHighIncluded} argument is required.</p>
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public class GridExtent implements GridEnvelope, Serializable {
+    /**
+     * Serial number for inter-operability with different versions.
+     */
+    private static final long serialVersionUID = -4717353677844056017L;
+
+    /**
+     * Minimum and maximum grid ordinates. The first half contains minimum ordinates (inclusive),
+     * while the last half contains maximum ordinates (<strong>inclusive</strong>). Note that the
+     * later is the opposite of Java2D usage but conform to ISO specification.
+     */
+    private final int[] ordinates;
+
+    /**
+     * Creates a new array of coordinates with the given number of dimensions.
+     *
+     * @throws IllegalArgumentException if the given number of dimensions is excessive.
+     */
+    private static int[] allocate(final int dimension) throws IllegalArgumentException {
+        if (dimension > Integer.MAX_VALUE / 2) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension));
+        }
+        return new int[dimension << 1];
+    }
+
+    /**
+     * Checks if ordinate values in the low part are less than or
+     * equal to the corresponding ordinate value in the high part.
+     *
+     * @throws IllegalArgumentException if a coordinate value in the low part is
+     *         greater than the corresponding coordinate value in the high part.
+     */
+    private static void checkCoherence(final int[] ordinates) throws IllegalArgumentException {
+        final int dimension = ordinates.length >>> 1;
+        for (int i=0; i<dimension; i++) {
+            final int lower = ordinates[i];
+            final int upper = ordinates[i + dimension];
+            if (lower > upper) {
+                throw new IllegalArgumentException(Resources.format(
+                        Resources.Keys.IllegalGridEnvelope_3, i, lower, upper));
+            }
+        }
+    }
+
+    /**
+     * Creates an initially empty grid envelope with the given number of dimensions.
+     * All grid coordinate values are initialized to zero. This constructor is private
+     * since {@code GridExtent} coordinate values can not be modified by public API.
+     *
+     * @param dimension  number of dimensions.
+     */
+    private GridExtent(final int dimension) {
+        ordinates = allocate(dimension);
+    }
+
+    /**
+     * Constructs a new grid envelope set to the specified coordinates.
+     * The given arrays contain a minimum (inclusive) and maximum value for each dimension of the grid coverage.
+     * The lowest valid grid coordinates are often zero, but this is not mandatory.
+     * As a convenience for this common case, a null {@code low} array means that all low coordinates are zero.
+     *
+     * @param  low   the valid minimum grid coordinate (always inclusive), or {@code null} for all zeros.
+     * @param  high  the valid maximum grid coordinate, inclusive or exclusive depending on the next argument.
+     * @param  isHighIncluded  {@code true} if the {@code high} values are inclusive (as in ISO 19123 specification),
+     *         or {@code false} if they are exclusive (as in Java2D usage).
+     *         This argument does not apply to {@code low} values, which are always inclusive.
+     * @throws IllegalArgumentException if a coordinate value in the low part is
+     *         greater than the corresponding coordinate value in the high part.
+     *
+     * @see #getLow()
+     * @see #getHigh()
+     */
+    public GridExtent(final int[] low, final int[] high, final boolean isHighIncluded) {
+        ArgumentChecks.ensureNonNull("high", high);
+        final int dimension = high.length;
+        if (low != null && low.length != dimension) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedDimension_2, low.length, dimension));
+        }
+        ordinates = allocate(dimension);
+        if (low != null) {
+            System.arraycopy(low, 0, ordinates, 0, dimension);
+        }
+        System.arraycopy(high, 0, ordinates, dimension, dimension);
+        if (!isHighIncluded) {
+            for (int i=dimension; i < ordinates.length; i++) {
+                ordinates[i]--;
+            }
+        }
+        checkCoherence(ordinates);
+    }
+
+    /**
+     * Creates a new grid envelope as a copy of the given one.
+     *
+     * @param  extent  the grid envelope to copy.
+     * @throws IllegalArgumentException if a coordinate value in the low part is
+     *         greater than the corresponding coordinate value in the high part.
+     */
+    protected GridExtent(final GridEnvelope extent) {
+        ArgumentChecks.ensureNonNull("extent", extent);
+        final int dimension = extent.getDimension();
+        ordinates = allocate(dimension);
+        for (int i=0; i<dimension; i++) {
+            ordinates[i] = extent.getLow(i);
+            ordinates[i + dimension] = extent.getHigh(i);
+        }
+        checkCoherence(ordinates);
+    }
+
+    /**
+     * Returns the given grid envelope as a {@code GridExtent} implementation.
+     * If the given extent is already a {@code GridExtent} instance or is null, then it is returned as-is.
+     * Otherwise a new extent is created using the {@linkplain #GridExtent(GridEnvelope) copy constructor}.
+     *
+     * @param  extent  the grid envelope to cast or copy, or {@code null}.
+     * @return the grid envelope as a {@code GridExtent}, or {@code null} if the given extent was null.
+     */
+    public static GridExtent castOrCopy(final GridEnvelope extent) {
+        if (extent == null || extent instanceof GridExtent) {
+            return (GridExtent) extent;
+        } else {
+            return new GridExtent(extent);
+        }
+    }
+
+    /**
+     * Returns the number of dimensions.
+     *
+     * @return the number of dimensions.
+     */
+    @Override
+    public final int getDimension() {
+        return ordinates.length >>> 1;
+    }
+
+    /**
+     * Returns the valid minimum grid coordinates, inclusive.
+     * The sequence contains a minimum value for each dimension of the grid coverage.
+     *
+     * @return the valid minimum grid coordinates, inclusive.
+     */
+    @Override
+    public GridCoordinates getLow() {
+        return new GridCoordinatesView(ordinates, 0);
+    }
+
+    /**
+     * Returns the valid maximum grid coordinates, <strong>inclusive</strong>.
+     * The sequence contains a maximum value for each dimension of the grid coverage.
+     *
+     * @return the valid maximum grid coordinates, <strong>inclusive</strong>.
+     */
+    @Override
+    public GridCoordinates getHigh() {
+        return new GridCoordinatesView(ordinates, getDimension());
+    }
+
+    /**
+     * Returns the valid minimum inclusive grid coordinate along the specified dimension.
+     *
+     * @param  index  the dimension for which to obtain the coordinate value.
+     * @return the low coordinate value at the given dimension, inclusive.
+     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     *         than the {@linkplain #getDimension() grid dimension}.
+     *
+     * @see #getLow()
+     * @see #getHigh(int)
+     */
+    @Override
+    public int getLow(final int index) {
+        ArgumentChecks.ensureValidIndex(getDimension(), index);
+        return ordinates[index];
+    }
+
+    /**
+     * Returns the valid maximum <strong>inclusive</strong> grid coordinate along the specified dimension.
+     *
+     * @param  index  the dimension for which to obtain the coordinate value.
+     * @return the high coordinate value at the given dimension, <strong>inclusive</strong>.
+     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     *         than the {@linkplain #getDimension() grid dimension}.
+     *
+     * @see #getHigh()
+     * @see #getLow(int)
+     */
+    @Override
+    public int getHigh(final int index) {
+        final int dimension = getDimension();
+        ArgumentChecks.ensureValidIndex(dimension, index);
+        return ordinates[index + dimension];
+    }
+
+    /**
+     * Returns the number of integer grid coordinates along the specified dimension.
+     * This is equal to {@code getHigh(dimension) - getLow(dimension) + 1}.
+     *
+     * @param  index  the dimension for which to obtain the span.
+     * @return the span at the given dimension.
+     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     *         than the {@linkplain #getDimension() grid dimension}.
+     *
+     * @see #getLow(int)
+     * @see #getHigh(int)
+     */
+    @Override
+    public int getSpan(final int index) {
+        final int dimension = getDimension();
+        ArgumentChecks.ensureValidIndex(dimension, index);
+        return ordinates[dimension + index] - ordinates[index] + 1;
+    }
+
+    /**
+     * Returns a new grid envelope that encompass only some dimensions of this grid envelope.
+     * This method copies this grid envelope into a new grid envelope, beginning at dimension
+     * {@code lower} and extending to dimension {@code upper-1} inclusive. Thus the dimension
+     * of the sub grid envelope is {@code upper - lower}.
+     *
+     * @param  lower  the first dimension to copy, inclusive.
+     * @param  upper  the last  dimension to copy, exclusive.
+     * @return the sub grid envelope.
+     * @throws IndexOutOfBoundsException if an index is out of bounds.
+     */
+    public GridExtent subEnvelope(final int lower, final int upper) {
+        final int dimension = getDimension();
+        ArgumentChecks.ensureValidIndexRange(dimension, lower, upper);
+        final int newDim = upper - lower;
+        if (newDim == dimension && getClass() == GridExtent.class) {
+            return this;
+        }
+        final GridExtent sub = new GridExtent(newDim);
+        System.arraycopy(ordinates, lower,           sub.ordinates, 0,      newDim);
+        System.arraycopy(ordinates, lower+dimension, sub.ordinates, newDim, newDim);
+        return sub;
+    }
+
+    /**
+     * Returns a hash value for this grid envelope. This value need not remain
+     * consistent between different implementations of the same class.
+     *
+     * @return a hash value for this grid envelope.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(ordinates) ^ (int) serialVersionUID;
+    }
+
+    /**
+     * Compares the specified object with this grid envelope for equality.
+     *
+     * @param  object  the object to compare with this grid envelope for equality.
+     * @return {@code true} if the given object is equal to this grid envelope.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object instanceof GridExtent) {
+            return Arrays.equals(ordinates, ((GridExtent) object).ordinates);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a string representation of this grid envelope. The returned string
+     * is implementation dependent and is provided for debugging purposes only.
+     */
+    @Debug
+    @Override
+    public String toString() {
+        final int dimension = getDimension();
+        final StringBuilder buffer = new StringBuilder("GridEnvelope").append('[');
+        for (int i=0; i<dimension; i++) {
+            if (i != 0) buffer.append(", ");
+            buffer.append(ordinates[i]).append('…').append(ordinates[i + dimension]);
+        }
+        return buffer.append(']').toString();
+    }
+}
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
new file mode 100644
index 0000000..79ac7cc
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -0,0 +1,582 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.coverage.grid;
+
+import java.util.Objects;
+import java.io.Serializable;
+import java.awt.image.RenderedImage;            // For javadoc only.
+import org.opengis.geometry.Envelope;
+import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.math.MathFunctions;
+import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.ImmutableEnvelope;
+import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.PassThroughTransform;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.Debug;
+
+// Branch-dependent imports
+import org.opengis.coverage.grid.GridEnvelope;
+
+
+/**
+ * Describes the valid extent of grid coordinates and the transform from those grid coordinates
+ * to real world coordinates. Grid geometries contains:
+ *
+ * <ul>
+ *   <li>A <cite>grid envelope</cite> (a.k.a. <cite>"grid extent"</cite>),
+ *       often inferred from the {@link RenderedImage} size.</li>
+ *   <li>A <cite>grid to CRS</cite> {@link MathTransform},
+ *       which can be inferred from the grid envelope and the georeferenced envelope.</li>
+ *   <li>A georeferenced {@link Envelope}, which can be inferred from the grid envelope
+ *       and the <cite>grid to CRS</cite> transform.</li>
+ *   <li>An optional {@link CoordinateReferenceSystem} (CRS) specified as part of the georeferenced envelope.
+ *       This CRS is the target of the <cite>grid to CRS</cite> transform.</li>
+ * </ul>
+ *
+ * All above properties except the CRS should be mandatory, but are allowed to be temporarily absent during
+ * grid coverage construction. Temporarily absent properties are allowed because they may be inferred from
+ * a wider context. For example a grid geometry know nothing about {@link RenderedImage},
+ * but {@code GridCoverage2D} does and may use that information for providing a missing grid envelope.
+ * By default, any request for an undefined property will throw an {@link IncompleteGridGeometryException}.
+ * In order to check if a property is defined, use {@link #isDefined(int)}.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @author  Alessio Fabiani (Geosolutions)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public class GridGeometry implements Serializable {
+    /**
+     * Serial number for inter-operability with different versions.
+     */
+    private static final long serialVersionUID = -954786616001606624L;
+
+    /**
+     * A bitmask to specify the validity of the Coordinate Reference System property.
+     *
+     * @see #isDefined(int)
+     * @see #getCoordinateReferenceSystem()
+     */
+    public static final int CRS = 1;
+
+    /**
+     * A bitmask to specify the validity of the geodetic envelope property.
+     *
+     * @see #isDefined(int)
+     * @see #getEnvelope()
+     */
+    public static final int ENVELOPE = 2;
+
+    /**
+     * A bitmask to specify the validity of the grid envelope property.
+     *
+     * @see #isDefined(int)
+     * @see #getExtent()
+     */
+    public static final int EXTENT = 4;
+
+    /**
+     * A bitmask to specify the validity of the <cite>"grid to CRS"</cite> transform.
+     *
+     * @see #isDefined(int)
+     * @see #getGridToCRS()
+     */
+    public static final int GRID_TO_CRS = 8;
+
+    /**
+     * The valid domain of a grid coverage, or {@code null} if unknown. The lowest valid grid coordinate is zero
+     * for {@link java.awt.image.BufferedImage}, but may be non-zero for arbitrary {@link RenderedImage}.
+     * A grid with 512 cells can have a minimum coordinate of 0 and maximum of 511.
+     */
+    protected final GridExtent extent;
+
+    /**
+     * The geodetic envelope, or {@code null} if unknown. If non-null, this envelope is usually the grid {@link #extent}
+     * {@linkplain #gridToCRS transformed} to real world coordinates. The Coordinate Reference System} (CRS) of this
+     * envelope defines the "real world" CRS of this grid geometry.
+     */
+    protected final ImmutableEnvelope envelope;
+
+    /**
+     * The math transform from grid indices to "real world" coordinates, or {@code null} if unknown.
+     * This math transform is usually affine. It maps {@linkplain PixelInCell#CELL_CENTER pixel center}
+     * to "real world" coordinate using the following line:
+     *
+     * {@preformat java
+     *     DirectPosition aCellIndices = ...:
+     *     DirectPosition aPixelCenter = gridToCRS.transform(pixels, aCellIndices);
+     * }
+     */
+    protected final MathTransform gridToCRS;
+
+    /**
+     * Same transform than {@link #gridToCRS} but from {@linkplain PixelInCell#CELL_CORNER pixel corner}
+     * instead than center. This transform is preferable to {@code gridToCRS} for transforming envelopes.
+     *
+     * @serial This field is serialized because it may be a value specified explicitly at construction time,
+     *         in which case it can be more accurate than a computed value.
+     */
+    private final MathTransform cornerToCRS;
+
+    /**
+     * The resolution in units of the CRS axes.
+     * Computed only when first needed.
+     */
+    private transient double[] resolution;
+
+    /**
+     * Creates a new grid geometry with the same values than the given grid geometry.
+     * This is a copy constructor for subclasses.
+     *
+     * @param other  the other grid geometry to copy.
+     */
+    protected GridGeometry(final GridGeometry other) {
+        extent      = other.extent;
+        gridToCRS   = other.gridToCRS;
+        cornerToCRS = other.cornerToCRS;
+        envelope    = other.envelope;
+        resolution  = other.resolution;
+    }
+
+    /**
+     * Creates a new grid geometry from a grid envelope and a math transform mapping pixel <em>center</em>.
+     *
+     * @param extent     the valid extent of grid coordinates, or {@code null} if unknown.
+     * @param gridToCRS  the math transform which allows for the transformations from grid coordinates
+     *                   ({@linkplain PixelInCell#CELL_CENTER pixel center}) to real world earth coordinates.
+     *                   May be {@code null} if unknown.
+     * @param crs        the coordinate reference system for the "real world" coordinates, or {@code null} if unknown.
+     *                   This CRS is given to the {@linkplain #getEnvelope() envelope}.
+     *
+     * @throws MismatchedDimensionException if the math transform and the CRS do not have consistent dimensions.
+     * @throws TransformException if the math transform can not compute the geospatial envelope from the grid envelope.
+     */
+    public GridGeometry(final GridEnvelope extent, final MathTransform gridToCRS, final CoordinateReferenceSystem crs)
+            throws TransformException
+    {
+        this(extent, PixelInCell.CELL_CENTER, gridToCRS, crs);
+    }
+
+    /**
+     * Creates a new grid geometry from a grid envelope and a math transform mapping pixel center or corner.
+     * This is the most general constructor, the one that gives the maximal control over the grid geometry
+     * to be created.
+     *
+     * @param extent     the valid extent of grid coordinates, or {@code null} if unknown.
+     * @param anchor     {@link PixelInCell#CELL_CENTER} for OGC conventions or
+     *                   {@link PixelInCell#CELL_CORNER} for Java2D/JAI conventions.
+     * @param gridToCRS  the math transform which allows for the transformations from grid coordinates
+     *                   to real world earth coordinates. May be {@code null} if unknown.
+     * @param crs        the coordinate reference system for the "real world" coordinates, or {@code null} if unknown.
+     *                   This CRS is given to the {@linkplain #getEnvelope() envelope}.
+     * @throws MismatchedDimensionException if the math transform and the CRS do not have consistent dimensions.
+     * @throws TransformException if the math transform can not compute the geospatial envelope from the grid envelope.
+     */
+    public GridGeometry(final GridEnvelope extent, final PixelInCell anchor, final MathTransform gridToCRS,
+            final CoordinateReferenceSystem crs) throws TransformException
+    {
+        if (gridToCRS != null) {
+            if (extent != null) {
+                ensureDimensionMatches("extent", extent.getDimension(), gridToCRS.getSourceDimensions());
+            }
+            if (crs != null) {
+                ensureDimensionMatches("crs", crs.getCoordinateSystem().getDimension(), gridToCRS.getTargetDimensions());
+            }
+        }
+        this.extent      = GridExtent.castOrCopy(extent);
+        this.gridToCRS   = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CENTER);
+        this.cornerToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CORNER);
+        GeneralEnvelope env = null;
+        if (extent != null && gridToCRS != null) {
+            final int dimension = extent.getDimension();
+            env = new GeneralEnvelope(dimension);
+            for (int i=0; i<dimension; i++) {
+                env.setRange(i, extent.getLow(i), extent.getHigh(i) + 1);
+            }
+            env = Envelopes.transform(cornerToCRS, env);
+            if (crs != null) {
+                env.setCoordinateReferenceSystem(crs);
+            }
+        } else if (crs != null) {
+            env = new GeneralEnvelope(crs);
+            env.setToNaN();
+        }
+        envelope = (env != null) ? new ImmutableEnvelope(env) : null;
+    }
+
+    /**
+     * Ensures that the given dimension is equals to the expected value. If not, throws an exception.
+     *
+     * @param argument  the name of the argument being tested.
+     * @param dimension the dimension of the argument value.
+     * @param expected  the expected dimension.
+     */
+    private static void ensureDimensionMatches(final String argument, final int dimension, final int expected)
+            throws MismatchedDimensionException
+    {
+        if (dimension != expected) {
+            throw new MismatchedDimensionException(Errors.format(
+                    Errors.Keys.MismatchedDimension_3, argument, dimension, expected));
+        }
+    }
+
+    /**
+     * Returns the number of dimensions of the <em>grid</em>. This is typically the same
+     * than the number of dimension of the envelope or the CRS, but not necessarily.
+     *
+     * @return the number of grid dimensions.
+     */
+    public int getDimension() {
+        if (gridToCRS != null) {
+            return gridToCRS.getSourceDimensions();
+        }
+        return extent.getDimension();
+    }
+
+    /**
+     * Returns the "real world" coordinate reference system.
+     *
+     * @return the coordinate reference system (never {@code null}).
+     * @throws IncompleteGridGeometryException if this grid geometry has no CRS
+     *         (i.e. <code>{@linkplain #isDefined isDefined}({@linkplain #CRS})</code> returned {@code false}).
+     */
+    public CoordinateReferenceSystem getCoordinateReferenceSystem() throws IncompleteGridGeometryException {
+        if (envelope != null) {
+            final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
+            if (crs != null) {
+                assert isDefined(CRS);
+                return crs;
+            }
+        }
+        assert !isDefined(CRS);
+        throw new IncompleteGridGeometryException(/* Errors.Keys.UnspecifiedCrs */);
+    }
+
+    /**
+     * Returns the bounding box of "real world" coordinates for this grid geometry. This envelope is the
+     * {@linkplain #getExtent() grid extent} {@linkplain #getGridToCRS() transformed} to the "real world"
+     * coordinate system.
+     *
+     * @return the bounding box in "real world" coordinates (never {@code null}).
+     * @throws IncompleteGridGeometryException if this grid geometry has no envelope
+     *         (i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #ENVELOPE})</code> returned {@code false}).
+     */
+    public Envelope getEnvelope() throws IncompleteGridGeometryException {
+        if (envelope != null && !envelope.isAllNaN()) {
+            assert isDefined(ENVELOPE);
+            return envelope;
+        }
+        assert !isDefined(ENVELOPE);
+        throw new IncompleteGridGeometryException(/* gridToCRS == null ?
+                    Errors.Keys.UnspecifiedTransform : Errors.Keys.UnspecifiedGridExtent */);
+    }
+
+    /**
+     * Returns the valid coordinate range of a grid coverage. The lowest valid grid coordinate is zero
+     * for {@link java.awt.image.BufferedImage}, but may be non-zero for arbitrary {@link RenderedImage}.
+     * A grid with 512 cells can have a minimum coordinate of 0 and maximum of 511.
+     *
+     * @return the grid envelope (never {@code null}).
+     * @throws IncompleteGridGeometryException if this grid geometry has no extent
+     *         (i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #EXTENT})</code> returned {@code false}).
+     */
+    public GridEnvelope getExtent() throws IncompleteGridGeometryException {
+        if (extent != null) {
+            assert isDefined(EXTENT);
+            return extent;
+        }
+        assert !isDefined(EXTENT);
+        throw new IncompleteGridGeometryException(/* Errors.Keys.UnspecifiedImageSize */);
+    }
+
+    /**
+     * Returns the transform from grid coordinates to "real world" coordinates in pixel centers.
+     * The transform is often an affine transform.
+     * The coordinate reference system of the real world coordinates is given by
+     * {@link org.opengis.coverage.Coverage#getCoordinateReferenceSystem()}.
+     *
+     * <p><strong>Note:</strong> OGC 01-004 requires that the transform maps <em>pixel centers</em> to real
+     * world coordinates. This is different from some other systems that map pixel's upper left corner.</p>
+     *
+     * @return the transform (never {@code null}).
+     * @throws IncompleteGridGeometryException if this grid geometry has no transform
+     *         (i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #GRID_TO_CRS})</code> returned {@code false}).
+     */
+    public MathTransform getGridToCRS() throws IncompleteGridGeometryException {
+        if (gridToCRS != null) {
+            assert isDefined(GRID_TO_CRS);
+            return gridToCRS;
+        }
+        assert !isDefined(GRID_TO_CRS);
+        throw new IncompleteGridGeometryException(/* Errors.Keys.UnspecifiedTransform */);
+    }
+
+    /**
+     * Returns the transform from grid coordinates to "real world" coordinates.
+     * This is similar to {@link #getGridToCRS()} except that the transform may map
+     * other pixel parts than {@linkplain PixelInCell#CELL_CENTER pixel center}.
+     *
+     * @param  anchor  the pixel part to map.
+     * @return the transform (never {@code null}).
+     * @throws IncompleteGridGeometryException if this grid geometry has no transform
+     *         (i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #GRID_TO_CRS})</code> returned {@code false}).
+     */
+    public MathTransform getGridToCRS(final PixelInCell anchor) throws IncompleteGridGeometryException {
+        final MathTransform mt;
+        if (PixelInCell.CELL_CENTER.equals(anchor)) {
+            mt = gridToCRS;
+        } else if (PixelInCell.CELL_CORNER.equals(anchor)) {
+            mt = cornerToCRS;
+        }  else {
+            mt = PixelTranslation.translate(gridToCRS, PixelInCell.CELL_CENTER, anchor);
+        }
+        if (mt == null) {
+            throw new IncompleteGridGeometryException(/* Errors.Keys.UnspecifiedTransform */);
+        }
+        return mt;
+    }
+
+    /**
+     * Estimates the grid resolution in units of the coordinate reference system.
+     * If non-null, the length of the returned array is the number of CRS dimensions.
+     * If some resolutions are not constant factors (i.e. the {@code gridToCRS} transform for the
+     * corresponding dimension is non-linear), then the resolution is set to one of the following values:
+     *
+     * <ul>
+     *   <li>{@link Double#NaN} if {@code allowEstimates} is {@code false}.</li>
+     *   <li>an arbitrary resolution otherwise (currently the resolution in the grid center,
+     *       but this arbitrary choice may change in any future Apache SIS version).</li>
+     * </ul>
+     *
+     * @param  allowEstimates  whether to provide some values even for resolutions that are not constant factors.
+     * @return the grid resolution, or {@code null} if unknown.
+     * @throws TransformException if an error occurred while computing the grid resolution.
+     */
+    public double[] resolution(final boolean allowEstimates) throws TransformException {
+        /*
+         * 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.
+         */
+        Matrix mat = MathTransforms.getMatrix(gridToCRS);
+        if (mat != null) {
+            return resolution(mat, 1);
+        }
+        if (extent == null || gridToCRS == null) {
+            return null;
+        }
+        /*
+         * If we reach this line, the gridToCRS transform has some non-linear parts.
+         * The easiest way to estimate a resolution is to ask for the derivative at
+         * some arbitrary point. For this method, we take the grid center.
+         */
+        final int gridDimension = extent.getDimension();
+        final GeneralDirectPosition gridCenter = new GeneralDirectPosition(gridDimension);
+        for (int i=0; i<gridDimension; i++) {
+            gridCenter.setOrdinate(i, extent.getLow(i) + 0.5*extent.getSpan(i));
+        }
+        final double[] res = resolution(gridToCRS.derivative(gridCenter), 0);
+        if (!allowEstimates) {
+            /*
+             * If we reach this line, we successfully estimated the resolutions but we need to hide non-constant values.
+             * We currently don't have an API for finding the non-linear dimensions. We assume that everthing else than
+             * LinearTransform and pass-through dimensions are non-linear. This is not always true (e.g. in a Mercator
+             * projection, the "longitude → easting" part is linear too), but should be okay for GridGeometry purposes.
+             *
+             * We keep trace of non-linear dimensions in a bitmask, with bits of non-linear dimensions set to 1.
+             * This limit us to 64 dimensions, which is assumed more than enough.
+             */
+            long nonLinearDimensions = 0;
+            for (final MathTransform step : MathTransforms.getSteps(gridToCRS)) {
+                mat = MathTransforms.getMatrix(step);
+                if (mat != null) {
+                    /*
+                     * For linear transforms there is no bits to set. However if some bits were set by a previous
+                     * iteration, we may need to move them (for example the transform may swap axes). We take the
+                     * current bitmasks as source dimensions and find what are the target dimensions for them.
+                     */
+                    long mask = nonLinearDimensions;
+                    nonLinearDimensions = 0;
+                    while (mask != 0) {
+                        final int i = Long.numberOfTrailingZeros(mask);         // Source dimension of non-linear part
+                        for (int j = mat.getNumRow() - 1; --j >= 0;) {          // Possible target dimensions
+                            if (mat.getElement(j, i) != 0) {
+                                if (j >= Long.SIZE) {
+                                    throw new ArithmeticException("Excessive number of dimensions.");
+                                }
+                                nonLinearDimensions |= (1 << j);
+                            }
+                        }
+                        mask &= ~(1 << i);
+                    }
+                } else if (step instanceof PassThroughTransform) {
+                    /*
+                     * Assume that all modified coordinates use non-linear transform. We do not inspect the
+                     * sub-transform recursively because if it had a non-linear step, PassThroughTransform
+                     * should have moved that step outside the sub-transform for easier concatenation with
+                     * the LinearTransforms before of after that PassThroughTransform.
+                     */
+                    long mask = 0;
+                    final int dimIncrease = step.getTargetDimensions() - step.getSourceDimensions();
+                    final int maxBits = Long.SIZE - Math.max(dimIncrease, 0);
+                    for (final int i : ((PassThroughTransform) step).getModifiedCoordinates()) {
+                        if (i >= maxBits) {
+                            throw new ArithmeticException("Excessive number of dimensions.");
+                        }
+                        mask |= (1 << i);
+                    }
+                    /*
+                     * The mask we just computed identifies non-linear source dimensions, but we need target
+                     * dimensions. They are usually the same (the pass-through coordinate values do not have
+                     * their order changed). However we have a difficulty if the number of dimensions changes.
+                     * We know that the change happen in the sub-transform, but we do not know where exactly.
+                     * For example if the mask is 001010 and the number of dimensions increases by 1, we know
+                     * that we still have "00" at the beginning and "0" at the end of the mask, but we don't
+                     * know what happen between the two. Does "101" become "1101" or "1011"? We conservatively
+                     * take "1111", i.e. we unconditionally set all bits in the middle to 1.
+                     *
+                     * Mathematics:
+                     *   (Long.highestOneBit(mask) << 1) - 1
+                     *   is a mask identifying all source dimensions before trailing pass-through dimensions.
+                     *
+                     *   maskHigh = (Long.highestOneBit(mask) << (dimIncrease + 1)) - 1
+                     *   is a mask identifying all target dimensions before trailing pass-through dimensions.
+                     *
+                     *   maskLow = Long.lowestOneBit(mask) - 1
+                     *   is a mask identifying all leading pass-through dimensions (both source and target).
+                     *
+                     *   maskHigh & ~maskLow
+                     *   is a mask identifying only target dimensions after leading pass-through and before
+                     *   trailing pass-through dimensions. In our case, all 1 bits in maskLow are also 1 bits
+                     *   in maskHigh. So we can rewrite as
+                     *
+                     *   maskHigh - maskLow
+                     *   and the -1 terms cancel each other.
+                     */
+                    if (dimIncrease != 0) {
+                        mask = (Long.highestOneBit(mask) << (dimIncrease + 1)) - Long.lowestOneBit(mask);
+                    }
+                    nonLinearDimensions |= mask;
+                } else {
+                    /*
+                     * Not a know transform. Assume dimension may become non-linear.
+                     */
+                    return null;
+                }
+            }
+            /*
+             * Set the resolution to NaN for all dimensions that we have determined to be non-linear.
+             */
+            while (nonLinearDimensions != 0) {
+                final int i = Long.numberOfTrailingZeros(nonLinearDimensions);
+                nonLinearDimensions &= ~(1 << i);
+                res[i] = Double.NaN;
+            }
+        }
+        return res;
+    }
+
+    /**
+     * Computes the resolutions from the given matrix. This is the length 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).
+     */
+    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++) {
+            for (int i=0; i<buffer.length; i++) {
+                buffer[i] = gridToCRS.getElement(j,i);
+            }
+            resolution[j] = MathFunctions.magnitude(buffer);
+        }
+        return resolution;
+    }
+
+    /**
+     * Returns {@code true} if all the parameters specified by the argument are set.
+     *
+     * @param  bitmask any combination of {@link #CRS}, {@link #ENVELOPE}, {@link #EXTENT} and {@link #GRID_TO_CRS}.
+     * @return {@code true} if all specified attributes are defined (i.e. invoking the
+     *         corresponding method will not thrown an {@link IncompleteGridGeometryException}).
+     * @throws IllegalArgumentException if the specified bitmask is not a combination of known masks.
+     */
+    public boolean isDefined(final int bitmask) throws IllegalArgumentException {
+        if ((bitmask & ~(CRS | ENVELOPE | EXTENT | GRID_TO_CRS)) != 0) {
+            throw new IllegalArgumentException(Errors.format(
+                    Errors.Keys.IllegalArgumentValue_2, "bitmask", bitmask));
+        }
+        return ((bitmask & CRS)         == 0 || (envelope  != null && envelope.getCoordinateReferenceSystem() != null))
+            && ((bitmask & ENVELOPE)    == 0 || (envelope  != null && !envelope.isAllNaN()))
+            && ((bitmask & EXTENT)      == 0 || (extent    != null))
+            && ((bitmask & GRID_TO_CRS) == 0 || (gridToCRS != null));
+    }
+
+    /**
+     * Returns a hash value for this grid geometry. This value need not remain
+     * consistent between different implementations of the same class.
+     */
+    @Override
+    public int hashCode() {
+        int code = (int) serialVersionUID;
+        if (gridToCRS != null) {
+            code += gridToCRS.hashCode();
+        }
+        if (extent != null) {
+            code += extent.hashCode();
+        }
+        // We do not check the envelope since it has a determinist relationship with other attributes.
+        return code;
+    }
+
+    /**
+     * Compares the specified object with this grid geometry for equality.
+     *
+     * @param  object  the object to compare with.
+     * @return {@code true} if the given object is equals to this grid geometry.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (object != null && object.getClass() == getClass()) {
+            final GridGeometry that = (GridGeometry) object;
+            return Objects.equals(this.extent,    that.extent)    &&
+                   Objects.equals(this.gridToCRS, that.gridToCRS) &&
+                   Objects.equals(this.envelope,  that.envelope);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a string representation of this grid geometry. The returned string
+     * is implementation dependent. It is provided for debugging purposes only.
+     */
+    @Debug
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + '[' + extent + ", " + gridToCRS + ']';
+    }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/IncompleteGridGeometryException.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/IncompleteGridGeometryException.java
new file mode 100644
index 0000000..0ea406c
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/IncompleteGridGeometryException.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.coverage.grid;
+
+import org.apache.sis.internal.raster.Resources;
+
+
+/**
+ * Thrown by {@link GridGeometry} when a grid geometry can not provide the requested information.
+ * For example this exception is thrown when {@link GridGeometry#getEnvelope()} is invoked while
+ * the grid geometry has been built with a null envelope.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public class IncompleteGridGeometryException extends IllegalStateException {
+    /**
+     * Serial number for inter-operability with different versions.
+     */
+    private static final long serialVersionUID = -7386283388753448743L;
+
+    /**
+     * Constructs an exception with no detail message.
+     */
+    public IncompleteGridGeometryException() {
+    }
+
+    /**
+     * Constructs an exception with a detail message from the specified error code.
+     * Should not be public because the SIS I18N framework is not a committed one.
+     */
+    IncompleteGridGeometryException(final short code) {
+        super(Resources.format(code));
+    }
+
+    /**
+     * Constructs an exception with the specified detail message.
+     *
+     * @param  message  the detail message.
+     */
+    public IncompleteGridGeometryException(final String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs an exception with the specified detail message and cause.
+     *
+     * @param  message  the detail message.
+     * @param  cause    the cause for this exception.
+     */
+    public IncompleteGridGeometryException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/PixelTranslation.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/PixelTranslation.java
new file mode 100644
index 0000000..ab683b0
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/PixelTranslation.java
@@ -0,0 +1,358 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.coverage.grid;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Objects;
+import java.io.Serializable;
+
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.metadata.spatial.PixelOrientation;
+import static org.opengis.metadata.spatial.PixelOrientation.*;
+
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.Static;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+
+
+/**
+ * The translation to apply for different values of {@link PixelOrientation} or {@link PixelInCell}.
+ * The translation are returned by a call to one of the following static methods:
+ *
+ * <ul>
+ *   <li>{@link #getPixelTranslation(PixelOrientation)} for the two-dimensional case.</li>
+ *   <li>{@link #getPixelTranslation(PixelInCell)} for the <var>n</var>-dimensional case.</li>
+ * </ul>
+ *
+ * This class provides also a few {@code translate(…)} convenience methods,
+ * which apply the pixel translation on a given {@link MathTransform} instance.
+ *
+ * <div class="note"><b>Example:</b>
+ * if the following code snippet, {@code gridToCRS} is an {@link java.awt.geom.AffineTransform} from
+ * <cite>grid cell</cite> coordinates (typically pixel coordinates) to some arbitrary CRS coordinates.
+ * In this example, the transform maps pixels {@linkplain PixelOrientation#CENTER center},
+ * while the {@linkplain PixelOrientation#UPPER_LEFT upper left} corner is desired.
+ * This code will switch the affine transform from the <cite>pixel center</cite> to
+ * <cite>upper left corner</cite> convention:
+ *
+ * {@preformat java
+ *   final AffineTransform  gridToCRS = ...;
+ *   final PixelOrientation current   = PixelOrientation.CENTER;
+ *   final PixelOrientation expected  = PixelOrientation.UPPER_LEFT;
+ *
+ *   // Switch the transform from 'current' to 'expected' convention.
+ *   final PixelTranslation source = getPixelTranslation(current);
+ *   final PixelTranslation target = getPixelTranslation(expected);
+ *   gridToCRS.translate(target.dx - source.dx,
+ *                       target.dy - source.dy);
+ * }
+ * </div>
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ *
+ * @see PixelInCell
+ * @see PixelOrientation
+ *
+ * @since 1.0
+ * @module
+ */
+public final class PixelTranslation extends Static implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -5671620211497720808L;
+
+    /**
+     * Math transforms created by {@link #translate(MathTransform, PixelInCell, PixelInCell)}.
+     * Each element in this array will be created when first needed.
+     * Even indices are translations by -0.5 while odd indices are translations by +0.5.
+     */
+    private static final MathTransform[] translations = new MathTransform[16];
+
+    /**
+     * The pixel orientation for this translation.
+     */
+    public final PixelOrientation orientation;
+
+    /**
+     * The translation among the <var>x</var> axis relative to pixel center.
+     * The value is typically in the [-0.5 .. +0.5] range.
+     */
+    public final double dx;
+
+    /**
+     * The translation among the <var>y</var> axis relative to pixel center.
+     * The value is typically in the [-0.5 .. +0.5] range.
+     */
+    public final double dy;
+
+    /**
+     * The offset for various pixel orientations. Keys must be upper-case names.
+     */
+    private static final Map<PixelOrientation, PixelTranslation> ORIENTATIONS = new HashMap<>(12);
+    static {
+        add(CENTER,       0.0,  0.0);
+        add(UPPER_LEFT,  -0.5, -0.5);
+        add(UPPER_RIGHT,  0.5, -0.5);
+        add(LOWER_LEFT,  -0.5,  0.5);
+        add(LOWER_RIGHT,  0.5,  0.5);
+    }
+
+    /** For {@link #ORIENTATIONS} construction only. */
+    private static void add(final PixelOrientation orientation, final double dx, final double dy) {
+        if (ORIENTATIONS.put(orientation, new PixelTranslation(orientation, dx, dy)) != null) {
+            throw new AssertionError();
+        }
+    }
+
+    /**
+     * Creates a new pixel translation.
+     */
+    private PixelTranslation(final PixelOrientation orientation, final double dx, final double dy) {
+        this.orientation = orientation;
+        this.dx = dx;
+        this.dy = dy;
+    }
+
+    /**
+     * Returns the pixel orientation for the given {@code PixelInCell} code.
+     *
+     * @param  anchor  the {@code PixelInCell} code, or {@code null}.
+     * @return the corresponding pixel orientation, or {@code null} if the argument was null.
+     * @throws IllegalArgumentException if the given code is unknown.
+     */
+    public static PixelOrientation getPixelOrientation(final PixelInCell anchor) {
+        if (anchor == null) {
+            return null;
+        } else if (anchor.equals(PixelInCell.CELL_CENTER)) {
+            return CENTER;
+        } else if (anchor.equals(PixelInCell.CELL_CORNER)) {
+            return UPPER_LEFT;
+        } else {
+            throw new IllegalArgumentException(Errors.format(
+                    Errors.Keys.IllegalArgumentValue_2, "anchor", anchor));
+        }
+    }
+
+    /**
+     * Returns the position relative to the pixel center.
+     * This method returns a value from the following table:
+     *
+     * <table>
+     *   <caption>Translations for "pixel in cell" values</caption>
+     *   <tr><th>Pixel in cell</th><th>offset</th></tr>
+     *   <tr><td>{@link PixelInCell#CELL_CENTER  CELL_CENTER}</td><td> 0.0</td></tr>
+     *   <tr><td>{@link PixelInCell#CELL_CORNER  CELL_CORNER}</td><td>-0.5</td></tr>
+     * </table>
+     *
+     * This method is typically used for <var>n</var>-dimensional grids,
+     * where the number of dimension is unknown.
+     *
+     * @param  anchor  the "pixel in cell" value.
+     * @return the translation for the given "pixel in cell" value.
+     */
+    public static double getPixelTranslation(final PixelInCell anchor) {
+        if (PixelInCell.CELL_CENTER.equals(anchor)) {
+            return 0;
+        } else if (PixelInCell.CELL_CORNER.equals(anchor)) {
+            return -0.5;
+        } else {
+            throw new IllegalArgumentException(Errors.format(
+                    Errors.Keys.IllegalArgumentValue_2, "anchor", anchor));
+        }
+    }
+
+    /**
+     * Returns the specified position relative to the pixel center.
+     * This method returns a value from the following table:
+     *
+     * <table>
+     *   <caption>Translations for "pixel orientation" values</caption>
+     *   <tr><th>Pixel orientation</th>                               <th>  x </th><th>  y </th></tr>
+     *   <tr><td>{@link PixelOrientation#CENTER      CENTER}</td>     <td> 0.0</td><td> 0.0</td></tr>
+     *   <tr><td>{@link PixelOrientation#UPPER_LEFT  UPPER_LEFT}</td> <td>-0.5</td><td>-0.5</td></tr>
+     *   <tr><td>{@link PixelOrientation#UPPER_RIGHT UPPER_RIGHT}</td><td>+0.5</td><td>-0.5</td></tr>
+     *   <tr><td>{@link PixelOrientation#LOWER_LEFT  LOWER_LEFT}</td> <td>-0.5</td><td>+0.5</td></tr>
+     *   <tr><td>{@link PixelOrientation#LOWER_RIGHT LOWER_RIGHT}</td><td>+0.5</td><td>+0.5</td></tr>
+     * </table>
+     *
+     * This method can be used for grid restricted to 2 dimensions.
+     *
+     * @param  anchor  the pixel orientation.
+     * @return the position relative to the pixel center.
+     * @throws IllegalArgumentException if the specified orientation is unknown.
+     */
+    public static PixelTranslation getPixelTranslation(final PixelOrientation anchor) {
+        final PixelTranslation offset = ORIENTATIONS.get(anchor);
+        if (offset == null) {
+            throw new IllegalArgumentException(Errors.format(
+                    Errors.Keys.IllegalArgumentValue_2, "anchor", anchor));
+        }
+        return offset;
+    }
+
+    /**
+     * Returns the pixel orientation for the given offset, or {@code null} if none.
+     * This is the reverse of {@link #getPixelTranslation(PixelOrientation)}.
+     *
+     * @param  dx  the translation along <var>x</var> axis.
+     * @param  dy  the translation along <var>y</var> axis.
+     * @return the pixel orientation of the given values, or {@code null} if none.
+     */
+    public static PixelOrientation getPixelOrientation(final double dx, final double dy) {
+        for (final PixelTranslation candidate : ORIENTATIONS.values()) {
+            if (candidate.dx == dx && candidate.dy == dy) {
+                return candidate.orientation;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Translates the specified math transform according the specified pixel orientations.
+     *
+     * @param  gridToCRS  a math transform from <cite>pixel</cite> coordinates to any CRS.
+     * @param  current    the pixel orientation of the given {@code gridToCRS} transform.
+     * @param  expected   the pixel orientation of the desired transform.
+     * @return the translation from {@code current} to {@code expected}.
+     */
+    public static MathTransform translate(final MathTransform gridToCRS, final PixelInCell current, final PixelInCell expected) {
+        if (Objects.equals(current, expected)) {
+            return gridToCRS;
+        }
+        if (gridToCRS == null) {
+            return null;
+        }
+        final int dimension = gridToCRS.getSourceDimensions();
+        final double offset = getPixelTranslation(expected) - getPixelTranslation(current);
+        final int index;
+        if (offset == -0.5) {
+            index = 2*dimension;
+        } else if (offset == 0.5) {
+            index = 2*dimension + 1;
+        } else {
+            index = translations.length;
+        }
+        MathTransform mt;
+        if (index >= translations.length) {
+            mt = translate(dimension, offset);
+        } else synchronized (translations) {
+            mt = translations[index];
+            if (mt == null) {
+                mt = translate(dimension, offset);
+                translations[index] = mt;
+            }
+        }
+        return MathTransforms.concatenate(mt, gridToCRS);
+    }
+
+    /**
+     * Translates the specified math transform according the specified pixel orientations.
+     *
+     * @param  gridToCRS   a math transform from <cite>pixel</cite> coordinates to any CRS.
+     * @param  current     the pixel orientation of the given {@code gridToCRS} transform.
+     * @param  expected    the pixel orientation of the desired transform.
+     * @param  xDimension  the dimension of <var>x</var> coordinates (pixel columns). Often 0.
+     * @param  yDimension  the dimension of <var>y</var> coordinates (pixel rows). Often 1.
+     * @return the translation from {@code current} to {@code expected}.
+     */
+    public static MathTransform translate(final MathTransform gridToCRS,
+                                          final PixelOrientation current,
+                                          final PixelOrientation expected,
+                                          final int xDimension,
+                                          final int yDimension)
+    {
+        if (Objects.equals(current, expected)) {
+            return gridToCRS;
+        }
+        if (gridToCRS == null) {
+            return null;
+        }
+        final int dimension = gridToCRS.getSourceDimensions();
+        if (xDimension < 0 || xDimension >= dimension) {
+            throw illegalDimension("xDimension", xDimension);
+        }
+        if (yDimension < 0 || yDimension >= dimension) {
+            throw illegalDimension("yDimension", yDimension);
+        }
+        if (xDimension == yDimension) {
+            throw illegalDimension("xDimension", "yDimension");
+        }
+        final PixelTranslation source = getPixelTranslation(current);
+        final PixelTranslation target = getPixelTranslation(expected);
+        final double dx = target.dx - source.dx;
+        final double dy = target.dy - source.dy;
+        MathTransform mt;
+        if (dimension == 2 && (xDimension | yDimension) == 1 && dx == dy && Math.abs(dx) == 0.5) {
+            final int index = (dx >= 0) ? 5 : 4;
+            synchronized (translations) {
+                mt = translations[index];
+                if (mt == null) {
+                    mt = translate(dimension, dx);
+                    translations[index] = mt;
+                }
+            }
+        } else {
+            final Matrix matrix = Matrices.createIdentity(dimension + 1);
+            matrix.setElement(xDimension, dimension, dx);
+            matrix.setElement(yDimension, dimension, dy);
+            mt = MathTransforms.linear(matrix);
+        }
+        return MathTransforms.concatenate(mt, gridToCRS);
+    }
+
+    /**
+     * Formats an exception for an illegal dimension.
+     */
+    private static IllegalArgumentException illegalDimension(final String name, final Object dimension) {
+        return new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, name, dimension));
+    }
+
+    /**
+     * Creates an affine transform that apply the same linear conversion for all dimensions.
+     * For each dimension, input values <var>x</var> are converted into output values <var>y</var>
+     * using the following equation:
+     *
+     * <blockquote><var>y</var> &nbsp;=&nbsp; <var>x</var> × {@code scale} + {@code offset}</blockquote>
+     *
+     * @param  dimension  the input and output dimensions.
+     * @param  offset     the {@code offset} term in the linear equation.
+     * @return the linear transform for the given scale and offset.
+     */
+    private static MathTransform translate(final int dimension, final double offset) {
+        final Matrix matrix = Matrices.createIdentity(dimension + 1);
+        for (int i=0; i<dimension; i++) {
+            matrix.setElement(i, dimension, offset);
+        }
+        return MathTransforms.linear(matrix);
+    }
+
+    /**
+     * Returns a string representation of this pixel translation.
+     */
+    @Debug
+    @Override
+    public String toString() {
+        return String.valueOf(orientation) + '[' + dx + ", " + dy + ']';
+    }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/package-info.java
similarity index 73%
copy from core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
copy to core/sis-raster/src/main/java/org/apache/sis/coverage/grid/package-info.java
index 8a19ee5..597ff30 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/package-info.java
@@ -15,17 +15,14 @@
  * limitations under the License.
  */
 
+
 /**
- * A set of helper classes for the SIS implementation.
- *
- * <p><strong>Do not use!</strong></p>
- *
- * This package is for internal use by SIS only. Classes in this package
- * may change in incompatible ways in any future version without notice.
+ * A coverage backed by a regular grid. In the two-dimensional case, the grid coverage is an image and the cells are pixels.
+ * In the three-dimensional case, the cells are voxels.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since   0.8
+ * @version 1.0
+ * @since   1.0
  * @module
  */
-package org.apache.sis.internal.raster;
+package org.apache.sis.coverage.grid;
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/DefaultIterator.java b/core/sis-raster/src/main/java/org/apache/sis/image/DefaultIterator.java
index bef1739..cd5f58e 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/DefaultIterator.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/DefaultIterator.java
@@ -48,8 +48,8 @@ import org.apache.sis.util.ArgumentChecks;
  *
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since   0.8
+ * @version 1.0
+ * @since   1.0
  * @module
  *
  * @todo Change iteration order on tiles for using Hilbert iterator.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/PixelIterator.java b/core/sis-raster/src/main/java/org/apache/sis/image/PixelIterator.java
index 2b9fd54..c076236 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/PixelIterator.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/PixelIterator.java
@@ -53,8 +53,8 @@ import static java.lang.Math.floorDiv;
  *
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since   0.8
+ * @version 1.0
+ * @since   1.0
  * @module
  */
 public abstract class PixelIterator {
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/TransferType.java b/core/sis-raster/src/main/java/org/apache/sis/image/TransferType.java
index b611308..e70c1ee 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/TransferType.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/TransferType.java
@@ -42,11 +42,11 @@ import org.apache.sis.util.resources.Errors;
  * <a href="http://openjdk.java.net/jeps/301">JEP 301</a> is implemented.</div>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @param  <T>  the type of buffer which can be used for transferring data.
  *
- * @since 0.8
+ * @since 1.0
  * @module
  *
  * @see Raster#getTransferType()
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/WritablePixelIterator.java b/core/sis-raster/src/main/java/org/apache/sis/image/WritablePixelIterator.java
index 5e8a8aa..2e71179 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/WritablePixelIterator.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/WritablePixelIterator.java
@@ -51,8 +51,8 @@ import org.apache.sis.internal.raster.Resources;
  *
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since   0.8
+ * @version 1.0
+ * @since   1.0
  * @module
  */
 public abstract class WritablePixelIterator extends PixelIterator implements Closeable {
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/package-info.java b/core/sis-raster/src/main/java/org/apache/sis/image/package-info.java
index fbb7f73..9150e80 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/package-info.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/package-info.java
@@ -34,8 +34,8 @@
  *
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since   0.8
+ * @version 1.0
+ * @since   1.0
  * @module
  */
 package org.apache.sis.image;
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 7287cb1..01256eb 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
@@ -30,8 +30,8 @@ import org.apache.sis.util.resources.IndexedResourceBundle;
  * all modules in the Apache SIS project, see {@link org.apache.sis.util.resources} package.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since   0.8
+ * @version 1.0
+ * @since   1.0
  * @module
  */
 public final class Resources extends IndexedResourceBundle {
@@ -42,7 +42,7 @@ public final class Resources extends IndexedResourceBundle {
      * pools of compiled classes.
      *
      * @author  Martin Desruisseaux (Geomatys)
-     * @since   0.8
+     * @since   1.0
      * @module
      */
     @Generated("org.apache.sis.util.resources.IndexedResourceCompiler")
@@ -64,6 +64,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CoordinateOutsideDomain_2 = 1;
 
         /**
+         * Illegal grid envelope [{1} … {2}] for dimension {0}.
+         */
+        public static final short IllegalGridEnvelope_3 = 8;
+
+        /**
          * The ({0}, {1}) tile has an unexpected size, number of bands or sample layout.
          */
         public static final short IncompatibleTile_2 = 2;
@@ -167,4 +172,23 @@ public final class Resources extends IndexedResourceBundle {
     {
         return forLocale(null).getString(key, arg0, arg1);
     }
+
+    /**
+     * Gets a string for the given key are replace all occurrence of "{0}",
+     * "{1}", with values of {@code arg0}, {@code arg1}, etc.
+     *
+     * @param  key   the key for the desired string.
+     * @param  arg0  value to substitute to "{0}".
+     * @param  arg1  value to substitute to "{1}".
+     * @param  arg2  value to substitute to "{2}".
+     * @return the formatted string for the given key.
+     * @throws MissingResourceException if no object for the given key can be found.
+     */
+    public static String format(final short  key,
+                                final Object arg0,
+                                final Object arg1,
+                                final Object arg2) throws MissingResourceException
+    {
+        return forLocale(null).getString(key, arg0, arg1, arg2);
+    }
 }
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 510912e..7ffc66e 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
@@ -20,6 +20,7 @@
 # For resources shared by all modules in the Apache SIS project, see "org.apache.sis.util.resources" package.
 #
 CoordinateOutsideDomain_2         = The ({0}, {1}) pixel coordinate is outside iterator domain.
+IllegalGridEnvelope_3             = Illegal grid envelope [{1} \u2026 {2}] for dimension {0}.
 IncompatibleTile_2                = The ({0}, {1}) tile has an unexpected size, number of bands or sample layout.
 IterationIsFinished               = Iteration is finished.
 IterationNotStarted               = Iteration did not started.
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 9ac7916..418351a 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
@@ -25,6 +25,7 @@
 #   U+00A0 NO-BREAK SPACE         before  :
 #
 CoordinateOutsideDomain_2         = La coordonn\u00e9e pixel ({0}, {1}) est en dehors du domaine de l\u2019it\u00e9rateur.
+IllegalGridEnvelope_3             = La plage d\u2019index [{1} \u2026 {2}] de la dimension {0} n\u2019est pas valide.
 IncompatibleTile_2                = La tuile ({0}, {1}) a une taille, un nombre de bandes ou une disposition des valeurs inattendu.
 IterationIsFinished               = L\u2019it\u00e9ration est termin\u00e9e.
 IterationNotStarted               = L\u2019it\u00e9ration n\u2019a pas commenc\u00e9e.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
index 8a19ee5..29a4094 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
@@ -24,8 +24,8 @@
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since   0.8
+ * @version 1.0
+ * @since   1.0
  * @module
  */
 package org.apache.sis.internal.raster;

-- 
To stop receiving notification emails like this one, please contact
desruisseaux@apache.org.

Mime
View raw message