sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 26/33: First draft of a GeoTIFF reader capable to compute the GridGeometry.
Date Mon, 18 Jun 2018 21:02:48 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 de981705959e7305097513f9e60640e3012a76d3
Author: Martin Desruisseaux <desruisseaux@apache.org>
AuthorDate: Wed Jun 6 14:54:43 2018 +0000

    First draft of a GeoTIFF reader capable to compute the GridGeometry.
    
    
    git-svn-id: https://svn.apache.org/repos/asf/sis/branches/JDK8@1833032 13f79535-47bb-0310-9956-ffa450edef68
---
 .../org/apache/sis/coverage/grid/GridGeometry.java |  25 ++
 .../apache/sis/coverage/grid/PixelTranslation.java |  62 ++--
 .../apache/sis/coverage/grid/GridGeometryTest.java |  25 +-
 .../sis/coverage/grid/PixelTranslationTest.java    |  21 +-
 .../apache/sis/util/logging/WarningListeners.java  |   8 +-
 .../org/apache/sis/internal/geotiff/Resources.java |   5 +
 .../sis/internal/geotiff/Resources.properties      |   1 +
 .../sis/internal/geotiff/Resources_fr.properties   |   1 +
 .../org/apache/sis/storage/geotiff/CRSBuilder.java | 126 ++++---
 .../sis/storage/geotiff/GridGeometryBuilder.java   | 368 +++++++++++++++++++++
 .../sis/storage/geotiff/ImageFileDirectory.java    | 184 +++++------
 .../apache/sis/storage/geotiff/Localization.java   | 118 +------
 12 files changed, 619 insertions(+), 325 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index ac6777c..30dc210 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.coverage.grid;
 
+import java.util.Arrays;
 import java.util.Objects;
 import java.io.Serializable;
 import java.awt.image.RenderedImage;            // For javadoc only.
@@ -287,6 +288,30 @@ public class GridGeometry implements Serializable {
     }
 
     /**
+     * Creates a grid geometry with only an extent and a coordinate reference system.
+     * This constructor can be used when the <cite>grid to CRS</cite> transform is unknown.
+     *
+     * @param  extent     the valid extent of grid coordinates, or {@code null} if unknown.
+     * @param  crs        the coordinate reference system of the "real world" coordinates, or {@code null} if unknown.
+     * @throws NullPointerException if {@code extent} and {@code crs} arguments are both null.
+     */
+    public GridGeometry(final GridExtent extent, final CoordinateReferenceSystem crs) {
+        this.extent = extent;
+        gridToCRS   = null;
+        cornerToCRS = null;
+        resolution  = null;
+        nonLinears  = 0;
+        if (crs == null) {
+            ArgumentChecks.ensureNonNull("extent", extent);
+            envelope = null;
+        } else {
+            final double[] coords = new double[crs.getCoordinateSystem().getDimension()];
+            Arrays.fill(coords, Double.NaN);
+            envelope = new ImmutableEnvelope(coords, coords, crs);
+        }
+    }
+
+    /**
      * Returns the number of dimensions of the <em>grid</em>. This is typically the same
      * than the number of {@linkplain #getEnvelope() envelope} dimensions or the number of
      * {@linkplain #getCoordinateReferenceSystem() coordinate reference system} dimensions,
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
index e1598a3..d63564b 100644
--- 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
@@ -55,11 +55,11 @@ import org.apache.sis.referencing.operation.transform.MathTransforms;
  * {@preformat java
  *   final AffineTransform  gridToCRS = ...;
  *   final PixelOrientation current   = PixelOrientation.CENTER;
- *   final PixelOrientation expected  = PixelOrientation.UPPER_LEFT;
+ *   final PixelOrientation desired   = PixelOrientation.UPPER_LEFT;
  *
- *   // Switch the transform from 'current' to 'expected' convention.
+ *   // Switch the transform from 'current' to 'desired' convention.
  *   final PixelTranslation source = getPixelTranslation(current);
- *   final PixelTranslation target = getPixelTranslation(expected);
+ *   final PixelTranslation target = getPixelTranslation(desired);
  *   gridToCRS.translate(target.dx - source.dx,
  *                       target.dy - source.dy);
  * }
@@ -224,30 +224,29 @@ public final class PixelTranslation extends Static implements Serializable {
      * If the two given conventions are the same, then this method returns the given transform unchanged.
      *
      * <div class="note"><b>Example:</b>
-     * if a given {@code gridToCRS} was mapping the pixel corner to "real world" coordinates, then a call to
-     * <code>translate(gridToCRS, {@link PixelInCell#CELL_CORNER}, {@link PixelInCell#CELL_CENTER})</code>
-     * will return a new transform performing the following steps:
-     * <ol>
-     *   <li>Translate the grid coordinates by +0.5, so that the (0,0) coordinate in "pixel center" convention
-     *       map to (½,½) in "pixel corner" convention.</li>
-     *   <li>Apply the transform described by the "pixel corner" convention.</li>
-     * </ol></div>
+     * if a given {@code gridToCRS} transform was mapping the <em>cell corner</em> to "real world" coordinates, then a call to
+     * <code>translate(gridToCRS, {@link PixelInCell#CELL_CORNER CELL_CORNER}, {@link PixelInCell#CELL_CENTER CELL_CENTER})</code>
+     * will return a new transform performing the following steps: first convert grid coordinates from <var>cell center</var>
+     * convention ({@code desired}) to <var>cell corner</var> convention ({@code current}), then concatenate the given
+     * {@code gridToCRS} transform which was designed for the <em>cell corner</em> convention.
+     * The above-cited <var>cell center</var> → <var>cell corner</var> conversion is done by translating the grid coordinates
+     * by +½, because the grid coordinates (0,0) relative to cell center is (½,½) relative to cell corner.</div>
      *
      * If the given {@code gridToCRS} is null, then this method ignores all other arguments and returns {@code null}.
-     * Otherwise {@code current} and {@code expected} arguments must be non-null.
+     * Otherwise {@code current} and {@code desired} arguments must be non-null.
      *
      * @param  gridToCRS  a math transform from <cite>pixel</cite> coordinates to any CRS, or {@code null}.
      * @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}, or {@code null} if {@code gridToCRS} was null.
-     * @throws IllegalArgumentException if {@code current} or {@code expected} is not a known code list value.
+     * @param  desired    the pixel orientation of the desired transform.
+     * @return the translation from {@code current} to {@code desired}, or {@code null} if {@code gridToCRS} was null.
+     * @throws IllegalArgumentException if {@code current} or {@code desired} is not a known code list value.
      */
