sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1833032 [2/3] - in /sis/branches/JDK8: core/sis-raster/src/main/java/org/apache/sis/coverage/grid/ core/sis-raster/src/test/java/org/apache/sis/coverage/grid/ core/sis-utility/src/main/java/org/apache/sis/util/logging/ storage/sis-geotiff/...
Date Wed, 06 Jun 2018 14:54:43 GMT
Copied: sis/branches/JDK8/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java (from r1832917, sis/branches/JDK8/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java)
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java?p2=sis/branches/JDK8/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java&p1=sis/branches/JDK8/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java&r1=1832917&r2=1833032&rev=1833032&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java [UTF-8] Wed Jun  6 14:54:43 2018
@@ -16,323 +16,92 @@
  */
 package org.apache.sis.storage.geotiff;
 
-import java.io.IOException;
-import java.text.ParseException;
-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.citation.DateType;
 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.NoSuchAuthorityCodeException;
-import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.referencing.operation.matrix.Matrices;
+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.internal.geotiff.Resources;
+import org.apache.sis.referencing.operation.matrix.Matrices;
 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.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;
-import org.apache.sis.measure.Units;
 
 
 /**
- * An Image File Directory (FID) in a TIFF image.
+ * Helper class for creating a {@link GridGeometry} from GeoTIFF data.
+ * The coordinate reference system part is built by {@link CRSBuilder}.
  *
- * @author  Rémi Maréchal (Geomatys)
- * @author  Alexis Manin (Geomatys)
- * @author  Johann Sorel (Geomatys)
- * @author  Thi Phuong Hao Nguyen (VNSC)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * <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
  *
- * @see <a href="http://www.awaresystems.be/imaging/tiff/tifftags.html">TIFF Tag Reference</a>
+ * {@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)
+ * }
  *
- * @since 0.8
+ * 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 ImageFileDirectory {
-    /**
-     * 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.
-     */
-    private static final byte TILE = 1, STRIP = 2;
-
+final class GridGeometryBuilder {
     /**
-     * Possible value for {@link #sampleFormat} specifying how to interpret each data sample in a pixel.
-     * Those values are not necessarily the same than the ones documented in {@link Tags#SampleFormat}.
-     * Default value is {@link #UNSIGNED}.
-     */
-    private static final byte SIGNED = 1, UNSIGNED = 0, FLOAT = 3;
-
-    /**
-     * The GeoTIFF reader which contain this {@code ImageFileDirectory}.
-     * Used for fetching information like the input channel and where to report warnings.
+     * The reader for which we will create coordinate reference systems.
+     * This is used for reporting warnings.
      */
     private final Reader reader;
 
-    /**
-     * {@code true} if this {@code ImageFileDirectory} has not yet read all deferred entries.
-     * When this flag is {@code true}, the {@code ImageFileDirectory} is not yet ready for use.
-     */
-    boolean hasDeferredEntries;
-
-    /**
-     * The size of the image described by this FID, or -1 if the information has not been found.
-     * The image may be much bigger than the memory capacity, in which case the image shall be tiled.
-     *
-     * <p><b>Note:</b>
-     * the {@link #imageHeight} attribute is named {@code ImageLength} in TIFF specification.</p>
-     */
-    private long imageWidth = -1, imageHeight = -1;
-
-    /**
-     * The size of each tile, or -1 if the information has not be found.
-     * Tiles shall be small enough for fitting in memory, typically in a {@link java.awt.image.Raster} object.
-     * The TIFF specification requires that tile width and height must be a multiple of 16, but the SIS reader
-     * implementation works for any size. Tiles need not be square.
-     *
-     * <p>Assuming integer arithmetic, the number of tiles in an image can be computed as below
-     * (these computed values are not TIFF fields):</p>
-     *
-     * {@preformat math
-     *   tilesAcross   = (imageWidth  + tileWidth  - 1) / tileWidth
-     *   tilesDown     = (imageHeight + tileHeight - 1) / tileHeight
-     *   tilesPerImage = tilesAcross * tilesDown
-     * }
-     *
-     * Note that {@link #imageWidth} can be less than {@code tileWidth} and/or {@link #imageHeight} can be less
-     * than {@code tileHeight}. Such case means that the tiles are too large or that the tiled image is too small,
-     * neither of which is recommended.
-     *
-     * <p><b>Note:</b>
-     * the {@link #tileHeight} attribute is named {@code TileLength} in TIFF specification.</p>
-     *
-     * <div class="section">Strips considered as tiles</div>
-     * The TIFF specification also defines a {@code RowsPerStrip} tag, which is equivalent to the
-     * height of tiles having the same width than the image. While the TIFF specification handles
-     * "tiles" and "strips" separately, Apache SIS handles strips as a special kind of tiles where
-     * only {@code tileHeight} is specified and {@code tileWidth} defaults to {@link #imageWidth}.
-     */
-    private int tileWidth = -1, tileHeight = -1;
-
-    /**
-     * For each tile, the byte offset of that tile, as compressed and stored on disk.
-     * The offset is specified with respect to the beginning of the TIFF file.
-     * Each tile has a location independent of the locations of other tiles
-     *
-     * <p>Offsets are ordered left-to-right and top-to-bottom. if {@link #isPlanar} is {@code true}
-     * (i.e. components are stored in separate “component planes”), then the offsets for the first
-     * component plane are stored first, followed by all the offsets for the second component plane,
-     * and so on.</p>
-     *
-     * <div class="section">Strips considered as tiles</div>
-     * The TIFF specification also defines a {@code StripOffsets} tag, which contains the byte offset
-     * of each strip. In Apache SIS implementation, strips are considered as a special kind of tiles
-     * having a width equals to {@link #imageWidth}.
-     */
-    private Vector tileOffsets;
-
-    /**
-     * For each tile, the number of (compressed) bytes in that tile.
-     * See {@link #tileOffsets} for a description of how the byte counts are ordered.
-     *
-     * <div class="section">Strips considered as tiles</div>
-     * The TIFF specification also defines a {@code RowsPerStrip} tag, which is the number
-     * of bytes in the strip after compression. In Apache SIS implementation, strips are
-     * considered as a special kind of tiles having a width equals to {@link #imageWidth}.
-     */
-    private Vector tileByteCounts;
-
-    /**
-     * Whether the tiling was specified using the {@code Tile*} family of TIFF tags or the {@code Strip*}
-     * family of tags. Value can be {@link #TILE}, {@link #STRIP} or 0 if unspecified. This field is used
-     * for error detection since Each TIFF file shall use exactly one family of tags.
-     */
-    private byte tileTagFamily;
-
-    /**
-     * If {@code true}, the components are stored in separate “component planes”.
-     * The default is {@code false}, which stands for the "chunky" format
-     * (for example RGB data stored as RGBRGBRGB).
-     */
-    private boolean isPlanar;
-
-    /**
-     * How to interpret each data sample in a pixel.
-     * Possible values are {@link #SIGNED}, {@link #UNSIGNED} or {@link #FLOAT}.
-     */
-    private byte sampleFormat;
-
-    /**
-     * Whether the bit order should be reversed. This boolean value is determined from the {@code FillOrder} TIFF tag.
-     *
-     * <ul>
-     *   <li>Value 1 (mapped to {@code false}) means that pixels with lower column values are stored in the
-     *       higher-order bits of the byte. This is the default value.</li>
-     *   <li>Value 2 (mapped to {@code true}) means that pixels with lower column values are stored in the
-     *       lower-order bits of the byte. In practice, this order is very uncommon and is not recommended.</li>
-     * </ul>
-     *
-     * Value 1 is mapped to {@code false} and 2 is mapped to {@code true}.
-     */
-    private boolean reverseBitsOrder;
-
-    /**
-     * Number of bits per component.
-     * The TIFF specification allows a different number of bits per component for each component corresponding to a pixel.
-     * For example, RGB color data could use a different number of bits per component for each of the three color planes.
-     * However, current Apache SIS implementation requires that all components have the same {@code BitsPerSample} value.
-     */
-    private short bitsPerSample;
-
-    /**
-     * The number of components per pixel.
-     * The {@code samplesPerPixel} value is usually 1 for bilevel, grayscale and palette-color images,
-     * and 3 for RGB images. If this value is higher, then the {@code ExtraSamples} TIFF tag should
-     * give an indication of the meaning of the additional channels.
-     */
-    private short samplesPerPixel;
-
-    /**
-     * Specifies that each pixel has {@code extraSamples.size()} extra components whose interpretation is defined
-     * by one of the values listed below. When this field is used, the {@link #samplesPerPixel} field has a value
-     * greater than what the {@link #photometricInterpretation} field suggests. For example, full-color RGB data
-     * normally has {@link #samplesPerPixel} = 3. If {@code samplesPerPixel} is greater than 3, then this
-     * {@code extraSamples} field describes the meaning of the extra samples. If {@code samplesPerPixel} is,
-     * say, 5 then this {@code extraSamples} field will contain 2 values, one for each extra sample.
-     *
-     * <p>Extra components that are present must be stored as the last components in each pixel.
-     * For example, if {@code samplesPerPixel} is 4 and there is 1 extra component, then it is
-     * located in the last component location in each pixel.</p>
-     *
-     * <p>ExtraSamples is typically used to include non-color information, such as opacity, in an image.
-     * The possible values for each item are:</p>
-     *
-     * <ul>
-     *   <li>0 = Unspecified data.</li>
-     *   <li>1 = Associated alpha data (with pre-multiplied color).</li>
-     *   <li>2 = Unassociated alpha data.</li>
-     * </ul>
-     *
-     * Associated alpha is generally interpreted as true transparency information. Indeed, the original color
-     * values are lost in the case of complete transparency, and rounded in the case of partial transparency.
-     * Also, associated alpha is only logically possible as the single extra channel.
-     * Unassociated alpha channels, on the other hand, can be used to encode a number of independent masks.
-     * The original color data is preserved without rounding. Any number of unassociated alpha channels can
-     * accompany an image.
-     *
-     * <p>If an extra sample is used to encode information that has little or nothing to do with alpha,
-     * then {@code extraSample} = 0 ({@code EXTRASAMPLE_UNSPECIFIED}) is recommended.</p>
-     */
-    private Vector extraSamples;
-
-    /**
-     * The color space of the image data, or -1 if unspecified.
-     *
-     * <table>
-     *   <caption>Color space codes</caption>
-     *   <tr><th>Value</th> <th>Label</th>        <th>Description</th></tr>
-     *   <tr><td>0</td> <td>WhiteIsZero</td>      <td>For bilevel and grayscale images. 0 is imaged as white.</td></tr>
-     *   <tr><td>1</td> <td>BlackIsZero</td>      <td>For bilevel and grayscale images. 0 is imaged as black.</td></tr>
-     *   <tr><td>2</td> <td>RGB</td>              <td>RGB value of (0,0,0) represents black, and (255,255,255) represents white.</td></tr>
-     *   <tr><td>3</td> <td>PaletteColor</td>     <td>The value of the component is used as an index into the RGB values of the {@link #colorMap}.</td></tr>
-     *   <tr><td>4</td> <td>TransparencyMask</td> <td>Defines an irregularly shaped region of another image in the same TIFF file.</td></tr>
-     * </table>
-     */
-    private byte photometricInterpretation = -1;
-
-    /**
-     * A color map for palette color images ({@link #photometricInterpretation} = 3).
-     * This vector defines a Red-Green-Blue color map (often called a lookup table) for palette-color images.
-     * In a palette-color image, a pixel value is used to index into an RGB lookup table. For example, a
-     * palette-color pixel having a value of 0 would be displayed according to the 0th Red, Green, Blue triplet.
-     *
-     * <p>In a TIFF ColorMap, all the Red values come first, followed by all Green values, then all Blue values.
-     * The number of values for each color is 1 {@literal <<} {@link #bitsPerSample}. Therefore, the {@code ColorMap}
-     * vector for an 8-bit palette-color image would have 3 * 256 values. 0 represents the minimum intensity and 65535
-     * represents the maximum intensity. Black is represented by 0,0,0 and white by 65535, 65535, 65535.</p>
-     *
-     * <p>{@code ColorMap} must be included in all palette-color images.
-     * In Specification Supplement 1, support was added for color maps containing other then RGB values.
-     * This scheme includes the {@code Indexed} tag, with value 1, and a {@link #photometricInterpretation}
-     * different from {@code PaletteColor}.</p>
-     */
-    private Vector colorMap;
-
-    /**
-     * The size of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file.
-     * This field should be present only if {@code Threshholding} tag is 2 (an ordered dither or halftone
-     * technique has been applied to the image data). Special values:
-     *
-     * <ul>
-     *   <li>-1 means that {@code Threshholding} is 1 or unspecified.</li>
-     *   <li>-2 means that {@code Threshholding} is 2 but the matrix size has not yet been specified.</li>
-     *   <li>-3 means that {@code Threshholding} is 3 (randomized process such as error diffusion).</li>
-     * </ul>
-     */
-    private short cellWidth = -1, cellHeight = -1;
-
-    /**
-     * The minimum or maximum sample value found in the image, with one value per band.
-     * May be a vector of length 1 if the same single value applies to all bands.
-     */
-    private Vector minValues, maxValues;
-
-    /**
-     * {@code true} if {@link #minValues} and {@link #maxValues} have been explicitly specified
-     * in the TIFF file, or {@code false} if they have been inferred from {@link #bitsPerSample}.
-     */
-    private boolean isMinSpecified, isMaxSpecified;
-
-    /**
-     * The number of pixels per {@link #resolutionUnit} in the {@link #imageWidth} and the {@link #imageHeight}
-     * directions, or {@link Double#NaN} is unspecified. Since ISO 19115 does not have separated resolution fields
-     * for image width and height, Apache SIS stores only the maximal value.
-     */
-    private double resolution = Double.NaN;
-
-    /**
-     * The unit of measurement for the {@linkplain #resolution} value, or {@code null} if none.
-     * A null value is used for images that may have a non-square aspect ratio, but no meaningful
-     * absolute dimensions. Default value for TIFF files is inch.
-     */
-    private Unit<Length> resolutionUnit = Units.INCH;
-
-    /**
-     * The compression method, or {@code null} if unknown. If the compression method is unknown
-     * or unsupported we can not read the image, but we still can read the metadata.
-     */
-    private Compression compression;
+    ////////////////////////////////////////////////////////////////////////////////////////
+    ////                                                                                ////
+    ////    Information to be set by ImageFileDirectory during GeoTIFF file parsing.    ////
+    ////                                                                                ////
+    ////////////////////////////////////////////////////////////////////////////////////////
 
     /**
      * 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;
+    public Vector keyDirectory;
 
     /**
-     * The numeric values referenced by the {@link #geoKeyDirectory}.
-     * This is a GeoTIFF extension to the TIFF specification.
-     * Content will be parsed by {@link CRSBuilder}.
+     * The numeric values referenced by the {@link #keyDirectory}.
      */
-    private Vector numericGeoParameters;
+    public Vector numericParameters;
 
     /**
-     * The characters referenced by the {@link #geoKeyDirectory}.
-     * This is a GeoTIFF extension to the TIFF specification.
-     * Content will be parsed by {@link CRSBuilder}.
+     * The characters referenced by the {@link #keyDirectory}.
      */
-    private String asciiGeoParameters;
+    public String asciiParameters;
 
     /**
      * Raster model tie points. This vector contains ordinate values structured as (I,J,K, X,Y,Z) records.
@@ -340,11 +109,11 @@ final class ImageFileDirectory {
      * 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;
+    public Vector modelTiePoints;
 
     /**
-     * The conversion from grid coordinates to CRS coordinates. It can be determined in different ways,
-     * from simpler to more complex:
+     * 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
@@ -358,888 +127,163 @@ final class ImageFileDirectory {
      *
      * By convention, the translation column is set to NaN values if it needs to be computed from the tie point.
      */
-    private MatrixSIS gridToCRS;
+    private MatrixSIS affine;
 
     /**
-     * {@code true} if {@link #gridToCRS} has been specified by a complete matrix ({@link Tags#ModelTransformation}),
+     * {@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;
 
     /**
-     * Creates a new image file directory.
+     * Sets the {@link #affine} transform from a complete matrix.
      *
-     * @param reader  information about the input stream to read, the metadata and the character encoding.
+     * @param  terms  the matrix in a row-major fashion.
+     * @param  size   the matrix size, either 3 or 4.
      */
-    ImageFileDirectory(final Reader reader) {
-        this.reader = reader;
-    }
-
-    /**
-     * Shortcut for a frequently requested information.
-     */
-    private ChannelDataInput input() {
-        return reader.input;
-    }
-
-    /**
-     * Shortcut for a frequently requested information.
-     */
-    private String filename() {
-        return input().filename;
+    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));
+        }
     }
 
     /**
-     * Shortcut for a frequently requested information.
+     * 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.
      */
