You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2020/10/09 11:07:08 UTC

[sis] 04/04: Replace `GridOrientation` enumeration by a more configurable class.

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 0885edcc7cc54f0ade6575c874190e9991d56678
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Oct 9 13:05:33 2020 +0200

    Replace `GridOrientation` enumeration by a more configurable class.
---
 .../sis/coverage/grid/GridCoverageBuilder.java     |  19 +-
 .../org/apache/sis/coverage/grid/GridExtent.java   |   6 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java |  55 +++---
 .../apache/sis/coverage/grid/GridOrientation.java  | 210 ++++++++++++++++++---
 .../sis/coverage/grid/GridCoverageBuilderTest.java |   8 +-
 .../sis/coverage/grid/GridDerivationTest.java      |   6 +-
 .../apache/sis/coverage/grid/GridGeometryTest.java |   2 +-
 .../sis/coverage/grid/GridOrientationTest.java     |  75 ++++++++
 .../coverage/grid/ResampledGridCoverageTest.java   |   3 +-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 10 files changed, 314 insertions(+), 71 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
index c598725..cf5fa01 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
@@ -159,7 +159,7 @@ public class GridCoverageBuilder {
      * Set of grid axes to reverse, as a bit mask. For any dimension <var>i</var>, the bit
      * at {@code 1L << i} is set to 1 if the grid axis at that dimension should be flipped.
      *
-     * @see #flipAxis(int)
+     * @see #flipGridAxis(int)
      */
     private long flippedAxes;
 
@@ -168,6 +168,7 @@ public class GridCoverageBuilder {
      *
      * @see #addImageProperty(String, Object)
      */
+    @SuppressWarnings("UseOfObsoleteCollectionType")
     private Hashtable<String,Object> properties;
 
     /**
@@ -208,7 +209,7 @@ public class GridCoverageBuilder {
      * coordinates in the coverage domain (e.g. latitude values) are also increasing toward up.
      * It often results in images flipped vertically, because popular image formats such as PNG
      * use row indices increasing in the opposite direction (toward down).
-     * This effect can be compensated by invoking <code>{@linkplain #flipAxis(int) flipAxis}(1)</code>.
+     * This effect can be compensated by invoking <code>{@linkplain #flipGridAxis(int) flipGridAxis}(1)</code>.
      *
      * <div class="note"><b>Design note:</b>
      * {@code GridCoverageBuilder} does not flip the <var>y</var> axis by default because not all
@@ -230,11 +231,11 @@ public class GridCoverageBuilder {
      *                 or {@code null} for removing previous domain setting.
      * @return {@code this} for method invocation chaining.
      *
-     * @see #flipAxis(int)
+     * @see #flipGridAxis(int)
      * @see GridGeometry#GridGeometry(GridExtent, Envelope)
      */
     public GridCoverageBuilder setDomain(final Envelope domain) {
-        return setDomain(domain == null ? null : new GridGeometry(null, domain, null));
+        return setDomain(domain == null ? null : new GridGeometry(null, domain, GridOrientation.HOMOTHETY));
     }
 
     /**
@@ -309,7 +310,7 @@ public class GridCoverageBuilder {
      * must be equal to the number of sample dimensions.
      *
      * <p><b>Note:</b> row indices in an image are usually increasing down, while geographic coordinates
-     * are usually increasing up. Consequently the <code>{@linkplain #flipAxis(int) flipAxis}(1)</code>
+     * are usually increasing up. Consequently the <code>{@linkplain #flipGridAxis(int) flipGridAxis}(1)</code>
      * method may need to be invoked after this method.</p>
      *
      * @param  data  the rendered image to be wrapped in a {@code GridCoverage}. Can not be {@code null}.
@@ -333,7 +334,7 @@ public class GridCoverageBuilder {
      * number of sample dimensions.
      *
      * <p><b>Note:</b> row indices in a raster are usually increasing down, while geographic coordinates
-     * are usually increasing up. Consequently the <code>{@linkplain #flipAxis(int) flipAxis}(1)</code>
+     * are usually increasing up. Consequently the <code>{@linkplain #flipGridAxis(int) flipGridAxis}(1)</code>
      * method may need to be invoked after this method.</p>
      *
      * @param  data  the raster to be wrapped in a {@code GridCoverage}. Can not be {@code null}.
@@ -379,7 +380,7 @@ public class GridCoverageBuilder {
     /**
      * Reverses axis direction in the specified grid dimension.
      * For example if grid indices are (<var>column</var>, <var>row</var>),
-     * then {@code flipAxis(1)} will reverse the direction of rows axis.
+     * then {@code flipGridAxis(1)} will reverse the direction of rows axis.
      * Invoking this method a second time for the same dimension will cancel the flipping.
      *
      * <p>When building coverage with a {@linkplain #setDomain(Envelope) domain specified by an envelope}
@@ -392,8 +393,9 @@ public class GridCoverageBuilder {
      * @return {@code this} for method invocation chaining.
      *
      * @see #setDomain(Envelope)
+     * @see GridOrientation#flipGridAxis(int)
      */
-    public GridCoverageBuilder flipAxis(final int dimension) {
+    public GridCoverageBuilder flipGridAxis(final int dimension) {
         ArgumentChecks.ensurePositive("dimension", dimension);
         if (dimension >= Long.SIZE) {
             throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension + 1));
@@ -412,6 +414,7 @@ public class GridCoverageBuilder {
      *
      * @since 1.1
      */
+    @SuppressWarnings("UseOfObsoleteCollectionType")
     public void addImageProperty(final String key, final Object value) {
         ArgumentChecks.ensureNonNull("key",   key);
         ArgumentChecks.ensureNonNull("value", value);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index 727ea00..7652d5b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -1375,11 +1375,11 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable
      * axis direction except for those specified in the {@code flips} bitmask. The transform maps cell corners.
      *
      * @param  env               the target envelope. Despite this method name, the envelope CRS is ignored.
-     * @param  flips             bitmask of target axes to flip (0 if none).
+     * @param  flippedAxes       bitmask of target axes to flip (0 if none).
      * @param  sourceDimensions  source dimension for each target dimension, or {@code null} if dimensions are the same.
      * @return an affine transform from this grid extent to the given envelope, expressed as a matrix.
      */
-    final MatrixSIS cornerToCRS(final Envelope env, final long flips, final int[] sourceDimensions) {
+    final MatrixSIS cornerToCRS(final Envelope env, final long flippedAxes, final int[] sourceDimensions) {
         final int          srcDim = getDimension();
         final int          tgtDim = env.getDimension();
         final MatrixSIS    affine = Matrices.create(tgtDim + 1, srcDim + 1, ExtendedPrecisionMatrix.ZERO);
@@ -1388,7 +1388,7 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable
         for (int j=0; j<tgtDim; j++) {
             final int i = (sourceDimensions != null) ? sourceDimensions[j] : j;
             if (i < srcDim) {
-                final boolean flip = (flips & Numerics.bitmask(j)) != 0;
+                final boolean flip = (flippedAxes & Numerics.bitmask(j)) != 0;
                 offset.set(coordinates[i]);
                 scale.set(coordinates[i + srcDim]);
                 scale.subtract(offset);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index e02e8fa..6aa28df 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -46,7 +46,6 @@ import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.referencing.crs.AbstractCRS;
-import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
@@ -592,15 +591,15 @@ public class GridGeometry implements LenientComparable, Serializable {
      *
      * <h4>Dimension order</h4>
      * The given envelope shall always declare dimensions in same order than the given extent.
-     * This constructor may reorder CRS axes if {@code orientation} is {@link GridOrientation#DISPLAY},
-     * but in such case this constructor will derive itself an envelope and CRS with the reordered axes.
+     * This constructor may reorder axes if {@code orientation} is (for example) {@link GridOrientation#DISPLAY},
+     * but in such case this constructor will derive itself an envelope and a CRS with reordered axes.
      *
      * @param  extent       the valid extent of grid coordinates, or {@code null} if unknown.
      * @param  envelope     the envelope together with CRS of the "real world" coordinates, or {@code null} if unknown.
      * @param  orientation  high-level description of desired characteristics of the {@code gridToCRS} transform.
-     *                      Ignored (can be null) if {@code extent} or {@code envelope} is null.
+     *                      Ignored (can be null) if {@code envelope} is null.
      * @throws NullPointerException if {@code extent} and {@code envelope} arguments are both null,
-     *         or if only the {@code orientation} argument is null.
+     *         or if {@code envelope} is non-null but {@code orientation} is null.
      *
      * @see <a href="https://en.wikipedia.org/wiki/Axis-aligned_object">Axis-aligned object on Wikipedia</a>
      *
@@ -616,27 +615,29 @@ public class GridGeometry implements LenientComparable, Serializable {
         long flip = 0;                      // Bitmask specifying whether to reverse axis in each dimension.
         ImmutableEnvelope target = null;    // May have different axis order than the specified `envelope` CRS.
         int[] sourceDimensions = null;      // Indices in source envelope of axes colinear with the target envelope.
-        final boolean reorderExtent = (orientation == GridOrientation.DISPLAY_GRID);
-        if (envelope != null && (reorderExtent || orientation == GridOrientation.DISPLAY)) {
-            final AbstractCRS sourceCRS = AbstractCRS.castOrCopy(envelope.getCoordinateReferenceSystem());
-            if (sourceCRS != null) {
-                final AbstractCRS targetCRS = sourceCRS.forConvention(AxesConvention.DISPLAY_ORIENTED);
-                if (targetCRS != sourceCRS) {
-                    final CoordinateSystem sourceCS = sourceCRS.getCoordinateSystem();
-                    final CoordinateSystem targetCS = targetCRS.getCoordinateSystem();
-                    sourceDimensions = AxisDirections.indicesOfColinear(sourceCS, targetCS);
-                    if (sourceDimensions != null) {
-                        final double[] lowerCorner = new double[sourceDimensions.length];
-                        final double[] upperCorner = new double[sourceDimensions.length];
-                        for (int i=0; i < sourceDimensions.length; i++) {
-                            final int s = sourceDimensions[i];
-                            lowerCorner[i] = envelope.getMinimum(s);
-                            upperCorner[i] = envelope.getMaximum(s);
-                            if (sourceCS.getAxis(s).getDirection() != targetCS.getAxis(i).getDirection()) {
-                                flip |= Numerics.bitmask(i);
+        if (envelope != null) {
+            ArgumentChecks.ensureNonNull("orientation", orientation);
+            if (orientation.crsVariant != null) {
+                final AbstractCRS sourceCRS = AbstractCRS.castOrCopy(envelope.getCoordinateReferenceSystem());
+                if (sourceCRS != null) {
+                    final AbstractCRS targetCRS = sourceCRS.forConvention(orientation.crsVariant);
+                    if (targetCRS != sourceCRS) {
+                        final CoordinateSystem sourceCS = sourceCRS.getCoordinateSystem();
+                        final CoordinateSystem targetCS = targetCRS.getCoordinateSystem();
+                        sourceDimensions = AxisDirections.indicesOfColinear(sourceCS, targetCS);
+                        if (sourceDimensions != null) {
+                            final double[] lowerCorner = new double[sourceDimensions.length];
+                            final double[] upperCorner = new double[sourceDimensions.length];
+                            for (int i=0; i < sourceDimensions.length; i++) {
+                                final int s = sourceDimensions[i];
+                                lowerCorner[i] = envelope.getMinimum(s);
+                                upperCorner[i] = envelope.getMaximum(s);
+                                if (sourceCS.getAxis(s).getDirection() != targetCS.getAxis(i).getDirection()) {
+                                    flip |= Numerics.bitmask(i);
+                                }
                             }
+                            target = new ImmutableEnvelope(lowerCorner, upperCorner, targetCRS);
                         }
-                        target = new ImmutableEnvelope(lowerCorner, upperCorner, targetCRS);
                     }
                 }
             }
@@ -658,7 +659,7 @@ public class GridGeometry implements LenientComparable, Serializable {
         } else {
             this.envelope = target;
             if (extent != null) {
-                if (reorderExtent) {
+                if (orientation != null && orientation.canReorderGridAxis) {
                     if (!ArraysExt.isRange(0, sourceDimensions)) {
                         extent = extent.reorder(sourceDimensions);
                     }
@@ -671,8 +672,8 @@ public class GridGeometry implements LenientComparable, Serializable {
                      * that all scale factors are on diagonal, which allows simpler calculation than full
                      * matrix multiplication. Use double-double arithmetic everywhere.
                      */
-                    ArgumentChecks.ensureNonNull("orientation", orientation);
-                    final MatrixSIS affine = extent.cornerToCRS(target, orientation.flip ^ flip, sourceDimensions);
+                    @SuppressWarnings("null")           // `!nilEnvelope` implies non-null `orientation`.
+                    final MatrixSIS affine = extent.cornerToCRS(target, orientation.flippedAxes ^ flip, sourceDimensions);
                     cornerToCRS = MathTransforms.linear(affine);
                     final int srcDim = cornerToCRS.getSourceDimensions();       // Translation column in matrix.
                     final int tgtDim = cornerToCRS.getTargetDimensions();       // Number of matrix rows before last row.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridOrientation.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridOrientation.java
index 9f92f1b..5bf5ec3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridOrientation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridOrientation.java
@@ -16,17 +16,21 @@
  */
 package org.apache.sis.coverage.grid;
 
+import java.util.Objects;
+import java.io.Serializable;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.datum.PixelInCell;
 import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
- * High-level description about how a grid is orientated relative to the CRS axes.
- * This is determined by the {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform.
- * For example conversion from grid coordinates to CRS coordinates may flip the <var>y</var> axis
+ * High-level description about how a grid is orientated relative to the CRS axes. The orientation of a grid
+ * is closely related to the {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform.
+ * For example the conversion from grid coordinates to CRS coordinates may flip the <var>y</var> axis
  * (grid coordinates increasing toward down on screen), or may swap <var>x</var> and <var>y</var> axes, <i>etc.</i>
- * The possibilities are infinite; this enumeration covers only a few common types where the grid is
+ * The constants enumerated in this class cover only a few common cases where the grid is
  * <a href="https://en.wikipedia.org/wiki/Axis-aligned_object">axis-aligned</a> with the CRS.
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -37,7 +41,12 @@ import org.apache.sis.referencing.cs.AxesConvention;
  * @since 1.1
  * @module
  */
-public enum GridOrientation {
+public final class GridOrientation implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -1354776950822418237L;
+
     /**
      * The {@code gridToCRS} transform applies only scales and translations (no axis flip or swap).
      * Moving along the grid axis in dimension <var>i</var> causes a displacement along the CRS axis
@@ -59,11 +68,12 @@ public enum GridOrientation {
      * <var>S<sub>y</sub></var> &gt; 0 and
      * <var>S<sub>z</sub></var> &gt; 0.
      */
-    HOMOTHETY(0),
+    public static final GridOrientation HOMOTHETY = new GridOrientation(0, null, false);
 
     /**
      * The {@code gridToCRS} transform applies scales and translations with a flip of the second axis (<var>y</var>).
-     * This is the same kind of conversion than {@link #HOMOTHETY} except that the <var>S<sub>y</sub></var> term in
+     * This is equivalent to {@code HOMOTHETY.flipGridAxis(1)}; i.e.
+     * this is the same kind of conversion than {@link #HOMOTHETY} except that the <var>S<sub>y</sub></var> term in
      * the matrix is replaced by −<var>S<sub>y</sub></var> and the <var>T<sub>y</sub></var> term has a different value.
      * For example in the three-dimensional case, the {@code gridToCRS} transform is:
      *
@@ -76,21 +86,25 @@ public enum GridOrientation {
      *   └                 ┘
      * }
      *
+     * <h4>When to use</h4>
      * {@code REFLECTION_Y} is commonly used when the grid is a {@link java.awt.image.RenderedImage}.
      * By contrast, an {@link #HOMOTHETY} transform often results in <var>y</var> axis oriented toward up,
      * instead of down as commonly expected with rendered images.
      * This {@code REFLECTION_Y} value matches the common usage for grids backed by images.
+     *
+     * @see #flipGridAxis(int)
      */
-    REFLECTION_Y(2),
+    public static final GridOrientation REFLECTION_Y = new GridOrientation(2, null, false);
 
     /**
      * CRS axes are reordered and oriented toward directions commonly used for displaying purpose.
+     * This is equivalent to {@code REFLECTION_Y.useVariantOfCRS(AxesConvention.DISPLAY_ORIENTED)}.
      * {@link GridGeometry}s created with this orientation have properties computed as below:
      *
      * <ul>
      *   <li>The {@link GridExtent} specified by user (never modified).</li>
-     *   <li>An envelope initialized to user-specified envelope (potentially modified below).</li>
-     *   <li>A {@code gridToCRS} initialized to {@link #REFLECTION_Y} (potentially modified below).</li>
+     *   <li>An envelope initialized to user-specified envelope (potentially modified as described below).</li>
+     *   <li>A {@code gridToCRS} initialized to {@link #REFLECTION_Y} (potentially modified as described below).</li>
      *   <li>The {@linkplain AxesConvention#DISPLAY_ORIENTED display oriented} variant of the CRS specified by user.</li>
      *   <li>If above CRS variant is same as user-specified CRS, we are done. Otherwise:
      *     <ul>
@@ -115,36 +129,184 @@ public enum GridOrientation {
      *   └                  ┘
      * }
      *
+     * <h4>When to use</h4>
      * This orientation can be used for deriving a coordinate reference system with the
      * <i>(<var>longitude</var>, <var>latitude</var>)</i> or <i>(<var>x</var>,<var>y</var>)</i> axis order,
      * but without altering grid axes order.
      *
+     * <h4>Alternative</h4>
+     * {@code DISPLAY.canReorderGridAxis(true)} is an alternative where grid axes get the same reordering than CRS axes.
+     * Consequently the {@link GridExtent} may be different then the specified extent
+     * but the {@code gridToCRS} transform always has the form shown in {@link #REFLECTION_Y}.
+     * This alternative can be used for deriving a coordinate reference system with the
+     * <i>(<var>longitude</var>, <var>latitude</var>)</i> or <i>(<var>x</var>,<var>y</var>)</i> axis order,
+     * and modify grid cell layout (i.e. replace the {@link GridExtent} instance)
+     * in way that allows {@link java.awt.image.RenderedImage} to appear with expected orientation.
+     *
+     * @see #useVariantOfCRS(AxesConvention)
      * @see AxesConvention#DISPLAY_ORIENTED
      */
-    DISPLAY(2),
+    public static final GridOrientation DISPLAY = new GridOrientation(2, AxesConvention.DISPLAY_ORIENTED, false);
 
     /**
-     * CRS and grid axes are reordered and oriented toward directions commonly used for displaying purpose.
-     * This is similar to {@link #DISPLAY} except that {@link GridExtent} axes get the same reordering than CRS axes.
-     * Consequently the {@code gridToCRS} transform always has the form shown in {@link #REFLECTION_Y}.
+     * Set of grid axes to reverse, as a bit mask. For any dimension <var>i</var>, the bit
+     * at {@code 1L << i} is set to 1 if the grid axis at that dimension should be flipped.
+     * This is the argument to give in calls to {@link GridExtent#cornerToCRS(Envelope, long, int[])}.
      *
-     * <p>This orientation can be used for deriving a coordinate reference system with the
-     * <i>(<var>longitude</var>, <var>latitude</var>)</i> or <i>(<var>x</var>,<var>y</var>)</i> axis order,
-     * and modify grid cell layout (i.e. replace the {@link GridExtent} instance)
-     * in way that allows {@link java.awt.image.RenderedImage} to appear with expected orientation.</p>
+     * @see #flipGridAxis(int)
      */
-    DISPLAY_GRID(2);
+    final long flippedAxes;
 
     /**
-     * Bitmask of axes to flip.
-     * This is the argument to give in calls to {@link GridExtent#cornerToCRS(Envelope, long, int[])}.
+     * If the user-specified CRS should be substituted by a variant of that CRS, the variant to use.
+     * Otherwise {@code null}. If non-null, either the {@code gridToCRS} matrix may be non-diagonal
+     * or the {@link GridExtent} axes may be ordered, depending on {@link #canReorderGridAxis} value.
+     *
+     * @see #useVariantOfCRS(AxesConvention)
      */
-    final int flip;
+    final AxesConvention crsVariant;
+
+    /**
+     * Whether {@link GridExtent} can be rewritten with a different axis order
+     * for matching the CRS axis order specified by {@link #crsVariant}.
+     * If {@code false}, then axis order changes will be handled in the {@code gridToCRS} transform instead.
+     *
+     * @see #canReorderGridAxis(boolean)
+     */
+    final boolean canReorderGridAxis;
 
     /**
      * Creates a new enumeration value.
      */
-    private GridOrientation(final int flip) {
-        this.flip = flip;
+    private GridOrientation(final long flippedAxes, final AxesConvention crsVariant, final boolean canReorderGridAxis) {
+        this.flippedAxes = flippedAxes;
+        this.crsVariant  = crsVariant;
+        this.canReorderGridAxis = canReorderGridAxis;
+    }
+
+    /**
+     * Reverses axis direction in the specified grid dimension.
+     * For example if grid indices are (<var>column</var>, <var>row</var>),
+     * then {@code flipGridAxis(1)} will reverse the direction of rows axis.
+     * Invoking this method a second time for the same dimension will cancel the flipping.
+     *
+     * @param  dimension  index of the dimension in the grid on which to apply direction reversal.
+     * @return a grid orientation equals to this one except for the axis flip in specified dimension.
+     *
+     * @see #REFLECTION_Y
+     * @see GridCoverageBuilder#flipGridAxis(int)
+     */
+    public GridOrientation flipGridAxis(final int dimension) {
+        ArgumentChecks.ensurePositive("dimension", dimension);
+        if (dimension >= Long.SIZE) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension + 1));
+        }
+        return new GridOrientation(flippedAxes ^ (1L << dimension), crsVariant, canReorderGridAxis);
+    }
+
+    /**
+     * Substitutes the user-specified CRS by a variant of it, for example with different axis order.
+     * If the CRS axis order changed as a result of this substitution, then:
+     *
+     * <ul>
+     *   <li>Order of envelope coordinates are changed accordingly.</li>
+     *   <li>CRS axis order change is mapped to the grid in one of the following ways:
+     *     <ul>
+     *       <li>If {@code canReorderGridAxis(true)} has been invoked, then the same change is applied
+     *           on grid axis order. Consequently grid axes and CRS axes stay in the same order,
+     *           but the resulting {@link GridExtent} may be different than the specified one.</li>
+     *       <li>Otherwise {@link GridExtent} stay unchanged and axis order change is handled in the
+     *           {@code gridToCRS} transform instead.</li>
+     *     </ul>
+     *   </li>
+     * </ul>
+     *
+     * <h4>Limitations</h4>
+     * Current implementation accepts only axis order changes and direction changes.
+     * The units of measurement changes are not yet supported.
+     * Consequently {@link AxesConvention#NORMALIZED} is not accepted.
+     *
+     * @param  variant  the kind of substitution to apply on CRS, or {@code null} if none.
+     * @return a grid orientation equals to this one except that it uses the specified CRS variant.
+     *
+     * @see #DISPLAY
+     */
+    public GridOrientation useVariantOfCRS(final AxesConvention variant) {
+        if (variant == crsVariant) {
+            return this;
+        }
+        if (variant == AxesConvention.NORMALIZED) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedArgumentValue_1, variant));
+        }
+        return new GridOrientation(flippedAxes, variant, canReorderGridAxis);
+    }
+
+    /**
+     * Specifies whether a change of CRS axis order should be accompanied by an equivalent change of grid axis order.
+     * A value of {@code true} implies that user-specified {@link GridExtent} may be replaced by a different extent.
+     * If {@code false} (the default), then axis order changes will be handled in the {@code gridToCRS} transform
+     * instead.
+     *
+     * @param  enabled  whether changes of CRS axis order should be reflected by changes of grid axis order.
+     * @return a grid orientation equals to this one except that it has the specified flag.
+     */
+    public GridOrientation canReorderGridAxis(final boolean enabled) {
+        if (enabled == canReorderGridAxis) {
+            return this;
+        }
+        return new GridOrientation(flippedAxes, crsVariant, enabled);
+    }
+
+    /**
+     * Returns whether this object is equal to the given object.
+     *
+     * @param  other  the other object to compare with this object.
+     * @return whether the two objects are equal.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (other instanceof GridOrientation) {
+            final GridOrientation that = (GridOrientation) other;
+            return flippedAxes == that.flippedAxes &&
+                   crsVariant  == that.crsVariant  &&
+                   canReorderGridAxis == that.canReorderGridAxis;
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value for this grid orientation.
+     */
+    @Override
+    public int hashCode() {
+        return Long.hashCode(flippedAxes) + Objects.hashCode(crsVariant) + Boolean.hashCode(canReorderGridAxis);
+    }
+
+    /**
+     * Returns a string representation of this grid orientation.
+     * This is for debugging purpose and may change in any future version.
+     */
+    @Override
+    public String toString() {
+        final StringBuilder buffer = new StringBuilder(getClass().getSimpleName()).append('[');
+        String separator = "";
+        if (flippedAxes != 0) {
+            buffer.append("flip={");
+            long f = flippedAxes;
+            do {
+                final long i = Long.numberOfTrailingZeros(f);
+                buffer.append(separator).append(i);
+                f &= ~(1L << i);
+                separator = ", ";
+            } while (f != 0);
+            buffer.append('}');
+        }
+        if (crsVariant != null) {
+            buffer.append(separator).append("crs=").append(crsVariant);
+            if (canReorderGridAxis) {
+                buffer.append(" with grid axis reordering");
+            }
+        }
+        return buffer.append(']').toString();
     }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
index 74137f2..7c88794 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
@@ -156,7 +156,7 @@ public final strictfp class GridCoverageBuilderTest extends TestCase {
      * Tests {@link GridCoverageBuilder#setValues(DataBuffer, Dimension)}.
      */
     @Test
-    public void createFromBufferTest() {
+    public void testCreateFromBuffer() {
         final DataBuffer buffer = new DataBufferByte(new byte[] {1,2,3,4,5,6}, 6);
         final GridCoverageBuilder builder = new GridCoverageBuilder();
         assertSame(builder, builder.setValues(buffer, null));
@@ -172,10 +172,10 @@ public final strictfp class GridCoverageBuilderTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridCoverageBuilder#flipAxis(int)}.
+     * Tests {@link GridCoverageBuilder#flipGridAxis(int)}.
      */
     @Test
-    public void flippedAxisTest() {
+    public void testFlipGridAxis() {
         final RenderedImage image = new BufferedImage(36, 18, BufferedImage.TYPE_INT_ARGB);
         final GeneralEnvelope domain = new GeneralEnvelope(HardCodedCRS.WGS84);
         domain.setRange(0, -180, +180);
@@ -201,7 +201,7 @@ public final strictfp class GridCoverageBuilderTest extends TestCase {
          * This is the common orientation for images.
          */
         {
-            assertSame(builder, builder.flipAxis(1));
+            assertSame(builder, builder.flipGridAxis(1));
             final GridCoverage coverage = builder.build();
             final GridGeometry gg = coverage.getGridGeometry();
             assertTrue(domain.equals(gg.getEnvelope(), STRICT, false));
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
index e32265d..45fb160 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
@@ -619,8 +619,8 @@ public final strictfp class GridDerivationTest extends TestCase {
         Envelope2D   domain   = new Envelope2D(HardCodedCRS.WGS84, 10, 20, 110, 70);
         Envelope2D   request  = new Envelope2D(HardCodedCRS.WGS84, -5, 25, 100, 90);
         Envelope2D   expected = new Envelope2D(HardCodedCRS.WGS84, 10, 25,  85, 65);
-        GridGeometry grid1    = new GridGeometry(null, domain,  null);
-        GridGeometry grid2    = new GridGeometry(null, request, null);
+        GridGeometry grid1    = new GridGeometry(null, domain,  GridOrientation.HOMOTHETY);
+        GridGeometry grid2    = new GridGeometry(null, request, GridOrientation.HOMOTHETY);
         GridGeometry subgrid  = grid1.derive().subgrid(grid2).build();
         assertTrue(subgrid.isEnvelopeOnly());
         assertEnvelopeEquals(expected, subgrid.getEnvelope(), STRICT);
@@ -630,7 +630,7 @@ public final strictfp class GridDerivationTest extends TestCase {
          */
         request.setCoordinateReferenceSystem(HardCodedCRS.WGS84_φλ);
         request.setRect(25, -5, 90, 100);
-        grid2   = new GridGeometry(null, request, null);
+        grid2   = new GridGeometry(null, request, GridOrientation.HOMOTHETY);
         subgrid = grid1.derive().subgrid(grid2).build();
         assertSame(HardCodedCRS.WGS84, subgrid.getCoordinateReferenceSystem());
         assertTrue(subgrid.isEnvelopeOnly());
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
index 68763d4..201d777 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
@@ -411,7 +411,7 @@ public final strictfp class GridGeometryTest extends TestCase {
          * Same extent and envelope, but reordering extend dimensions
          * instead than `gridToCRS` columns.
          */
-        grid = new GridGeometry(extent, aoi, GridOrientation.DISPLAY_GRID);
+        grid = new GridGeometry(extent, aoi, GridOrientation.DISPLAY.canReorderGridAxis(true));
         matrix = MathTransforms.getMatrix(grid.getGridToCRS(PixelInCell.CELL_CORNER));
         assertMatrixEquals("cornerToCRS", new Matrix3(
                 0.5,  0,   50,
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridOrientationTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridOrientationTest.java
new file mode 100644
index 0000000..82eda57
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridOrientationTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.referencing.cs.AxesConvention;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link GridOrientation}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final strictfp class GridOrientationTest extends TestCase {
+    /**
+     * Tests {@link GridOrientation#useVariantOfCRS(AxesConvention)}. This test may fail if new enumeration values
+     * are added in {@link AxesConvention} without updating {@link GridOrientation#useVariantOfCRS(AxesConvention)}
+     * code accordingly. The main purpose of this test is to warn the developer if such situation happens.
+     */
+    @Test
+    public void testUseVariantOfCRS() {
+        final GridOrientation test = GridOrientation.DISPLAY;
+        for (final AxesConvention c : AxesConvention.values()) {
+            switch (c) {
+                case POSITIVE_RANGE:   // Fallthrough
+                case RIGHT_HANDED:     assertNotSame(test, test.useVariantOfCRS(c)); break;
+                case DISPLAY_ORIENTED: assertSame   (test, test.useVariantOfCRS(c)); break;
+                default: {
+                    try {
+                        test.useVariantOfCRS(c);
+                        fail("Should not accept " + c);
+                    } catch (IllegalArgumentException e) {
+                        final String message = e.getMessage();
+                        assertTrue(message, message.contains(c.toString()));
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Tests {@link GridOrientation#toString()}.
+     */
+    @Test
+    public void testToString() {
+        assertEquals("GridOrientation[]",
+                GridOrientation.HOMOTHETY.toString());
+        assertEquals("GridOrientation[flip={1}]",
+                GridOrientation.REFLECTION_Y.toString());
+        assertEquals("GridOrientation[flip={1}, crs=DISPLAY_ORIENTED]",
+                GridOrientation.DISPLAY.toString());
+        assertEquals("GridOrientation[flip={1}, crs=DISPLAY_ORIENTED with grid axis reordering]",
+                GridOrientation.DISPLAY.canReorderGridAxis(true).toString());
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
index 53a6bbb..ee67304 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
@@ -440,7 +440,8 @@ public final strictfp class ResampledGridCoverageTest extends TestCase {
     @Test
     public void testSubGeographicArea() throws TransformException {
         final GridCoverage2D source = createCoverage2D();             // Envelope2D(20, 15, 60, 62)
-        GridGeometry gg = new GridGeometry(null, new Envelope2D(HardCodedCRS.WGS84, 18, 20, 17, 31), null);
+        final GridGeometry gg = new GridGeometry(null,
+                new Envelope2D(HardCodedCRS.WGS84, 18, 20, 17, 31), GridOrientation.HOMOTHETY);
         final GridCoverage target = resample(source, gg);
         final GridExtent sourceExtent = source.getGridGeometry().getExtent();
         final GridExtent targetExtent = target.getGridGeometry().getExtent();
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index d757336..fa96385 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -96,6 +96,7 @@ import org.junit.runners.Suite;
     org.apache.sis.coverage.SampleDimensionTest.class,
     org.apache.sis.coverage.SampleRangeFormatTest.class,
     org.apache.sis.coverage.grid.PixelTranslationTest.class,
+    org.apache.sis.coverage.grid.GridOrientationTest.class,
     org.apache.sis.coverage.grid.GridExtentTest.class,
     org.apache.sis.coverage.grid.GridGeometryTest.class,
     org.apache.sis.coverage.grid.GridDerivationTest.class,