-    public static MathTransform translate(final MathTransform gridToCRS, final PixelInCell current, final PixelInCell expected) {
-        if (gridToCRS == null || expected.equals(current)) {
+    public static MathTransform translate(final MathTransform gridToCRS, final PixelInCell current, final PixelInCell desired) {
+        if (gridToCRS == null || desired.equals(current)) {
             return gridToCRS;
         }
         final int dimension = gridToCRS.getSourceDimensions();
-        final double offset = getPixelTranslation(expected) - getPixelTranslation(current);
+        final double offset = getPixelTranslation(desired) - getPixelTranslation(current);
         final int ci;               // Cache index.
         if (offset == -0.5) {
             ci = 2*dimension - 2;
@@ -275,32 +274,27 @@ public final class PixelTranslation extends Static implements Serializable {
      * The given transform can have any number of input and output dimensions, but only two of them will be converted.
      *
      * <div class="note"><b>Example:</b>
-     * if a given {@code gridToCRS} was mapping the upper-left corner to "real world" coordinates, then a call to
-     * <code>translate(gridToCRS, {@link PixelOrientation#UPPER_LEFT}, {@link PixelOrientation#CENTER}, 0, 1)</code>
-     * will return a new transform performing the following steps:
-     * <ol>
-     *   <li>Translate the grid coordinates by +0.5, so that the (0,0) coordinate in "pixel center" convention
-     *       map to (½,½) in "upper-left corner" convention. This translation is applied only in the specified
-     *       grid (source) dimensions; all other dimensions (if any) are left unchanged.</li>
-     *   <li>Apply the transform described by the "upper-left corner" convention.</li>
-     * </ol></div>
+     * if a given {@code gridToCRS} transform was mapping the upper-left corner to "real world" coordinates, then a call to
+     * <code>translate(gridToCRS, {@link PixelOrientation#UPPER_LEFT UPPER_LEFT}, {@link PixelOrientation#CENTER CENTER}, 0, 1)</code>
+     * will return a new transform translating grid coordinates by +0.5 before to apply the given {@code gridToCRS} transform.
+     * See example in above {@link #translate(MathTransform, PixelInCell, PixelInCell) translate} method for more details.</div>
      *
      * If the given {@code gridToCRS} is null, then this method ignores all other arguments and returns {@code null}.
-     * Otherwise {@code current} and {@code expected} arguments must be non-null.
+     * Otherwise {@code current} and {@code desired} arguments must be non-null.
      *
      * @param  gridToCRS   a math transform from <cite>pixel</cite> coordinates to any CRS, or {@code null}.
      * @param  current     the pixel orientation of the given {@code gridToCRS} transform.
-     * @param  expected    the pixel orientation of the desired transform.
+     * @param  desired     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}, or {@code null} if {@code gridToCRS} was null.
-     * @throws IllegalArgumentException if {@code current} or {@code expected} is not a known code list value.
+     * @return the translation from {@code current} to {@code desired}, or {@code null} if {@code gridToCRS} was null.
+     * @throws IllegalArgumentException if {@code current} or {@code desired} is not a known code list value.
      */
     public static MathTransform translate(final MathTransform gridToCRS,
-            final PixelOrientation current, final PixelOrientation expected,
+            final PixelOrientation current, final PixelOrientation desired,
             final int xDimension, final int yDimension)
     {
-        if (gridToCRS == null || expected.equals(current)) {
+        if (gridToCRS == null || desired.equals(current)) {
             return gridToCRS;
         }
         final int dimension = gridToCRS.getSourceDimensions();
@@ -314,7 +308,7 @@ public final class PixelTranslation extends Static implements Serializable {
             throw illegalDimension("xDimension", "yDimension");
         }
         final PixelTranslation source = getPixelTranslation(current);
-        final PixelTranslation target = getPixelTranslation(expected);
+        final PixelTranslation target = getPixelTranslation(desired);
         final double dx = target.dx - source.dx;
         final double dy = target.dy - source.dy;
         MathTransform mt;
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
index 8950546..164907e 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java
@@ -19,6 +19,7 @@ package org.apache.sis.coverage.grid;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.operation.matrix.Matrix4;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -52,7 +53,7 @@ public final strictfp class GridGeometryTest extends TestCase {
      * @throws TransformException if an error occurred while using the "grid to CRS" transform.
      */
     @Test
-    public void testSimple() throws TransformException {
+    public void testFromPixelCorner() throws TransformException {
         final long[]         low     = new long[] {100, 300, 3, 6};
         final long[]         high    = new long[] {200, 400, 4, 7};
         final GridExtent    extent   = new GridExtent(low, high, true);
@@ -98,7 +99,7 @@ public final strictfp class GridGeometryTest extends TestCase {
      * @throws TransformException if an error occurred while using the "grid to CRS" transform.
      */
     @Test
-    public void testShifted() throws TransformException {
+    public void testFromPixelCenter() throws TransformException {
         final long[]        low      = new long[] { 0,   0, 2};
         final long[]        high     = new long[] {99, 199, 4};
         final GridExtent    extent   = new GridExtent(low, high, true);
@@ -137,6 +138,26 @@ public final strictfp class GridGeometryTest extends TestCase {
     }
 
     /**
+     * Tests construction from a <cite>grid to CRS</cite> having a 0.5 pixel translation.
+     * This translation happens in transform mapping <cite>pixel center</cite> when the
+     * corresponding <cite>pixel corner</cite> transformation is identity.
+     *
+     * @throws TransformException if an error occurred while using the "grid to CRS" transform.
+     */
+    @Test
+    public void testShifted() throws TransformException {
+        final long[]        low      = new long[] {100, 300};
+        final long[]        high     = new long[] {200, 400};
+        final GridExtent    extent   = new GridExtent(low, high, true);
+        final MathTransform identity = MathTransforms.linear(new Matrix3(
+                1, 0, 0.5,
+                0, 1, 0.5,
+                0, 0, 1));
+        final GridGeometry grid = new GridGeometry(extent, PixelInCell.CELL_CENTER, identity, null);
+        assertTrue("gridToCRS.isIdentity", grid.getGridToCRS(PixelInCell.CELL_CORNER).isIdentity());
+    }
+
+    /**
      * Tests construction with a non-linear component in the transform.
      *
      * @throws TransformException if an error occurred while using the "grid to CRS" transform.
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/PixelTranslationTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/PixelTranslationTest.java
index cefeeb7..e4498f3 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/PixelTranslationTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/PixelTranslationTest.java
@@ -19,6 +19,7 @@ package org.apache.sis.coverage.grid;
 import org.opengis.metadata.spatial.PixelOrientation;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.operation.matrix.Matrix4;
@@ -52,20 +53,34 @@ public final strictfp class PixelTranslationTest extends TestCase {
     }
 
     /**
-     * Tests {@link PixelTranslation#translate(MathTransform, PixelInCell, PixelInCell)}.
+     * Tests {@link PixelTranslation#translate(MathTransform, PixelInCell, PixelInCell)} with an identity transform.
+     * If grid coordinates (0,0) in "pixel center" convention map to (0,0) in "real world" coordinates,
+     * then grid coordinates (0,0) in "pixel corner" convention shall map to (-½, -½) in real world.
+     * That way, grid coordinates (½,½) in "pixel corner" convention still map to (0,0) in real world.
+     *
+     * @throws TransformException if an error occurred while transforming a test point.
      */
     @Test
-    public void testTranslatePixelInCell() {
+    public void testTranslatePixelInCell() throws TransformException {
         final MathTransform mt = centerToCorner(3);
         assertMatrixEquals("center → corner", new Matrix4(
                 1, 0, 0, -0.5,
                 0, 1, 0, -0.5,
                 0, 0, 1, -0.5,
                 0, 0, 0,  1), MathTransforms.getMatrix(mt), STRICT);
+        /*
+         * Just for making clear what we explained in javadoc comment: the real world (0,0,0) coordinates was in the center
+         * of cell (0,0,0). After we switched to "cell corner" convention, that center is (½,½,½) in grid coordinates but
+         * should still map (0,0,0) in "real world" coordinates.
+         */
+        final double[] coordinates = new double[] {0.5, 0.5, 0.5};
+        mt.transform(coordinates, 0, coordinates, 0, 1);
+        assertArrayEquals(new double[3], coordinates, STRICT);
     }
 
     /**
      * Tests {@link PixelTranslation#translate(MathTransform, PixelOrientation, PixelOrientation, int, int)}.
+     * See {@link #testTranslatePixelInCell()} for discussion on expected values.
      */
     @Test
     public void testTranslatePixelOrientation() {
@@ -76,7 +91,7 @@ public final strictfp class PixelTranslationTest extends TestCase {
                 0, 0,  1), MathTransforms.getMatrix(mt), STRICT);
 
         mt = PixelTranslation.translate(MathTransforms.identity(3), PixelOrientation.LOWER_LEFT, PixelOrientation.CENTER, 1, 2);
-        assertMatrixEquals("center → corner", new Matrix4(
+        assertMatrixEquals("corner → center", new Matrix4(
                 1, 0, 0,  0.0,
                 0, 1, 0, +0.5,
                 0, 0, 1, -0.5,
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java b/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java
index 47fcebd..ac8709b 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java
@@ -71,7 +71,7 @@ import org.apache.sis.internal.util.UnmodifiableArrayList;
 public class WarningListeners<S> implements Localized {
     /**
      * The declared source of warnings. This is not necessarily the real source,
-     * but this is the source that the implementor wants to declare as public API.
+     * but this is the source that the implementer wants to declare as public API.
      */
     private final S source;
 
@@ -95,7 +95,7 @@ public class WarningListeners<S> implements Localized {
      * unless at least one listener is {@linkplain #addWarningListener registered}.
      *
      * @param source  the declared source of warnings. This is not necessarily the real source,
-     *                but this is the source that the implementor wants to declare as public API.
+     *                but this is the source that the implementer wants to declare as public API.
      */
     public WarningListeners(final S source) {
         ArgumentChecks.ensureNonNull("source", source);
@@ -108,7 +108,7 @@ public class WarningListeners<S> implements Localized {
      * be duplicated for concurrency reasons.
      *
      * @param source  the declared source of warnings. This is not necessarily the real source,
-     *                but this is the source that the implementor wants to declare as public API.
+     *                but this is the source that the implementer wants to declare as public API.
      * @param other   the existing instance from which to copy the listeners, or {@code null} if none.
      *
      * @since 0.8
@@ -231,7 +231,7 @@ public class WarningListeners<S> implements Localized {
      * If the stack trace is desired, then users can either:
      * <ul>
      *   <li>invoke {@code warning(LogRecord)} directly, or</li>
-     *   <li>override {@code warning(LogRecord)} and invoke {@link LogRecord#setThrown(Throwable)} explicitely, or</li>
+     *   <li>override {@code warning(LogRecord)} and invoke {@link LogRecord#setThrown(Throwable)} explicitly, or</li>
      *   <li>register a listener which will log the record itself.</li>
      * </ul>
      *
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources.java
index 9bc187a..278ed2d 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources.java
@@ -61,6 +61,11 @@ public final class Resources extends IndexedResourceBundle {
         }
 
         /**
+         * Can not compute the grid geometry of “{0}” TIFF file.
+         */
+        public static final short CanNotComputeGridGeometry_1 = 26;
+
+        /**
          * TIFF file “{0}” has circular references in its chain of images.
          */
         public static final short CircularImageReference_1 = 1;
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources.properties b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources.properties
index aa75321..219bc40 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources.properties
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources.properties
@@ -19,6 +19,7 @@
 # Resources in this file are for "sis-geotiff" usage only and should not be used by any other module.
 # For resources shared by all modules in the Apache SIS project, see "org.apache.sis.util.resources" package.
 #
+CanNotComputeGridGeometry_1       = Can not compute the grid geometry of \u201c{0}\u201d TIFF file.
 CircularImageReference_1          = TIFF file \u201c{0}\u201d has circular references in its chain of images.
 ConstantValueRequired_3           = Apache SIS implementation requires that all \u201c{0}\u201d elements have the same value, but the element found in \u201c{1}\u201d are {2}.
 ComputedValueForAttribute_2       = No value specified for the \u201c{0}\u201d TIFF tag. Computed the {1} value from other tags.
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources_fr.properties b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources_fr.properties
index 669605a..b778c54 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources_fr.properties
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Resources_fr.properties
@@ -24,6 +24,7 @@
 #   U+202F NARROW NO-BREAK SPACE  before  ; ! and ?
 #   U+00A0 NO-BREAK SPACE         before  :
 #
+CanNotComputeGridGeometry_1       = Ne peut pas calculer la g\u00e9om\u00e9trie de la grille du fichier TIFF \u00ab\u202f{0}\u202f\u00bb.
 CircularImageReference_1          = Le fichier TIFF \u00ab\u202f{0}\u202f\u00bb a des r\u00e9f\u00e9rences circulaires dans sa cha\u00eene d\u2019images.
 ConstantValueRequired_3           = L\u2019impl\u00e9mentation de Apache SIS requiert que tous les \u00e9l\u00e9ments de \u00ab\u202f{0}\u202f\u00bb aient la m\u00eame valeur, mais les \u00e9l\u00e9ments trouv\u00e9s dans \u00ab\u202f{1}\u202f\u00bb sont {2}.
 ComputedValueForAttribute_2       = Aucune valeur n\u2019a \u00e9t\u00e9 sp\u00e9cifi\u00e9e pour le tag TIFF \u00ab\u202f{0}\u202f\u00bb. La valeur {1} a \u00e9t\u00e9 calcul\u00e9e \u00e0 partir des autres tags.
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
index 5cffea1..237fa2d 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
@@ -35,7 +35,6 @@ import javax.measure.quantity.Length;
 
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.spatial.CellGeometry;
-import org.opengis.metadata.spatial.PixelOrientation;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.referencing.IdentifiedObject;
@@ -64,7 +63,6 @@ import org.apache.sis.internal.metadata.WKTKeywords;
 import org.apache.sis.internal.referencing.CoordinateOperations;
 import org.apache.sis.internal.referencing.NilReferencingObject;
 import org.apache.sis.internal.referencing.ReferencingUtilities;
-import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.Utilities;
@@ -203,7 +201,7 @@ final class CRSBuilder {
      * so we can easily detect at the end of the parsing process which GeoTIFF keys were
      * unrecognized or ignored.
      */
-    private final Map<Short,Object> geoKeys = new HashMap<>();
+    private final Map<Short,Object> geoKeys;
 
     /**
      * Factory for creating geodetic objects from EPSG codes, or {@code null} if not yet fetched.
@@ -247,6 +245,19 @@ final class CRSBuilder {
     private Identifier lastName;
 
     /**
+     * Suggested value for a general description of the transformation form grid coordinates to "real world" coordinates.
+     * This is computed by {@link #build(Vector, Vector, String)} and made available as additional information to the caller.
+     */
+    public String description;
+
+    /**
+     * {@code POINT} if {@link GeoKeys#RasterType} is {@link GeoCodes#RasterPixelIsPoint},
+     * {@code AREA} if it is {@link GeoCodes#RasterPixelIsArea}, or null if unspecified.
+     * This is computed by {@link #build(Vector, Vector, String)} and made available to the caller.
+     */
+    public CellGeometry cellGeometry;
+
+    /**
      * {@code true} when an exception has been thrown but this {@code CRSBuilder} already reported a warning,
      * so there is no need for the caller to report a warning again. {@code CRSBuilder} sometime reports warnings
      * itself when it can provide a better warning message than what the caller can do.
@@ -260,6 +271,7 @@ final class CRSBuilder {
      */
     CRSBuilder(final Reader reader) {
         this.reader = reader;
+        geoKeys = new HashMap<>();
     }
 
     /**
@@ -483,7 +495,7 @@ final class CRSBuilder {
 
     /**
      * Verifies that a value found in the GeoTIFF file is approximatively equal to the expected value.
-     * This method is invoked when a CRS component is defined both explicitely and by EPSG code,
+     * This method is invoked when a CRS component is defined both explicitly and by EPSG code,
      * in which case we expect the given value to be equal to the value fetched from the EPSG database.
      * If the values do not match, a warning is reported and the caller should use the EPSG value.
      *
@@ -554,6 +566,8 @@ final class CRSBuilder {
     /**
      * Decodes all the given GeoTIFF keys, then creates a coordinate reference system.
      * An overview of the key directory structure is given in {@linkplain CRSBuilder class javadoc}.
+     * The {@link #description} and {@link #cellGeometry} fields are set as a side-effect.
+     * A warning is emitted if any GeoTIFF tags were ignored.
      *
      * @param  keyDirectory       the GeoTIFF keys to be associated to values. Can not be null.
      * @param  numericParameters  a vector of {@code double} parameters, or {@code null} if none.
@@ -566,7 +580,7 @@ final class CRSBuilder {
      * @throws FactoryException if an error occurred during objects creation with the factories.
      */
     @SuppressWarnings("null")
-    final CoordinateReferenceSystem build(final Vector keyDirectory, final Vector numericParameters, final String asciiParameters)
+    public CoordinateReferenceSystem build(final Vector keyDirectory, final Vector numericParameters, final String asciiParameters)
             throws FactoryException
     {
         final int numberOfKeys;
@@ -703,80 +717,44 @@ final class CRSBuilder {
             geoKeys.put(key, value);
         }
         /*
-         * At this point we finished copying all GeoTIFF keys in 'CRSBuilder.geoKeys' map.
+         * At this point we finished copying all GeoTIFF keys in the 'geoKeys' map. Before to create the CRS,
+         * store a few metadata. The first one is an ASCII reference to published documentation on the overall
+         * configuration of the GeoTIFF file. In practice it seems to be often the projected CRS name, despite
+         * GeoKeys.PCSCitation being already for that purpose.
+         */
+        description = getAsString(GeoKeys.Citation);
+        int code = getAsInteger(GeoKeys.RasterType);
+        switch (code) {
+            case GeoCodes.undefined: break;
+            case GeoCodes.RasterPixelIsArea:  cellGeometry = CellGeometry.AREA;  break;
+            case GeoCodes.RasterPixelIsPoint: cellGeometry = CellGeometry.POINT; break;
+            default: invalidValue(GeoKeys.RasterType, code); break;
+        }
+        /*
          * First create the main coordinate reference system, as determined by 'ModelType'.
          * Then if a vertical CRS exists and the main CRS is not geocentric (in which case
          * adding a vertical CRS would make no sense), create a three-dimensional compound CRS.
          */
-        CoordinateReferenceSystem crs;
+        CoordinateReferenceSystem crs = null;
         final int crsType = getAsInteger(GeoKeys.ModelType);
         switch (crsType) {
-            case GeoCodes.undefined:           return null;
-            case GeoCodes.ModelTypeProjected:  crs = createProjectedCRS(); break;
-            case GeoCodes.ModelTypeGeocentric: return createGeocentricCRS();        // Ignore vertical CRS.
-            case GeoCodes.ModelTypeGeographic: {
-                crs = createGeographicCRS(true,
-                        createUnit(GeoKeys.AngularUnits, GeoKeys.AngularUnitSize, Angle.class, Units.DEGREE));
-                break;
-            }
-            default: {
-                warning(Resources.Keys.UnsupportedCoordinateSystemKind_1, crsType);
-                return null;
-            }
+            case GeoCodes.undefined:           break;
+            case GeoCodes.ModelTypeProjected:  crs = createProjectedCRS();  break;
+            case GeoCodes.ModelTypeGeocentric: crs = createGeocentricCRS(); break;
+            case GeoCodes.ModelTypeGeographic: crs = createGeographicCRS(); break;
+            default: warning(Resources.Keys.UnsupportedCoordinateSystemKind_1, crsType); break;
         }
-        final VerticalCRS vertical = createVerticalCRS();
-        if (vertical != null) {
-            crs = objectFactory().createCompoundCRS(Collections.singletonMap(IdentifiedObject.NAME_KEY, crs.getName()), crs, vertical);
-        }
-        return crs;
-    }
-
-    /**
-     * Completes ISO 19115 metadata with some GeoTIFF values that are for documentation purposes.
-     * Those values do not participate directly to the construction of the Coordinate Reference System objects.
-     *
-     * <p><b>Pre-requite:</b></p>
-     * <ul>
-     *   <li>{@link #build(Vector, Vector, String)} must have been invoked before this method.</li>
-     *   <li>{@link ImageFileDirectory} must have filled its part of metadata before to invoke this method.</li>
-     *   <li>{@link MetadataBuilder#newGridRepresentation(MetadataBuilder.GridType)} should have been invoked
-     *       with the appropriate {@code GEORECTIFIED} or {@code GEOREFERENCEABLE} type.</li>
-     * </ul>
-     *
-     * After execution, this method emits a warning for unprocessed GeoTIFF tags.
-     *
-     * @param  metadata  the helper class where to write metadata values.
-     * @throws NumberFormatException if a numeric value was stored as a string and can not be parsed.
-     */
-    final void complete(final MetadataBuilder metadata) {
-        /*
-         * ASCII reference to published documentation on the overall configuration of the GeoTIFF file.
-         * Often the projected CRS name, despite GeoKeys.PCSCitation being already for that purpose.
-         * Checked first because this code is unlikely to throw an exception, while other parsings may
-         * interrupt this method with an exception.
-         */
-        final String title = getAsString(GeoKeys.Citation);
-        if (title != null) {
-            metadata.setGridToCRS(title);
-        }
-        /*
-         * Whether the pixel value is thought of as filling the cell area or is considered as point measurements at
-         * the vertices of the grid (not in the interior of a cell).  This is determined by the value associated to
-         * GeoKeys.RasterType, which can be GeoCodes.RasterPixelIsArea or GeoCodes.RasterPixelIsPoint.
-         */
-        CellGeometry     cg = null;
-        PixelOrientation po = null;
-        int code = getAsInteger(GeoKeys.RasterType);
-        switch (code) {
-            case GeoCodes.undefined: break;
-            case GeoCodes.RasterPixelIsArea:  cg = CellGeometry.AREA;  po = PixelOrientation.CENTER;     break;
-            case GeoCodes.RasterPixelIsPoint: cg = CellGeometry.POINT; po = PixelOrientation.UPPER_LEFT; break;
-            default: invalidValue(GeoKeys.RasterType, code); break;
+        if (crsType != GeoCodes.ModelTypeGeocentric) {
+            final VerticalCRS vertical = createVerticalCRS();
+            if (crs == null) {
+                crs = vertical;
+            } else if (vertical != null) {
+                crs = objectFactory().createCompoundCRS(Collections.singletonMap(IdentifiedObject.NAME_KEY, crs.getName()), crs, vertical);
+            }
         }
-        metadata.setCellGeometry(cg);
-        metadata.setPointInPixel(po);
         /*
-         * Build a list of remaining GeoKeys.
+         * At this point we finished parsing all GeoTIFF tags, both for metadata purpose or for CRS construction.
+         * Emit a warning for unprocessed GeoTIFF tags. A single warning is emitted for all ignored tags.
          */
         if (!geoKeys.isEmpty()) {
             final StringJoiner joiner = new StringJoiner(", ");
@@ -785,6 +763,7 @@ final class CRSBuilder {
             }
             warning(Resources.Keys.IgnoredGeoKeys_1, joiner.toString());
         }
+        return crs;
     }
 
     /**
@@ -1216,6 +1195,13 @@ final class CRSBuilder {
     }
 
     /**
+     * Creates the main CRS in the case where that CRS is geographic.
+     */
+    private GeographicCRS createGeographicCRS() throws FactoryException {
+        return createGeographicCRS(true, createUnit(GeoKeys.AngularUnits, GeoKeys.AngularUnitSize, Angle.class, Units.DEGREE));
+    }
+
+    /**
      * Creates a geographic CRS from an EPSG code or from user-defined parameters.
      * The GeoTIFF values used by this method are:
      *
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
new file mode 100644
index 0000000..5badf79
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
@@ -0,0 +1,368 @@
+/*
+ * 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.storage.geotiff;
+
+import java.util.NoSuchElementException;
+import org.opengis.util.FactoryException;
+import org.opengis.util.NoSuchIdentifierException;
+import org.opengis.metadata.spatial.CellGeometry;
+import org.opengis.metadata.spatial.PixelOrientation;
+import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.NoSuchAuthorityCodeException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.internal.storage.MetadataBuilder;
+import org.apache.sis.internal.geotiff.Resources;
+import org.apache.sis.internal.util.DoubleDouble;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.math.Vector;
+
+
+/**
+ * Helper class for creating a {@link GridGeometry} from GeoTIFF data.
+ * The coordinate reference system part is built by {@link CRSBuilder}.
+ *
+ * <div class="section">Pixel center versus pixel corner</div>
+ * The policy about whether the conversion map pixel corner or pixel center if GeoTIFF files does not seem
+ * totally clear. But the practice at least with GDAL seems to consider the following as equivalent:
+ *
+ * {@preformat text
+ *     ModelTiepointTag = (0.0, 0.0, 0.0, -180.0, 90.0, 0.0)
+ *     ModelPixelScaleTag = (0.002777777778, 0.002777777778, 0.0)
+ *     GeoKeyDirectoryTag:
+ *         GTModelTypeGeoKey    = 2    (ModelTypeGeographic)
+ *         GTRasterTypeGeoKey   = 1    (RasterPixelIsArea)
+ *         GeographicTypeGeoKey = 4326 (GCS_WGS_84)
+ * }
+ *
+ * and
+ *
+ * {@preformat text
+ *     ModelTiepointTag = (-0.5, -0.5, 0.0, -180.0, 90.0, 0.0)
+ *     ModelPixelScaleTag = (0.002777777778, 0.002777777778, 0.0)
+ *     GeoKeyDirectoryTag:
+ *         GTModelTypeGeoKey    = 2    (ModelTypeGeographic)
+ *         GTRasterTypeGeoKey   = 2    (RasterPixelIsPoint)
+ *         GeographicTypeGeoKey = 4326 (GCS_WGS_84)
+ * }
+ *
+ * The former is {@link PixelInCell#CELL_CORNER} convention while the later is {@link PixelInCell#CELL_CENTER}.
+ * Note that the translation coefficients in the <cite>grid to CRS</cite> matrix is {@code crs - grid × scale}.
+ * So compared to the {@code CELL_CORNER} case, the {@code CELL_CENTER} case has a translation of +0.5 × scale.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class GridGeometryBuilder {
+    /**
+     * The reader for which we will create coordinate reference systems.
+     * This is used for reporting warnings.
+     */
+    private final Reader reader;
+
+    ////////////////////////////////////////////////////////////////////////////////////////
+    ////                                                                                ////
+    ////    Information to be set by ImageFileDirectory during GeoTIFF file parsing.    ////
+    ////                                                                                ////
+    ////////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * References the {@link GeoKeys} needed for building the Coordinate Reference System.
+     */
+    public Vector keyDirectory;
+
+    /**
+     * The numeric values referenced by the {@link #keyDirectory}.
+     */
+    public Vector numericParameters;
+
+    /**
+     * The characters referenced by the {@link #keyDirectory}.
+     */
+    public String asciiParameters;
+
+    /**
+     * Raster model tie points. This vector contains ordinate values structured as (I,J,K, X,Y,Z) records.
+     * The (I,J,K) ordinate values specify the point at location (I,J) in raster space with pixel-value K,
+     * and (X,Y,Z) ordinate values specify the point in the Coordinate Reference System. In most cases the
+     * coordinate system is only two-dimensional, in which case both K and Z should be set to zero.
+     */
+    public Vector modelTiePoints;
+
+    /**
+     * The conversion from grid coordinates to CRS coordinates as an affine transform.
+     * The "grid to CRS" transform can be determined in different ways, from simpler to more complex:
+     *
+     * <ul>
+     *   <li>By a combination of a single {@link #modelTiePoints} with the 3 values given in
+     *       {@link Tags#ModelPixelScaleTag} as documented in the Javadoc of that tag.</li>
+     *   <li>By a {@link Tags#ModelTransformation} giving all coefficients of the 4×4 matrix}.
+     *       Note that the third row and the third column have all their value set to 0 if the
+     *       space model (or the coordinate reference system) should be two-dimensional.</li>
+     *   <li>By building a non-linear transformation from all {@link #modelTiePoints}.
+     *       Such transformation can not be stored in a matrix, so will leave this field {@code null}.</li>
+     * </ul>
+     *
+     * By convention, the translation column is set to NaN values if it needs to be computed from the tie point.
+     */
+    private MatrixSIS affine;
+
+    /**
+     * {@code true} if {@link #affine} has been specified by a complete matrix ({@link Tags#ModelTransformation}),
+     * or {@code false} if it has been specified by the scale factors only ({@link Tags#ModelPixelScaleTag}).
+     */
+    private boolean completeMatrixSpecified;
+
+    /**
+     * Sets the {@link #affine} transform from a complete matrix.
+     *
+     * @param  terms  the matrix in a row-major fashion.
+     * @param  size   the matrix size, either 3 or 4.
+     */
+    public void setGridToCRS(final Vector terms, final int size) {
+        final int length = terms.size();
+        completeMatrixSpecified = true;
+        affine = Matrices.createZero(size, size);
+        affine.setElement(size-1, size-1, 1);
+        for (int i=0; i<length; i++) {
+            affine.setElement(i / size, i % size, terms.doubleValue(i));
+        }
+    }
+
+    /**
+     * Sets only the scale terms of the {@link #affine} transform.
+     * The translation terms are set to NaN, meaning they will need to be determined later.
+     */
+    public void setScaleFactors(final Vector terms) {
+        final int size = terms.size();
+        completeMatrixSpecified = false;
+        affine = Matrices.createZero(size+1, size+1);
+        affine.setElement(size, size, 1);
+        for (int i=0; i<size; i++) {
+            double e = terms.doubleValue(i);
+            if (i == 1) e = -e;                             // Make y scale factor negative.
+            affine.setElement(i, i, e);
+            affine.setElement(i, size, Double.NaN);
+        }
+    }
+
+
+
+    ////////////////////////////////////////////////////////////////////////////////////////
+    ////                                                                                ////
+    ////    Information to be computed by GridGeometryBuilder based on above data.      ////
+    ////                                                                                ////
+    ////////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * The grid geometry to be created by {@link #build(GridExtent)}.
+     */
+    public GridGeometry gridGeometry;
+
+    /**
+     * Suggested value for a general description of the transformation form grid coordinates to "real world" coordinates.
+     * This information is obtained as a side-effect of {@link #build(GridExtent)} call.
+     */
+    private String description;
+
+    /**
+     * {@code POINT} if {@link GeoKeys#RasterType} is {@link GeoCodes#RasterPixelIsPoint},
+     * {@code AREA} if it is {@link GeoCodes#RasterPixelIsArea}, or null if unspecified.
+     * This information is obtained as a side-effect of {@link #build(GridExtent)} call.
+     */
+    private CellGeometry cellGeometry;
+
+    /**
+     * Creates a new builder.
+     *
+     * @param reader  where to report warnings if any.
+     */
+    GridGeometryBuilder(final Reader reader) {
+        this.reader = reader;
+    }
+
+    /**
+     * If {@link #affine} has been specified with only the scale factor, computes the translation terms now.
+     * If needed, this method computes the translation terms from the (usually singleton) tie point.
+     * This happen when the GeoTIFF file has a {@link Tags#ModelPixelScaleTag} and {@link Tags#ModelTiePoints}.
+     * The later should have a single record.
+     *
+     * @return {@code true} on success (including nothing to compute), or {@code false} if the computation attempt
+     *         failed because of missing {@link Tags#ModelTiePoints}.
+     *
+     * @see ImageFileDirectory#validateMandatoryTags()
+     */
+    public boolean validateMandatoryTags() {
+        final MatrixSIS affine = this.affine;
+        if (affine == null || completeMatrixSpecified) {
+            return true;
+        }
+        final Vector modelTiePoints = this.modelTiePoints;
+        if (modelTiePoints != null) {
+            /*
+             * The GeoTIFF specification recommends that the first point is located at grid indices (0,0).
+             * But as a safety, we will nevertheless search in the grid for the point closest to origin.
+             * If the grid is affine, using the corner closest to (0,0) reduces rounding errors compared
+             * to using another corner. If the grid is not affine, then ModelPixelScaleTag should not have
+             * been defined for that file…
+             */
+            int nearest = 0;                                // Index of the record nearest to origin.
+            double distance = Double.POSITIVE_INFINITY;     // Distance squared of the nearest record.
+            final int size = modelTiePoints.size();
+            for (int i=0; i<size; i += Localization.RECORD_LENGTH) {
+                double t;
+                final double d = (t = modelTiePoints.doubleValue(i    )) * t
+                               + (t = modelTiePoints.doubleValue(i + 1)) * t
+                               + (t = modelTiePoints.doubleValue(i + 2)) * t;
+                if (d < distance) {
+                    distance = d;
+                    nearest = i;
+                    if (d == 0) break;                      // Optimization for the standard case.
+                }
+            }
+            /*
+             * Grid to CRS conversion:  crs = grid × scale + translation
+             * We rearrange as:         translation = crs - grid × scale
+             * where:                   grid   =  modelTiePoints[i]
+             *                          crs    =  modelTiePoints[i + RECORD_LENGTH/2]
+             *                          scale  =  affine(i,i)  —  on the diagonal
+             */
+            if (distance != Double.POSITIVE_INFINITY) {
+                final DoubleDouble t = new DoubleDouble();
+                final int numDim = affine.getNumRow() - 1;
+                final int trCol  = affine.getNumCol() - 1;
+                for (int j=0; j<numDim; j++) {
+                    t.value = -modelTiePoints.doubleValue(nearest + j);
+                    t.error = DoubleDouble.errorForWellKnownValue(t.value);
+                    t.multiply(affine.getNumber(j, j));
+                    t.add(modelTiePoints.doubleValue(nearest + j + Localization.RECORD_LENGTH / 2));
+                    affine.setNumber(j, trCol, t);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Creates the grid geometry and collect related metadata.
+     * This method shall be invoked exactly once after {@link #validateMandatoryTags()}.
+     * After this method call (if successful), {@link #gridGeometry} is guaranteed non-null
+     * and can be used as a flag for determining that the build has been completed.
+     *
+     * @param  extent  the image width and height in pixels. Must be two-dimensional.
+     * @return {@link #gridGeometry}, guaranteed non-null.
+     * @throws FactoryException if an error occurred while creating a CRS or a transform.
+     */
+    public GridGeometry build(final GridExtent extent) throws FactoryException {
+        CoordinateReferenceSystem crs = null;
+        if (keyDirectory != null) {
+            final CRSBuilder helper = new CRSBuilder(reader);
+            try {
+                crs = helper.build(keyDirectory, numericParameters, asciiParameters);
+                description  = helper.description;
+                cellGeometry = helper.cellGeometry;
+            } catch (NoSuchIdentifierException | ParameterNotFoundException e) {
+                short key = Resources.Keys.UnsupportedProjectionMethod_1;
+                if (e instanceof NoSuchAuthorityCodeException) {
+                    key = Resources.Keys.UnknownCRS_1;
+                }
+                reader.owner.warning(reader.resources().getString(key, reader.owner.getDisplayName()), e);
+            } catch (IllegalArgumentException | NoSuchElementException | ClassCastException e) {
+                if (!helper.alreadyReported) {
+                    reader.owner.warning(null, e);
+                }
+            }
+        }
+        boolean pixelIsPoint = CellGeometry.POINT.equals(cellGeometry);
+        try {
+            final MathTransform gridToCRS;
+            if (affine != null) {
+                gridToCRS = MathTransforms.linear(affine);
+            } else {
+                gridToCRS = Localization.nonLinear(modelTiePoints);
+                pixelIsPoint = true;
+            }
+            gridGeometry = new GridGeometry(extent, pixelIsPoint ? PixelInCell.CELL_CENTER : PixelInCell.CELL_CORNER, gridToCRS, crs);
+        } catch (TransformException e) {
+            gridGeometry = new GridGeometry(extent, crs);
+            reader.owner.warning(null, e);
+            /*
+             * Note: we catch TransformExceptions because they may be caused by erroneous data in the GeoTIFF file,
+             * but let FactoryExceptions propagate because they are more likely to be a SIS configuration problem.
+             */
+        }
+        keyDirectory      = null;            // Not needed anymore, so let GC do its work.
+        numericParameters = null;
+        asciiParameters   = null;
+        modelTiePoints    = null;
+        affine            = null;
+        return gridGeometry;
+    }
+
+    /**
+     * Completes ISO 19115 metadata with some GeoTIFF values inferred from the geotags.
+     *
+     * <p><b>Pre-requite:</b></p>
+     * <ul>
+     *   <li>{@link #build(GridExtent)} must have been invoked successfully before this method.</li>
+     *   <li>{@link ImageFileDirectory} must have filled its part of metadata before to invoke this method.</li>
+     * </ul>
+     *
+     * This method invokes {@link MetadataBuilder#newGridRepresentation(MetadataBuilder.GridType)}
+     * with the appropriate {@code GEORECTIFIED} or {@code GEOREFERENCEABLE} type.
+     *
+     * @param  metadata  the helper class where to write metadata values.
+     * @throws NumberFormatException if a numeric value was stored as a string and can not be parsed.
+     */
+    public void completeMetadata(final MetadataBuilder metadata) {
+        final boolean isGeorectified = (modelTiePoints == null) || (affine != null);
+        metadata.newGridRepresentation(isGeorectified ? MetadataBuilder.GridType.GEORECTIFIED
+                                                      : MetadataBuilder.GridType.GEOREFERENCEABLE);
+        metadata.setGeoreferencingAvailability(affine != null, false, false);
+        if (gridGeometry != null && gridGeometry.isDefined(GridGeometry.CRS)) {
+            metadata.addReferenceSystem(gridGeometry.getCoordinateReferenceSystem());
+        }
+        metadata.setGridToCRS(description);
+        /*
+         * Whether the pixel value is thought of as filling the cell area or is considered as point measurements at
+         * the vertices of the grid (not in the interior of a cell).  This is determined by the value associated to
+         * GeoKeys.RasterType, which can be GeoCodes.RasterPixelIsArea or GeoCodes.RasterPixelIsPoint.
+         *
+         * Note: the pixel orientation (UPPER_LEFT versus CENTER) should be kept consistent with the discussion in
+         * GridGeometryBuilder class javadoc.
+         */
+        metadata.setCellGeometry(cellGeometry);
+        final PixelOrientation po;
+        if (CellGeometry.POINT.equals(cellGeometry)) {
+            po = PixelOrientation.CENTER;
+        } else if (CellGeometry.AREA.equals(cellGeometry)) {
+            po = PixelOrientation.UPPER_LEFT;
+        } else {
+            return;
+        }
+        metadata.setPointInPixel(po);
+    }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index c34c892..c3366b8 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -22,24 +22,21 @@ import java.util.Arrays;
 import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
-import java.util.NoSuchElementException;
 import java.nio.charset.Charset;
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
+import org.opengis.metadata.Metadata;
 import org.opengis.metadata.citation.DateType;
 import org.opengis.util.FactoryException;
-import org.opengis.util.NoSuchIdentifierException;
-import org.opengis.parameter.ParameterNotFoundException;
-import org.opengis.referencing.NoSuchAuthorityCodeException;
-import org.opengis.referencing.operation.TransformException;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.referencing.operation.matrix.Matrices;
-import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.internal.geotiff.Resources;
+import org.apache.sis.internal.storage.AbstractResource;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
 
@@ -59,7 +56,7 @@ import org.apache.sis.measure.Units;
  * @since 0.8
  * @module
  */
-final class ImageFileDirectory {
+final class ImageFileDirectory extends AbstractResource implements GridCoverageResource {
     /**
      * Possible value for the {@link #tileTagFamily} field. That field tells whether image tiling
      * was specified using the {@code Tile*} family of TIFF tags or the {@code Strip*} family.
@@ -314,57 +311,28 @@ final class ImageFileDirectory {
     private Compression compression;
 
     /**
-     * References the {@link GeoKeys} needed for building the Coordinate Reference System.
-     * This is a GeoTIFF extension to the TIFF specification.
-     * Content will be parsed by {@link CRSBuilder}.
-     */
-    private Vector geoKeyDirectory;
-
-    /**
-     * The numeric values referenced by the {@link #geoKeyDirectory}.
-     * This is a GeoTIFF extension to the TIFF specification.
-     * Content will be parsed by {@link CRSBuilder}.
-     */
-    private Vector numericGeoParameters;
-
-    /**
-     * The characters referenced by the {@link #geoKeyDirectory}.
-     * This is a GeoTIFF extension to the TIFF specification.
-     * Content will be parsed by {@link CRSBuilder}.
-     */
-    private String asciiGeoParameters;
-
-    /**
-     * Raster model tie points. This vector contains ordinate values structured as (I,J,K, X,Y,Z) records.
-     * The (I,J,K) ordinate values specify the point at location (I,J) in raster space with pixel-value K,
-     * and (X,Y,Z) ordinate values specify the point in the Coordinate Reference System. In most cases the
-     * coordinate system is only two-dimensional, in which case both K and Z should be set to zero.
-     */
-    private Vector modelTiePoints;
-
-    /**
-     * The conversion from grid coordinates to CRS coordinates. It can be determined in different ways,
-     * from simpler to more complex:
+     * A helper class for building Coordinate Reference System and complete related metadata.
+     * Contains the following information:
      *
      * <ul>
-     *   <li>By a combination of a single {@link #modelTiePoints} with the 3 values given in
-     *       {@link Tags#ModelPixelScaleTag} as documented in the Javadoc of that tag.</li>
-     *   <li>By a {@link Tags#ModelTransformation} giving all coefficients of the 4×4 matrix}.
-     *       Note that the third row and the third column have all their value set to 0 if the
-     *       space model (or the coordinate reference system) should be two-dimensional.</li>
-     *   <li>By building a non-linear transformation from all {@link #modelTiePoints}.
-     *       Such transformation can not be stored in a matrix, so will leave this field {@code null}.</li>
+     *   <li>{@link GridGeometryBuilder#keyDirectory}</li>
+     *   <li>{@link GridGeometryBuilder#numericParameters}</li>
+     *   <li>{@link GridGeometryBuilder#asciiParameters}</li>
+     *   <li>{@link GridGeometryBuilder#modelTiePoints}</li>
      * </ul>
-     *
-     * By convention, the translation column is set to NaN values if it needs to be computed from the tie point.
      */
-    private MatrixSIS gridToCRS;
+    private GridGeometryBuilder referencing;
 
     /**
-     * {@code true} if {@link #gridToCRS} has been specified by a complete matrix ({@link Tags#ModelTransformation}),
-     * or {@code false} if it has been specified by the scale factors only ({@link Tags#ModelPixelScaleTag}).
+     * Returns {@link #referencing}, created when first needed. We delay its creation since
+     * this object is not needed for ordinary TIFF files (i.e. without the GeoTIFF extension).
      */
-    private boolean completeMatrixSpecified;
+    private GridGeometryBuilder referencing() {
+        if (referencing == null) {
+            referencing = new GridGeometryBuilder(reader);
+        }
+        return referencing;
+    }
 
     /**
      * Creates a new image file directory.
@@ -372,6 +340,7 @@ final class ImageFileDirectory {
      * @param reader  information about the input stream to read, the metadata and the character encoding.
      */
     ImageFileDirectory(final Reader reader) {
+        super(reader.owner);
         this.reader = reader;
     }
 
@@ -697,14 +666,14 @@ final class ImageFileDirectory {
              * The first 4 values are special, and contain GeoKey directory header information.
              */
             case Tags.GeoKeyDirectory: {
-                geoKeyDirectory = type.readVector(input(), count);
+                referencing().keyDirectory = type.readVector(input(), count);
                 break;
             }
             /*
              * Stores all of the 'double' valued GeoKeys, referenced by the GeoKeyDirectory.
              */
             case Tags.GeoDoubleParams: {
-                numericGeoParameters = type.readVector(input(), count);
+                referencing().numericParameters = type.readVector(input(), count);
                 break;
             }
             /*
@@ -716,8 +685,8 @@ final class ImageFileDirectory {
                 final String[] values = type.readString(input(), count, encoding());
                 switch (values.length) {
                     case 0:  break;
-                    case 1:  asciiGeoParameters = values[0]; break;
-                    default: asciiGeoParameters = String.join("\u0000", values).concat("\u0000"); break;
+                    case 1:  referencing().asciiParameters = values[0]; break;
+                    default: referencing().asciiParameters = String.join("\u0000", values).concat("\u0000"); break;
                 }
                 break;
             }
@@ -736,21 +705,15 @@ final class ImageFileDirectory {
              */
             case Tags.ModelTransformation: {
                 final Vector m = type.readVector(input(), count);
-                final int size = m.size();
                 final int n;
-                switch (size) {
+                switch (m.size()) {
                     case  6:                    // Assume 2D model with implicit [0 0 1] last row.
                     case  9: n = 3; break;      // Assume 2D model with full 3×3 matrix.
                     case 12:                    // Assume 3D model with implicit [0 0 0 1] last row.
                     case 16: n = 4; break;      // 3D model with full 4×4 matrix, as required by GeoTIFF spec.
                     default: return m;
                 }
-                completeMatrixSpecified = true;
-                gridToCRS = Matrices.createZero(n, n);
-                gridToCRS.setElement(n-1, n-1, 1);
-                for (int i=0; i<size; i++) {
-                    gridToCRS.setElement(i / n, i % n, m.doubleValue(i));
-                }
+                referencing().setGridToCRS(m, n);
                 break;
             }
             /*
@@ -763,22 +726,14 @@ final class ImageFileDirectory {
                 if (size < 2 || size > 3) {     // Length should be exactly 3, but we make this reader tolerant.
                     return m;
                 }
-                completeMatrixSpecified = false;
-                gridToCRS = Matrices.createZero(size+1, size+1);
-                gridToCRS.setElement(size, size, 1);
-                for (int i=0; i<size; i++) {
-                    double e = m.doubleValue(i);
-                    if (i == 1) e = -e;                             // Make y scale factor negative.
-                    gridToCRS.setElement(i, i, e);
-                    gridToCRS.setElement(i, size, Double.NaN);
-                }
+                referencing().setScaleFactors(m);
                 break;
             }
             /*
              * The mapping from pixel coordinates to CRS coordinates as a sequence of (I,J,K, X,Y,Z) records.
              */
             case Tags.ModelTiePoints: {
-                modelTiePoints = type.readVector(input(), count);
+                referencing().modelTiePoints = type.readVector(input(), count);
                 break;
             }
 
@@ -1161,10 +1116,8 @@ final class ImageFileDirectory {
          * If a "grid to CRS" conversion has been specified with only the scale factor, we need to compute
          * the translation terms now.
          */
-        if (gridToCRS != null && !completeMatrixSpecified) {
-            if (!Localization.setTranslationTerms(gridToCRS, modelTiePoints)) {
-                throw missingTag(Tags.ModelTiePoints);
-            }
+        if (referencing != null && !referencing.validateMandatoryTags()) {
+            throw missingTag(Tags.ModelTiePoints);
         }
     }
 
@@ -1229,40 +1182,51 @@ final class ImageFileDirectory {
          * in which case the CRS builder returns null. This is safe since all MetadataBuilder methods
          * ignore null values (a design choice because this pattern come very often).
          */
-        final boolean isGeorectified = (modelTiePoints == null) || (gridToCRS != null);
-        metadata.newGridRepresentation(isGeorectified ? MetadataBuilder.GridType.GEORECTIFIED
-                                                      : MetadataBuilder.GridType.GEOREFERENCEABLE);
-        metadata.setGeoreferencingAvailability(gridToCRS != null, false, false);
-        CoordinateReferenceSystem crs = null;
-        if (geoKeyDirectory != null) {
-            final CRSBuilder helper = new CRSBuilder(reader);
-            try {
-                crs = helper.build(geoKeyDirectory, numericGeoParameters, asciiGeoParameters);
-                metadata.addReferenceSystem(crs);
-                helper.complete(metadata);
-            } catch (NoSuchIdentifierException | ParameterNotFoundException e) {
-                short key = Resources.Keys.UnsupportedProjectionMethod_1;
-                if (e instanceof NoSuchAuthorityCodeException) {
-                    key = Resources.Keys.UnknownCRS_1;
-                }
-                reader.owner.warning(reader.resources().getString(key, reader.owner.getDisplayName()), e);
-            } catch (IllegalArgumentException | NoSuchElementException | ClassCastException e) {
-                if (!helper.alreadyReported) {
-                    reader.owner.warning(null, e);
-                }
-            }
+        if (referencing != null) {
+            getGridGeometry();                  // For calculation of gridGeometry if not already done.
+            referencing.completeMetadata(metadata);
         }
-        try {
-            if (!isGeorectified) {
-                metadata.addGeolocation(new Localization(filename(), crs, modelTiePoints));
+    }
+
+    @Override
+    public Metadata getMetadata() throws DataStoreContentException {
+        /*
+         * TODO:
+         *   - Modify ImageFileDirectory.completeMetadata(…) with the addition of a boolean telling that
+         *     that we invoke this method for a single image instead than the whole image. Use that flag
+         *     for skipping MetadataBuilder calls writing in metadata/identificationInfo/resourceFormat.
+         *   - Invoke ImageFileDirectory.completeMetadata(…) if not already done and cache in a field.
+         *   - Add a metadata utility method taking two Metadata in argument, search for properties that
+         *     are equal and replace them by the same instance.
+         *   - Invoke that method from here if GeoTiffStore already has a metadata, or conversely from
+         *     GeoTiffStore if ImageResource already has a metadata.
+         */
+        return null;
+    }
+
+    /**
+     * Returns the grid envelope for this image.
+     */
+    private GridExtent extent() {
+        return new GridExtent(null, new long[] {imageWidth, imageHeight}, false);
+    }
+
+    /**
+     * Returns the grid geometry for this image.
+     */
+    @Override
+    public GridGeometry getGridGeometry() throws DataStoreContentException {
+        if (referencing != null) {
+            GridGeometry gridGeometry = referencing.gridGeometry;
+            if (gridGeometry == null) try {
+                gridGeometry = referencing.build(extent());
+            } catch (FactoryException e) {
+                throw new DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1, filename(), e));
             }
-        } catch (TransformException e) {
-            reader.owner.warning(null, e);
+            return gridGeometry;
+        } else {
+            return new GridGeometry(extent(), null);
         }
-        geoKeyDirectory      = null;            // Not needed anymore, so let GC do its work.
-        numericGeoParameters = null;
-        asciiGeoParameters   = null;
-        modelTiePoints       = null;
     }
 
     /**
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Localization.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Localization.java
index 581e30d..dcd1d79 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Localization.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Localization.java
@@ -21,21 +21,13 @@ import java.util.Set;
 import java.util.Map;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
-import java.util.Collection;
-import java.util.Collections;
 import org.opengis.geometry.Envelope;
 import org.opengis.util.FactoryException;
-import org.opengis.metadata.quality.DataQuality;
-import org.opengis.metadata.spatial.GeolocationInformation;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
-import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
-import org.apache.sis.internal.util.DoubleDouble;
 import org.apache.sis.math.Vector;
 
 
@@ -43,47 +35,19 @@ import org.apache.sis.math.Vector;
  * The conversion or transformation from pixel coordinates to model coordinates.
  * The target CRS may be the image CRS if the image is "georeferenceable" instead of georeferenced.
  *
- * This is a placeholder before a real {@code GridGeometry} class is ported to Apache SIS.
- * We implement {@code CoordinateOperation} for now for allowing access to the math transform from
- * outside this package, but we will probably not keep this class hierarchy in a future version.
- *
- * <div class="section">Pixel center versus pixel corner</div>
- * The policy about whether the conversion map pixel corner or pixel center does not seem totally clear.
- * But the following seems to be the practice at least with GDAL:
- *
- * {@preformat text
- *     ModelTiepointTag = (0.0, 0.0, 0.0, -180.0, 90.0, 0.0)
- *     ModelPixelScaleTag = (0.002777777778, 0.002777777778, 0.0)
- *     GeoKeyDirectoryTag:
- *         GTModelTypeGeoKey    = 2    (ModelTypeGeographic)
- *         GTRasterTypeGeoKey   = 1    (RasterPixelIsArea)
- *         GeographicTypeGeoKey = 4326 (GCS_WGS_84)
- * }
- *
- * and
- *
- * {@preformat text
- *     ModelTiepointTag = (-0.5, -0.5, 0.0, -180.0, 90.0, 0.0)
- *     ModelPixelScaleTag = (0.002777777778, 0.002777777778, 0.0)
- *     GeoKeyDirectoryTag:
- *         GTModelTypeGeoKey    = 2    (ModelTypeGeographic)
- *         GTRasterTypeGeoKey   = 2    (RasterPixelIsPoint)
- *         GeographicTypeGeoKey = 4326 (GCS_WGS_84)
- * }
- *
- * are considered equivalent.
+ * This code is provided in a separated class for making easier to move it to some shared location
+ * if another data store needs similar functionality in the future.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   0.8
  * @module
  */
-@SuppressWarnings("serial")
-final class Localization extends AbstractCoordinateOperation implements GeolocationInformation {
+final class Localization {
     /**
      * Number of floating point values in each (I,J,K,X,Y,Z) record.
      */
-    private static final int RECORD_LENGTH = 6;
+    static final int RECORD_LENGTH = 6;
 
     /**
      * The desired precision of coordinate transformations in units of pixels.
@@ -92,12 +56,19 @@ final class Localization extends AbstractCoordinateOperation implements Geolocat
     private static final double PRECISION = 1E-6;
 
     /**
-     * Creates a new transformation from the information found by {@link ImageFileDirectory}.
+     * Do not allow instantiation of this class.
      */
-    Localization(final String name, final CoordinateReferenceSystem crs, final Vector modelTiePoints)
-            throws FactoryException, TransformException
-    {
-        super(Collections.singletonMap(NAME_KEY, name), null, crs, null, localizationGrid(modelTiePoints, null));
+    private Localization() {
+    }
+
+    /**
+     * Creates a new localization grid from the information found by {@link ImageFileDirectory}.
+     *
+     * @param  modelTiePoints  the tie points to use for computing {@code gridToCRS}.
+     * @return the grid geometry created from above properties. Never null.
+     */
+    static MathTransform nonLinear(final Vector modelTiePoints) throws FactoryException, TransformException {
+        return localizationGrid(modelTiePoints, null);
     }
 
     /**
@@ -253,61 +224,4 @@ final class Localization extends AbstractCoordinateOperation implements Geolocat
         }
         return Double.NaN;
     }
-
-    /**
-     * Computes translation terms in the given matrix from the (usually singleton) tie point.
-     * This method is invoked when the GeoTIFF file has a {@link Tags#ModelPixelScaleTag} and
-     * {@link Tags#ModelTiePoints}. The later should have a single record.
-     *
-     * @param  gridToCRS       the matrix to update. That matrix shall contain the scale factors before to invoke this method.
-     * @param  modelTiePoints  the vector of model tie points. Only the first point will be used.
-     * @return {@code true} if the given vector is non-null and contains at least one complete record.
-     */
-    static boolean setTranslationTerms(final MatrixSIS gridToCRS, final Vector modelTiePoints) {
-        if (modelTiePoints != null) {
-            /*
-             * The GeoTIFF specification recommends that the first point is located at grid indices (0,0).
-             * But as a safety, we will nevertheless search in the grid for the point closest to origin.
-             * If the grid is affine, using the corner closest to (0,0) reduces rounding errors compared
-             * to using another corner. If the grid is not affine, then ModelPixelScaleTag should not have
-             * been defined for that file…
-             */
-            int nearest = 0;                                // Index of the record nearest to origin.
-            double distance = Double.POSITIVE_INFINITY;     // Distance squared of the nearest record.
-            final int size = modelTiePoints.size();
-            for (int i=0; i<size; i += RECORD_LENGTH) {
-                double t;
-                final double d = (t = modelTiePoints.doubleValue(i    )) * t
-                               + (t = modelTiePoints.doubleValue(i + 1)) * t
-                               + (t = modelTiePoints.doubleValue(i + 2)) * t;
-                if (d < distance) {
-                    distance = d;
-                    nearest = i;
-                    if (d == 0) break;                      // Optimization for the standard case.
-                }
-            }
-            if (distance != Double.POSITIVE_INFINITY) {
-                final DoubleDouble t = new DoubleDouble();
-                final int numDim = gridToCRS.getNumRow() - 1;
-                final int trCol  = gridToCRS.getNumCol() - 1;
-                for (int j=0; j<numDim; j++) {
-                    t.value = -modelTiePoints.doubleValue(nearest + j);
-                    t.error = DoubleDouble.errorForWellKnownValue(t.value);
-                    t.divide(gridToCRS.getNumber(j, j));
-                    t.add(modelTiePoints.doubleValue(nearest + j + RECORD_LENGTH/2));
-                    gridToCRS.setNumber(j, trCol, t);
-                }
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Provides an overall assessment of quality of geolocation information.
-     */
-    @Override
-    public Collection<DataQuality> getQualityInfo() {
-        return Collections.emptyList();
-    }
 }


Mime
View raw message