-    private Charset encoding() {
-        return reader.owner.encoding;
+    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);
+        }
     }
 
-    /**
-     * Adds the value read from the current position in the given stream for the entry identified
-     * by the given GeoTIFF tag. This method may store the value either in a field of this class,
-     * or directly in the {@link MetadataBuilder}. However in the later case, this method should
-     * not write anything under the {@code "metadata/contentInfo"} node.
-     *
-     * @param  tag    the GeoTIFF tag to decode.
-     * @param  type   the GeoTIFF type of the value to read.
-     * @param  count  the number of values to read.
-     * @return {@code null} on success, or the unrecognized value otherwise.
-     * @throws IOException if an error occurred while reading the stream.
-     * @throws ParseException if the value need to be parsed as date and the parsing failed.
-     * @throws NumberFormatException if the value need to be parsed as number and the parsing failed.
-     * @throws ArithmeticException if the value can not be represented in the expected Java type.
-     * @throws IllegalArgumentException if a value which was expected to be a singleton is not.
-     * @throws UnsupportedOperationException if the given type is {@link Type#UNDEFINED}.
-     * @throws DataStoreException if a logical error is found or an unsupported TIFF feature is used.
-     */
-    Object addEntry(final short tag, final Type type, final long count)
-            throws IOException, ParseException, DataStoreException
-    {
-        switch (tag) {
-
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-            ////                                                                                        ////
-            ////    Essential information for being able to read the image at least as grayscale.       ////
-            ////    In Java2D, following information are needed for building the SampleModel.           ////
-            ////                                                                                        ////
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-
-            /*
-             * How the components of each pixel are stored.
-             * 1 = Chunky format. The component values for each pixel are stored contiguously (for example RGBRGBRGB).
-             * 2 = Planar format. For example one plane of Red components, one plane of Green and one plane if Blue.
-             */
-            case Tags.PlanarConfiguration: {
-                final int value = type.readInt(input(), count);
-                switch (value) {
-                    case 1:  isPlanar = false; break;
-                    case 2:  isPlanar = true;  break;
-                    default: return value;                  // Cause a warning to be reported by the caller.
-                }
-                break;
-            }
-            /*
-             * The number of columns in the image, i.e., the number of pixels per row.
-             */
-            case Tags.ImageWidth: {
-                imageWidth = type.readUnsignedLong(input(), count);
-                break;
-            }
-            /*
-             * The number of rows of pixels in the image.
-             */
-            case Tags.ImageLength: {
-                imageHeight = type.readUnsignedLong(input(), count);
-                break;
-            }
-            /*
-             * The tile width in pixels. This is the number of columns in each tile.
-             */
-            case Tags.TileWidth: {
-                setTileTagFamily(TILE);
-                tileWidth = type.readInt(input(), count);
-                break;
-            }
-            /*
-             * The tile length (height) in pixels. This is the number of rows in each tile.
-             */
-            case Tags.TileLength: {
-                setTileTagFamily(TILE);
-                tileHeight = type.readInt(input(), count);
-                break;
-            }
-            /*
-             * The number of rows per strip. This is considered by SIS as a special kind of tiles.
-             * From this point of view, TileLength = RowPerStrip and TileWidth = ImageWidth.
-             */
-            case Tags.RowsPerStrip: {
-                setTileTagFamily(STRIP);
-                tileHeight = type.readInt(input(), count);
-                break;
-            }
-            /*
-             * The tile length (height) in pixels. This is the number of rows in each tile.
-             */
-            case Tags.TileOffsets: {
-                setTileTagFamily(TILE);
-                tileOffsets = type.readVector(input(), count);
-                break;
-            }
-            /*
-             * For each strip, the byte offset of that strip relative to the beginning of the TIFF file.
-             * In Apache SIS implementation, strips are considered as a special kind of tiles.
-             */
-            case Tags.StripOffsets: {
-                setTileTagFamily(STRIP);
-                tileOffsets = type.readVector(input(), count);
-                break;
-            }
-            /*
-             * The tile width in pixels. This is the number of columns in each tile.
-             */
-            case Tags.TileByteCounts: {
-                setTileTagFamily(TILE);
-                tileByteCounts = type.readVector(input(), count);
-                break;
-            }
-            /*
-             * For each strip, the number of bytes in the strip after compression.
-             * In Apache SIS implementation, strips are considered as a special kind of tiles.
-             */
-            case Tags.StripByteCounts: {
-                setTileTagFamily(STRIP);
-                tileByteCounts = type.readVector(input(), count);
-                break;
-            }
-
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-            ////                                                                                        ////
-            ////    Information that defines how the sample values are organized (their layout).        ////
-            ////    In Java2D, following information are needed for building the SampleModel.           ////
-            ////                                                                                        ////
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-
-            /*
-             * Compression scheme used on the image data.
-             */
-            case Tags.Compression: {
-                final long value = type.readLong(input(), count);
-                compression = Compression.valueOf(value);
-                if (compression == null) {
-                    return value;                           // Cause a warning to be reported by the caller.
-                }
-                break;
-            }
-            /*
-             * The logical order of bits within a byte. If this value is 2, then
-             * bits order shall be reversed in every bytes before decompression.
-             */
-            case Tags.FillOrder: {
-                final int value = type.readInt(input(), count);
-                switch (value) {
-                    case 1: reverseBitsOrder = false; break;
-                    case 2: reverseBitsOrder = true;  break;
-                    default: return value;                  // Cause a warning to be reported by the caller.
-                }
-                break;
-            }
-            /*
-             * How to interpret each data sample in a pixel. The size of data samples is still
-             * specified by the BitsPerSample field.
-             */
-            case Tags.SampleFormat: {
-                final int value = type.readInt(input(), count);
-                switch (value) {
-                    default: return value;                          // Warning to be reported by the caller.
-                    case 1: sampleFormat = UNSIGNED; break;         // Unsigned integer data (default).
-                    case 2: sampleFormat = SIGNED;   break;         // Two’s complement signed integer data.
-                    case 3: sampleFormat = FLOAT;    break;         // IEEE floating point data.
-                    case 4: warning(Level.WARNING, Resources.Keys.UndefinedDataFormat_1, filename()); break;
-                }
-                break;
-            }
-            /*
-             * Number of bits per component. The array length should be the number of components in a
-             * pixel (e.g. 3 for RGB values). Typically, all components have the same number of bits.
-             * But the TIFF specification allows different values.
-             */
-            case Tags.BitsPerSample: {
-                final Vector values = type.readVector(input(), count);
-                /*
-                 * The current implementation requires that all 'bitsPerSample' elements have the same value.
-                 * This restriction may be revisited in future Apache SIS versions.
-                 * Note: 'count' is never zero when this method is invoked, so we do not need to check bounds.
-                 */
-                bitsPerSample = values.shortValue(0);
-                final int length = values.size();
-                for (int i = 1; i < length; i++) {
-                    if (values.shortValue(i) != bitsPerSample) {
-                        throw new DataStoreContentException(reader.resources().getString(
-                                Resources.Keys.ConstantValueRequired_3, "BitsPerSample", filename(), values));
-                    }
-                }
-                break;
-            }
-            /*
-             * The number of components per pixel. Usually 1 for bilevel, grayscale, and palette-color images,
-             * and 3 for RGB images. Default value is 1.
-             */
-            case Tags.SamplesPerPixel: {
-                samplesPerPixel = type.readShort(input(), count);
-                break;
-            }
-            /*
-             * Specifies that each pixel has N extra components. When this field is used, the SamplesPerPixel field
-             * has a value greater than the PhotometricInterpretation field suggests. For example, a full-color RGB
-             * image normally has SamplesPerPixel=3. If SamplesPerPixel is greater than 3, then the ExtraSamples field
-             * describes the meaning of the extra samples. It may be an alpha channel, but not necessarily.
-             */
-            case Tags.ExtraSamples: {
-                extraSamples = type.readVector(input(), count);
-                break;
-            }
-
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-            ////                                                                                        ////
-            ////    Information related to the color palette or the meaning of sample values.           ////
-            ////    In Java2D, following information are needed for building the ColorModel.            ////
-            ////                                                                                        ////
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-
-            /*
-             * The color space of the image data.
-             * 0 = WhiteIsZero. For bilevel and grayscale images: 0 is imaged as white.
-             * 1 = BlackIsZero. For bilevel and grayscale images: 0 is imaged as black.
-             * 2 = RGB. RGB value of (0,0,0) represents black, and (65535,65535,65535) represents white.
-             * 3 = Palette color. The value of the component is used as an index into the RGB values of the ColorMap.
-             * 4 = Transparency Mask the defines an irregularly shaped region of another image in the same TIFF file.
-             */
-            case Tags.PhotometricInterpretation: {
-                final short value = type.readShort(input(), count);
-                if (value < 0 || value > Byte.MAX_VALUE) return value;
-                photometricInterpretation = (byte) value;
-                break;
-            }
-            /*
-             * The lookup table for palette-color images. This is represented by IndexColorModel in Java2D.
-             * Color space is RGB if PhotometricInterpretation is "PaletteColor", or another color space otherwise.
-             * In the RGB case, all the Red values come first, followed by all Green values, then all Blue values.
-             * The number of values for each color is (1 << BitsPerSample) where 0 represents the minimum intensity
-             * (black is 0,0,0) and 65535 represents the maximum intensity.
-             */
-            case Tags.ColorMap: {
-                colorMap = type.readVector(input(), count);
-                break;
-            }
-            /*
-             * The minimum component value used. MinSampleValue is a single value that apply to all bands
-             * while SMinSampleValue lists separated values for each band. Default is 0.
-             */
-            case Tags.MinSampleValue:
-            case Tags.SMinSampleValue: {
-                minValues = extremum(minValues, type.readVector(input(), count), false);
-                isMinSpecified = true;
-                break;
-            }
-            /*
-             * The maximum component value used. Default is {@code (1 << BitsPerSample) - 1}.
-             * This field is for statistical purposes and should not to be used to affect the
-             * visual appearance of an image, unless a map styling is applied.
-             */
-            case Tags.MaxSampleValue:
-            case Tags.SMaxSampleValue: {
-                maxValues = extremum(maxValues, type.readVector(input(), count), true);
-                isMaxSpecified = true;
-                break;
-            }
-
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-            ////                                                                                        ////
-            ////    Information useful for defining the image role in a multi-images context.           ////
-            ////                                                                                        ////
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-
-            /*
-             * A general indication of the kind of data contained in this subfile, mainly useful when there
-             * are multiple subfiles in a single TIFF file. This field is made up of a set of 32 flag bits.
-             *
-             * Bit 0 is 1 if the image is a reduced-resolution version of another image in this TIFF file.
-             * Bit 1 is 1 if the image is a single page of a multi-page image (see PageNumber).
-             * Bit 2 is 1 if the image defines a transparency mask for another image in this TIFF file (see PhotometricInterpretation).
-             * Bit 4 indicates MRC imaging model as described in ITU-T recommendation T.44 [T.44] (See ImageLayer tag) - RFC 2301.
-             */
-            case Tags.NewSubfileType: {
-                // TODO
-                break;
-            }
-            /*
-             * Old version (now deprecated) of above NewSubfileType.
-             * 1 = full-resolution image data
-             * 2 = reduced-resolution image data
-             * 3 = a single page of a multi-page image (see PageNumber).
-             */
-            case Tags.SubfileType: {
-                // TODO
-                break;
-            }
-
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-            ////                                                                                        ////
-            ////    Information related to the Coordinate Reference System and the bounding box.        ////
-            ////                                                                                        ////
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-
-            /*
-             * References the "GeoKeys" needed for building the Coordinate Reference System.
-             * An array of unsigned SHORT values, which are primarily grouped into blocks of 4.
-             * The first 4 values are special, and contain GeoKey directory header information.
-             */
-            case Tags.GeoKeyDirectory: {
-                geoKeyDirectory = type.readVector(input(), count);
-                break;
-            }
-            /*
-             * Stores all of the 'double' valued GeoKeys, referenced by the GeoKeyDirectory.
-             */
-            case Tags.GeoDoubleParams: {
-                numericGeoParameters = type.readVector(input(), count);
-                break;
-            }
-            /*
-             * Stores all the characters referenced by the GeoKeyDirectory. Should contains exactly one string
-             * which will be splitted by CRSBuilder, but we allow an arbitrary amount as a paranoiac check.
-             * Note that TIFF files use 0 as the end delimiter in strings (C/C++ convention).
-             */
-            case Tags.GeoAsciiParams: {
-                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;
-                }
-                break;
-            }
-            /*
-             * The orientation of the image with respect to the rows and columns.
-             * This is an integer numeroted from 1 to 7 inclusive (see TIFF specification for meaning).
-             */
-            case Tags.Orientation: {
-                // TODO
-                break;
-            }
-            /*
-             * The "grid to CRS" conversion as a 4×4 matrix in row-major fashion. The third matrix row and
-             * the third matrix column may contain only zero values; this block does not reduce the number
-             * of dimensions from 3 to 2.
-             */
-            case Tags.ModelTransformation: {
-                final Vector m = type.readVector(input(), count);
-                final int size = m.size();
-                final int n;
-                switch (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));
-                }
-                break;
-            }
-            /*
-             * The "grid to CRS" conversion with only the scale factor specified. This block sets the
-             * translation column to NaN, meaning that it will need to be computed from the tie point.
-             */
-            case Tags.ModelPixelScaleTag: {
-                final Vector m = type.readVector(input(), count);
-                final int size = m.size();
-                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);
-                }
-                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);
-                break;
-            }
-
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-            ////                                                                                        ////
-            ////    Metadata for discovery purposes, conditions of use, etc.                            ////
-            ////    Those metadata are not "critical" information for reading the image.                ////
-            ////    Should not write anything under 'metadata/contentInfo' node.                        ////
-            ////                                                                                        ////
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-
-            /*
-             * A string that describes the subject of the image.
-             * For example, a user may wish to attach a comment such as "1988 company picnic" to an image.
-             */
-            case Tags.ImageDescription: {
-                for (final String value : type.readString(input(), count, encoding())) {
-                    reader.metadata.addTitle(value);
-                }
-                break;
-            }
-            /*
-             * Person who created the image. Some older TIFF files used this tag for storing
-             * Copyright information, but Apache SIS does not support this legacy practice.
-             */
-            case Tags.Artist: {
-                for (final String value : type.readString(input(), count, encoding())) {
-                    reader.metadata.addAuthor(value);
-                }
-                break;
-            }
-            /*
-             * Copyright notice of the person or organization that claims the copyright to the image.
-             * Example: “Copyright, John Smith, 1992. All rights reserved.”
-             */
-            case Tags.Copyright: {
-                for (final String value : type.readString(input(), count, encoding())) {
-                    reader.metadata.parseLegalNotice(value);
-                }
-                break;
-            }
-            /*
-             * Date and time of image creation. The format is: "YYYY:MM:DD HH:MM:SS" with 24-hour clock.
-             */
-            case Tags.DateTime: {
-                for (final String value : type.readString(input(), count, encoding())) {
-                    reader.metadata.addCitationDate(reader.getDateFormat().parse(value),
-                            DateType.CREATION, MetadataBuilder.Scope.RESOURCE);
-                }
-                break;
-            }
-            /*
-             * The computer and/or operating system in use at the time of image creation.
-             */
-            case Tags.HostComputer: {
-                for (final String value : type.readString(input(), count, encoding())) {
-                    reader.metadata.addHostComputer(value);
-                }
-                break;
-            }
-            /*
-             * Name and version number of the software package(s) used to create the image.
-             */
-            case Tags.Software: {
-                for (final String value : type.readString(input(), count, encoding())) {
-                    reader.metadata.addSoftwareReference(value);
-                }
-                break;
-            }
-            /*
-             * Manufacturer of the scanner, video digitizer, or other type of equipment used to generate the image.
-             * Synthetic images should not include this field.
-             */
-            case Tags.Make: {
-                // TODO: is Instrument.citation.citedResponsibleParty.party.name an appropriate place?
-                // what would be the citation title? A copy of Tags.Model?
-                break;
-            }
-            /*
-             * The model name or number of the scanner, video digitizer, or other type of equipment used to
-             * generate the image.
-             */
-            case Tags.Model: {
-                for (final String value : type.readString(input(), count, encoding())) {
-                    reader.metadata.addInstrument(null, value);
-                }
-                break;
-            }
-            /*
-             * The number of pixels per ResolutionUnit in the ImageWidth or ImageHeight direction.
-             */
-            case Tags.XResolution:
-            case Tags.YResolution: {
-                final double r = type.readDouble(input(), count);
-                if (Double.isNaN(resolution) || r > resolution) {
-                    resolution = r;
-                }
-                break;
-            }
-            /*
-             * The unit of measurement for XResolution and YResolution.
-             *
-             *   1 = None. Used for images that may have a non-square aspect ratio.
-             *   2 = Inch (default).
-             *   3 = Centimeter.
-             */
-            case Tags.ResolutionUnit: {
-                final short unit = type.readShort(input(), count);
-                switch (unit) {
-                    case 1:  resolutionUnit = null;             break;
-                    case 2:  resolutionUnit = Units.INCH;       break;
-                    case 3:  resolutionUnit = Units.CENTIMETRE; break;
-                    default: return unit;                   // Cause a warning to be reported by the caller.
-                }
-                break;
-            }
-            /*
-             * For black and white TIFF files that represent shades of gray, the technique used to convert
-             * from gray to black and white pixels. The default value is 1 (nothing done on the image).
-             *
-             *   1 = No dithering or halftoning has been applied to the image data.
-             *   2 = An ordered dither or halftone technique has been applied to the image data.
-             *   3 = A randomized process such as error diffusion has been applied to the image data.
-             */
-            case Tags.Threshholding: {
-                final short value = type.readShort(input(), count);
-                switch (value) {
-                    case 1:  break;
-                    case 2:  if (cellWidth >= 0 || cellHeight >= 0) return null; else break;
-                    case 3:  break;
-                    default: return value;                  // Cause a warning to be reported by the caller.
-                }
-                cellWidth = cellHeight = (short) -value;
-                break;
-            }
-            /*
-             * The width and height of the dithering or halftoning matrix used to create
-             * a dithered or halftoned bilevel file. Meaningful only if Threshholding = 2.
-             */
-            case Tags.CellWidth: {
-                cellWidth = type.readShort(input(), count);
-                break;
-            }
-            case Tags.CellLength: {
-                cellHeight = type.readShort(input(), count);
-                break;
-            }
-
-            ////////////////////////////////////////////////////////////////////////////////////////////////
-            ////                                                                                        ////
-            ////    Defined by TIFF specification but currently ignored.                                ////
-            ////                                                                                        ////
-            ////////////////////////////////////////////////////////////////////////////////////////////////
 
-            /*
-             * For each string of contiguous unused bytes in a TIFF file, the number of bytes and the byte offset
-             * in the string. Those tags are deprecated and do not need to be supported.
-             */
-            case Tags.FreeByteCounts:
-            case Tags.FreeOffsets:
-            /*
-             * For grayscale data, the optical density of each possible pixel value, plus the precision of that
-             * information. This is ignored by most TIFF readers.
-             */
-            case Tags.GrayResponseCurve:
-            case Tags.GrayResponseUnit: {
-                warning(Level.FINE, Resources.Keys.IgnoredTag_1, Tags.name(tag));
-                break;
-            }
-        }
-        return null;
-    }
 
-    /**
-     * Sets the {@link #tileTagFamily} field to the given value if it does not conflict with previous value.
-     *
-     * @param  family  either {@link #TILE} or {@link #STRIP}.
-     * @throws DataStoreContentException if {@link #tileTagFamily} is already set to another value.
-     */
-    private void setTileTagFamily(final byte family) throws DataStoreContentException {
-        if (tileTagFamily != family && tileTagFamily != 0) {
-            throw new DataStoreContentException(reader.resources().getString(
-                    Resources.Keys.InconsistentTileStrip_1, filename()));
-        }
-        tileTagFamily = family;
-    }
+    ////////////////////////////////////////////////////////////////////////////////////////
+    ////                                                                                ////
+    ////    Information to be computed by GridGeometryBuilder based on above data.      ////
+    ////                                                                                ////
+    ////////////////////////////////////////////////////////////////////////////////////////
 
     /**
-     * Computes the minimal or maximal values of the given vector. Those vectors do not need to have the same length.
-     * One of those two vector will be modified in-place.
-     *
-     * @param  a    the first vector, or {@code null} if none.
-     * @param  b    the new vector to combine with the existing one. Can not be null.
-     * @param  max  {@code true} for computing the maximal values, or {@code false} for the minimal value.
+     * The grid geometry to be created by {@link #build(GridExtent)}.
      */
-    private static Vector extremum(Vector a, Vector b, final boolean max) {
-        if (a != null) {
-            int s = b.size();
-            int i = a.size();
-            if (i > s) {                            // If a vector is longer than b, swap a and b.
-                i = s;
-                final Vector t = a; a = b; b = t;
-            }
-            while (--i >= 0) {                      // At this point, 'b' shall be the longest vector.
-                final double va = a.doubleValue(i);
-                final double vb = b.doubleValue(i);
-                if (Double.isNaN(vb) || (max ? va > vb : va < vb)) {
-                    b.set(i, va);
-                }
-            }
-        }
-        return b;
-    }
+    public GridGeometry gridGeometry;
 
     /**
-     * Multiplies the given value by the number of bytes in one pixel,
-     * or return -1 if the result is not an integer.
-     *
-     * @throws ArithmeticException if the result overflows.
+     * 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 long pixelToByteCount(long value) {
-        value = Math.multiplyExact(value, samplesPerPixel * (int) bitsPerSample);
-        return (value % Byte.SIZE == 0) ? value / Byte.SIZE : -1;
-    }
+    private String description;
 
     /**
-     * Computes the tile width or height from the other size,
-     * or returns a negative number if the size can not be computed.
-     *
-     * @param  knownSize  the tile width or height.
-     * @return the tile width if the known size was height, or the tile height if the known size was width,
-     *         or a negative number if the width or height can not be computed.
-     * @throws ArithmeticException if the result overflows.
+     * {@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 int computeTileSize(final int knownSize) {
-        final int n = tileByteCounts.size();
-        if (n != 0) {
-            final long count = tileByteCounts.longValue(0);
-            int i = 0;
-            do if (++i == n) {
-                // At this point, we verified that all vector values are equal.
-                final long length = pixelToByteCount(knownSize);
-                if (count % length != 0) break;
-                return Math.toIntExact(count / length);
-            } while (tileByteCounts.longValue(i) == n);
-        }
-        return -1;
-    }
+    private CellGeometry cellGeometry;
 
     /**
-     * Verifies that the mandatory tags are present and consistent with each others.
-     * If a mandatory tag is absent, then there is a choice:
-     *
-     * <ul>
-     *   <li>If the tag can be inferred from other tag values, performs that computation and logs a warning.</li>
-     *   <li>Otherwise throws an exception.</li>
-     * </ul>
-     *
-     * This method opportunistically computes default value of optional fields
-     * when those values can be computed from other (usually mandatory) fields.
+     * Creates a new builder.
      *
-     * @throws DataStoreContentException if a mandatory tag is missing and can not be inferred.
+     * @param reader  where to report warnings if any.
      */
-    final void validateMandatoryTags() throws DataStoreContentException {
-        if (imageWidth  < 0) throw missingTag(Tags.ImageWidth);
-        if (imageHeight < 0) throw missingTag(Tags.ImageLength);
-        final short offsetsTag, byteCountsTag;
-        switch (tileTagFamily) {
-            case STRIP: {
-                if (tileWidth  < 0) tileWidth  = Math.toIntExact(imageWidth);
-                if (tileHeight < 0) tileHeight = Math.toIntExact(imageHeight);
-                offsetsTag    = Tags.StripOffsets;
-                byteCountsTag = Tags.StripByteCounts;
-                break;
-            }
-            case TILE:  {
-                offsetsTag    = Tags.TileOffsets;
-                byteCountsTag = Tags.TileByteCounts;
-                break;
-            }
-            default: {
-                throw new DataStoreContentException(reader.resources().getString(
-                        Resources.Keys.InconsistentTileStrip_1, filename()));
-            }
-        }
-        if (tileOffsets == null) {
-            throw missingTag(offsetsTag);
-        }
-        if (samplesPerPixel == 0) {
-            samplesPerPixel = 1;
-            missingTag(Tags.SamplesPerPixel, 1, false);
-        }
-        if (bitsPerSample == 0) {
-            bitsPerSample = 1;
-            missingTag(Tags.BitsPerSample, 1, false);
-        }
-        if (colorMap != null) {
-            ensureSameLength(Tags.ColorMap, Tags.BitsPerSample, colorMap.size(),  3 * (1 << bitsPerSample));
-        }
-        if (sampleFormat != FLOAT) {
-            long minValue, maxValue;
-            if (sampleFormat == UNSIGNED) {
-                minValue =  0L;
-                maxValue = -1L;                 // All bits set to 1.
-            } else {
-                minValue = Long.MIN_VALUE;
-                maxValue = Long.MAX_VALUE;
-            }
-            final int shift = Long.SIZE - bitsPerSample;
-            if (shift >= 0 && shift < Long.SIZE) {
-                minValue >>>= shift;
-                maxValue >>>= shift;
-                if (minValue < maxValue) {      // Exclude the unsigned long case since we can not represent it.
-                    minValues = extremum(minValues, Vector.createSequence(minValue, 0, samplesPerPixel), false);
-                    maxValues = extremum(maxValues, Vector.createSequence(maxValue, 0, samplesPerPixel), true);
-                }
-            }
-        }
-        /*
-         * All of tile width, height and length information should be provided. But if only one of them is missing,
-         * we can compute it provided that the file does not use any compression method. If there is a compression,
-         * then we set a bit for preventing the 'switch' block to perform a calculation but we let the code performs
-         * the other checks in order to get an exception to be thrown with a good message.
-         */
-        int missing = !isPlanar && compression.equals(Compression.NONE) ? 0 : 0b1000;
-        if (tileWidth      < 0)     missing |= 0b0001;
-        if (tileHeight     < 0)     missing |= 0b0010;
-        if (tileByteCounts == null) missing |= 0b0100;
-        switch (missing) {
-            case 0:
-            case 0b1000: {          // Every thing is ok.
-                break;
-            }
-            case 0b0001: {          // Compute missing tile width.
-                tileWidth = computeTileSize(tileHeight);
-                missingTag(Tags.TileWidth, tileWidth, true);
-                break;
-            }
-            case 0b0010: {          // Compute missing tile height.
-                tileHeight = computeTileSize(tileWidth);
-                missingTag(Tags.TileLength, tileHeight, true);
-                break;
-            }
-            case 0b0100: {          // Compute missing tile byte count.
-                final long tileByteCount = pixelToByteCount(Math.multiplyExact(tileWidth, tileHeight));
-                final long[] tileByteCountArray = new long[tileOffsets.size()];
-                Arrays.fill(tileByteCountArray, tileByteCount);
-                tileByteCounts = Vector.create(tileByteCountArray, true);
-                missingTag(byteCountsTag, tileByteCount, true);
-                break;
-            }
-            default: {
-                final short tag;
-                switch (Integer.lowestOneBit(missing)) {
-                    case 0b0001: tag = Tags.TileWidth;  break;
-                    case 0b0010: tag = Tags.TileLength; break;
-                    default:     tag = byteCountsTag;   break;
-                }
-                throw missingTag(tag);
-            }
-        }
-        /*
-         * Report an error if the tile offset and tile byte count vectors do not have the same length.
-         * Then ensure that the number of tiles is equal to the expected number. The formula below is the
-         * one documented in the TIFF specification and reproduced in tileWidth & tileHeight fields javadoc.
-         */
-        ensureSameLength(offsetsTag, byteCountsTag, tileOffsets.size(), tileByteCounts.size());
-        long expectedCount = Math.multiplyExact(
-                Math.addExact(imageWidth,  tileWidth  - 1) / tileWidth,
-                Math.addExact(imageHeight, tileHeight - 1) / tileHeight);
-        if (isPlanar) {
-            expectedCount = Math.multiplyExact(expectedCount, samplesPerPixel);
-        }
-        final int actualCount = Math.min(tileOffsets.size(), tileByteCounts.size());
-        if (actualCount != expectedCount) {
-            throw new DataStoreContentException(reader.resources().getString(Resources.Keys.UnexpectedTileCount_3,
-                    filename(), expectedCount, actualCount));
-        }
-        /*
-         * 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);
-            }
-        }
+    GridGeometryBuilder(final Reader reader) {
+        this.reader = reader;
     }
 
     /**
-     * Completes the metadata with the information stored in the field of this IFD.
-     * This method is invoked only if the user requested the ISO 19115 metadata.
-     * This method creates a new {@code "metadata/contentInfo"} node for this image.
-     * Information not under the {@code "metadata/contentInfo"} node will be merged
-     * with the current content of the given {@code MetadataBuilder}.
-     *
-     * @param   metadata  where to write metadata information. Caller should have already invoked
-     *                    {@link MetadataBuilder#setFormat(String)} before {@code completeMetadata(…)} calls.
+     * 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.
      */
-    final void completeMetadata(final MetadataBuilder metadata, final Locale locale)
-            throws DataStoreContentException, FactoryException
-    {
-        metadata.newCoverage(false);
-        if (compression != null) {
-            metadata.addCompression(compression.name().toLowerCase(locale));
-        }
-        for (int band = 0; band < samplesPerPixel;) {
-            metadata.newSampleDimension();
-            metadata.setBitPerSample(bitsPerSample);
-            if (isMinSpecified) metadata.addMinimumSampleValue(minValues.doubleValue(Math.min(band, minValues.size()-1)));
-            if (isMaxSpecified) metadata.addMaximumSampleValue(maxValues.doubleValue(Math.min(band, maxValues.size()-1)));
-            metadata.setBandIdentifier(++band);
-        }
-        /*
-         * Add the resolution into the metadata. Our current ISO 19115 implementation restricts
-         * the resolution unit to metres, but it may be relaxed in a future SIS version.
-         */
-        if (!Double.isNaN(resolution) && resolutionUnit != null) {
-            metadata.addResolution(resolutionUnit.getConverterTo(Units.METRE).convert(resolution));
-        }
-        /*
-         * Cell size is relevant only if the Threshholding TIFF tag value is 2. By convention in
-         * this implementation class, other Threshholding values are stored as negative cell sizes:
-         *
-         *   -1 means that Threshholding is 1 or unspecified.
-         *   -2 means that Threshholding is 2 but the matrix size has not yet been specified.
-         *   -3 means that Threshholding is 3 (randomized process such as error diffusion).
-         */
-        switch (Math.min(cellWidth, cellHeight)) {
-            case -1: {
-                // Nothing to report.
-                break;
-            }
-            case -3: {
-                metadata.addProcessDescription(Resources.formatInternational(Resources.Keys.RandomizedProcessApplied));
-                break;
-            }
-            default: {
-                metadata.addProcessDescription(Resources.formatInternational(
-                            Resources.Keys.DitheringOrHalftoningApplied_2,
-                            (cellWidth  >= 0) ? cellWidth  : '?',
-                            (cellHeight >= 0) ? cellHeight : '?'));
-                break;
-            }
-        }
-        /*
-         * Add Coordinate Reference System built from GeoTIFF tags.  Note that the CRS may not exist,
-         * 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);
+    public GridGeometry build(final GridExtent extent) throws FactoryException {
         CoordinateReferenceSystem crs = null;
-        if (geoKeyDirectory != null) {
+        if (keyDirectory != null) {
             final CRSBuilder helper = new CRSBuilder(reader);
             try {
-                crs = helper.build(geoKeyDirectory, numericGeoParameters, asciiGeoParameters);
-                metadata.addReferenceSystem(crs);
-                helper.complete(metadata);
+                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) {
@@ -1252,60 +296,73 @@ final class ImageFileDirectory {
                 }
             }
         }
+        boolean pixelIsPoint = CellGeometry.POINT.equals(cellGeometry);
         try {
-            if (!isGeorectified) {
-                metadata.addGeolocation(new Localization(filename(), crs, modelTiePoints));
+            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.
+             */
         }
-        geoKeyDirectory      = null;            // Not needed anymore, so let GC do its work.
-        numericGeoParameters = null;
-        asciiGeoParameters   = null;
-        modelTiePoints       = null;
+        keyDirectory      = null;            // Not needed anymore, so let GC do its work.
+        numericParameters = null;
+        asciiParameters   = null;
+        modelTiePoints    = null;
+        affine            = null;
+        return gridGeometry;
     }
 
     /**
-     * Reports a warning with a message created from the given resource keys and parameters.
+     * Completes ISO 19115 metadata with some GeoTIFF values inferred from the geotags.
      *
-     * @param  level       the logging level for the message to log.
-     * @param  key         the {@code Resources} key of the message to format.
-     * @param  parameters  the parameters to put in the message.
-     */
-    private void warning(final Level level, final short key, final Object... parameters) {
-        final LogRecord r = reader.resources().getLogRecord(level, key, parameters);
-        reader.owner.warning(r);
-    }
-
-    /**
-     * Verifies that the given tags have the same length and reports a warning if they do not.
-     */
-    private void ensureSameLength(final short tag1, final short tag2, final int length1, final int length2) {
-        if (length1 != length2) {
-            warning(Level.WARNING, Resources.Keys.MismatchedLength_4, Tags.name(tag1), Tags.name(tag2), length1, length2);
-        }
-    }
-
-    /**
-     * Reports a warning for a missing TIFF tag for which a default value can be computed.
+     * <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>
      *
-     * @param  missing   the numerical value of the missing tag.
-     * @param  value     the default value or the computed value.
-     * @param  computed  whether the default value has been computed.
-     */
-    private void missingTag(final short missing, final long value, final boolean computed) {
-        warning(computed ? Level.WARNING : Level.FINE,
-                computed ? Resources.Keys.ComputedValueForAttribute_2 : Resources.Keys.DefaultValueForAttribute_2,
-                Tags.name(missing), value);
-    }
-
-    /**
-     * Builds an exception for a missing TIFF tag for which no default value can be computed.
+     * This method invokes {@link MetadataBuilder#newGridRepresentation(MetadataBuilder.GridType)}
+     * with the appropriate {@code GEORECTIFIED} or {@code GEOREFERENCEABLE} type.
      *
-     * @param  missing  the numerical value of the missing tag.
+     * @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.
      */
-    private DataStoreContentException missingTag(final short missing) {
-        return new DataStoreContentException(reader.resources().getString(
-                Resources.Keys.MissingValue_2, filename(), Tags.name(missing)));
+    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);
     }
 }



Mime
View raw message