sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 31/35: Take in account the change in number of dimensions when creating GeoTIFF GridGeometry. Fill more metadata using GridGeometry information.
Date Wed, 20 Jun 2018 13:48:26 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 31584c74f0dcc90167d23b9422982e51c8e361d9
Author: Martin Desruisseaux <desruisseaux@apache.org>
AuthorDate: Fri Jun 8 13:07:55 2018 +0000

    Take in account the change in number of dimensions when creating GeoTIFF GridGeometry.
    Fill more metadata using GridGeometry information.
    
    
    git-svn-id: https://svn.apache.org/repos/asf/sis/branches/JDK8@1833166 13f79535-47bb-0310-9956-ffa450edef68
---
 .../org/apache/sis/coverage/grid/GridExtent.java   | 182 ++++++++++++++++++---
 .../org/apache/sis/coverage/grid/GridGeometry.java |  98 ++++++++++-
 .../apache/sis/coverage/grid/GridGeometryTest.java |  13 +-
 .../java/org/apache/sis/util/resources/Errors.java |   5 +
 .../apache/sis/util/resources/Errors.properties    |   1 +
 .../apache/sis/util/resources/Errors_fr.properties |   1 +
 .../org/apache/sis/internal/geotiff/Resources.java |   2 +-
 .../sis/internal/geotiff/Resources.properties      |   2 +-
 .../sis/internal/geotiff/Resources_fr.properties   |   2 +-
 .../org/apache/sis/storage/geotiff/CRSBuilder.java |  17 +-
 .../sis/storage/geotiff/GridGeometryBuilder.java   |  85 ++++++----
 .../sis/storage/geotiff/ImageFileDirectory.java    |  11 +-
 .../apache/sis/storage/geotiff/Localization.java   |   5 +-
 .../apache/sis/storage/netcdf/MetadataReader.java  |  34 ++--
 .../sis/internal/storage/MetadataBuilder.java      |  92 ++++++++++-
 15 files changed, 456 insertions(+), 94 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index 6c5604c..9d972de 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -17,16 +17,21 @@
 package org.apache.sis.coverage.grid;
 
 import java.util.Arrays;
+import java.util.Optional;
 import java.io.Serializable;
 import org.opengis.geometry.DirectPosition;
+import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.collection.WeakValueHashMap;
 import org.apache.sis.internal.raster.Resources;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.io.TableAppender;
+import org.apache.sis.util.iso.Types;
 
 // Branch-dependent imports
 import org.opengis.coverage.grid.GridEnvelope;
@@ -61,6 +66,30 @@ public class GridExtent implements Serializable {
     private static final long serialVersionUID = -4717353677844056017L;
 
     /**
+     * Default axis types for the two-dimensional cases.
+     */
+    private static final DimensionNameType[] DEFAULT_TYPES = new DimensionNameType[] {
+        DimensionNameType.COLUMN,
+        DimensionNameType.ROW
+    };
+
+    /**
+     * A pool of shared {@link DimensionNameType} arrays. We use a pool
+     * because a small amount of arrays is shared by most grid extent.
+     */
+    private static final WeakValueHashMap<DimensionNameType[],DimensionNameType[]> POOL = new WeakValueHashMap<>(DimensionNameType[].class);
+
+    /**
+     * Type of each axis (vertical, temporal, …) or {@code null} if unspecified.
+     * If non-null, the array length shall be equal to {@link #getDimension()}.
+     * Any array element may be null if unspecified for that particular axis.
+     * The same array may be shared by many {@code GridExtent} instances.
+     *
+     * @see #getAxisType(int)
+     */
+    private final DimensionNameType[] types;
+
+    /**
      * Minimum and maximum grid ordinates. The first half contains minimum ordinates (inclusive),
      * while the last half contains maximum ordinates (<strong>inclusive</strong>). Note that the
      * later is the opposite of Java2D usage but conform to ISO specification.
@@ -99,14 +128,72 @@ public class GridExtent implements Serializable {
     }
 
     /**
+     * Verifies that the given array (if non-null) contains no duplicated values, then returns a copy of that array.
+     * The returned copy may be shared by many {@code GridExtent} instances. Consequently it shall not be modified.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    private static DimensionNameType[] validateAxisTypes(DimensionNameType[] types) {
+        if (types == null) {
+            return null;
+        }
+        if (Arrays.equals(DEFAULT_TYPES, types)) {          // Common case verified before POOL synchronized lock.
+            return DEFAULT_TYPES;
+        }
+        DimensionNameType[] shared = POOL.get(types);
+        if (shared == null) {
+            /*
+             * Verify the array only if it was not found in the pool. Arrays in the pool were already validated,
+             * so do not need to be verified again. The check performed here is inefficient (nested loop), but it
+             * should be okay since the arrays are usually small (less than 5 elements) and the checks should not
+             * be done often (because of the pool).
+             */
+            types = types.clone();
+            for (int i=1; i<types.length; i++) {
+                final DimensionNameType t = types[i];
+                if (t != null) {
+                    for (int j=i; --j >= 0;) {
+                        if (t.equals(types[j])) {
+                            throw new IllegalArgumentException(Errors.format(Errors.Keys.DuplicatedElement_1, t));
+                        }
+                    }
+                }
+            }
+            shared = POOL.putIfAbsent(types, types);
+            if (shared == null) {
+                return types;
+            }
+        }
+        return shared;
+    }
+
+    /**
      * Creates an initially empty grid envelope with the given number of dimensions.
      * All grid coordinate values are initialized to zero. This constructor is private
      * since {@code GridExtent} coordinate values can not be modified by public API.
      *
      * @param dimension  number of dimensions.
+     * @param axisTypes  the axis types, or {@code null} if unspecified.
      */
-    private GridExtent(final int dimension) {
+    private GridExtent(final int dimension, final DimensionNameType[] axisTypes) {
         ordinates = allocate(dimension);
+        types = validateAxisTypes(axisTypes);
+    }
+
+    /**
+     * Creates a new grid extent for an image or matrix of the given size.
+     * The {@linkplain #getLow() low} grid coordinates are zeros and the axis types are
+     * {@link DimensionNameType#COLUMN} and {@link DimensionNameType#ROW ROW} in that order.
+     *
+     * @param  width   number of pixels in each row.
+     * @param  height  number of pixels in each column.
+     */
+    public GridExtent(final long width, final long height) {
+        ArgumentChecks.ensureStrictlyPositive("width",  width);
+        ArgumentChecks.ensureStrictlyPositive("height", height);
+        ordinates = new long[4];
+        ordinates[2] = width  - 1;
+        ordinates[3] = height - 1;
+        types = DEFAULT_TYPES;
     }
 
     /**
@@ -115,8 +202,21 @@ public class GridExtent implements Serializable {
      * The lowest valid grid coordinates are often zero, but this is not mandatory.
      * As a convenience for this common case, a null {@code low} array means that all low coordinates are zero.
      *
-     * @param  low   the valid minimum grid coordinate (always inclusive), or {@code null} for all zeros.
-     * @param  high  the valid maximum grid coordinate, inclusive or exclusive depending on the next argument.
+     * <p>An optional (nullable) {@code axisTypes} argument can be used for attaching a label to each grid axis.
+     * For example if this {@code GridExtent} is four-dimensional, then the axis types may be
+     * {{@linkplain DimensionNameType#COLUMN   column}  (<var>x</var>),
+     *  {@linkplain DimensionNameType#ROW      row}     (<var>y</var>),
+     *  {@linkplain DimensionNameType#VERTICAL vertical (<var>z</var>),
+     *  {@linkplain DimensionNameType#TIME     time}    (<var>t</var>)},
+     * which means that the last axis is for the temporal dimension, the third axis is for the vertical dimension, <i>etc.</i>
+     * This information is related to the "real world" coordinate reference system axes, but not necessarily in the same order;
+     * it is caller responsibility to ensure that the grid axes are consistent with the CRS axes.
+     * The {@code axisTypes} array shall not contain duplicated elements,
+     * but may contain {@code null} elements if the type of some axes are unknown.</p>
+     *
+     * @param  axisTypes  the type of each grid axis, or {@code null} if unspecified.
+     * @param  low    the valid minimum grid coordinate (always inclusive), or {@code null} for all zeros.
+     * @param  high   the valid maximum grid coordinate, inclusive or exclusive depending on the next argument.
      * @param  isHighIncluded  {@code true} if the {@code high} values are inclusive (as in ISO 19123 specification),
      *         or {@code false} if they are exclusive (as in Java2D usage).
      *         This argument does not apply to {@code low} values, which are always inclusive.
@@ -126,12 +226,15 @@ public class GridExtent implements Serializable {
      * @see #getLow()
      * @see #getHigh()
      */
-    public GridExtent(final long[] low, final long[] high, final boolean isHighIncluded) {
+    public GridExtent(final DimensionNameType[] axisTypes, final long[] low, final long[] high, final boolean isHighIncluded) {
         ArgumentChecks.ensureNonNull("high", high);
         final int dimension = high.length;
         if (low != null && low.length != dimension) {
             throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedDimension_2, low.length, dimension));
         }
+        if (axisTypes != null && axisTypes.length != dimension) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedArrayLengths));
+        }
         ordinates = allocate(dimension);
         if (low != null) {
             System.arraycopy(low, 0, ordinates, 0, dimension);
@@ -143,6 +246,7 @@ public class GridExtent implements Serializable {
             }
         }
         checkCoherence(ordinates);
+        types = validateAxisTypes(axisTypes);
     }
 
     /**
@@ -161,6 +265,7 @@ public class GridExtent implements Serializable {
             ordinates[i + dimension] = extent.getHigh(i);
         }
         checkCoherence(ordinates);
+        types = (extent instanceof GridExtent) ? ((GridExtent) extent).types : null;
     }
 
     /**
@@ -251,16 +356,16 @@ public class GridExtent implements Serializable {
      * Returns the number of integer grid coordinates along the specified dimension.
      * This is equal to {@code getHigh(dimension) - getLow(dimension) + 1}.
      *
-     * @param  index  the dimension for which to obtain the span.
-     * @return the span at the given dimension.
+     * @param  index  the dimension for which to obtain the size.
+     * @return the number of integer grid coordinates along the given dimension.
      * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
      *         than the {@linkplain #getDimension() grid dimension}.
-     * @throws ArithmeticException if the span is too large for the {@code long} primitive type.
+     * @throws ArithmeticException if the size is too large for the {@code long} primitive type.
      *
      * @see #getLow(int)
      * @see #getHigh(int)
      */
-    public long getSpan(final int index) {
+    public long getSize(final int index) {
         final int dimension = getDimension();
         ArgumentChecks.ensureValidIndex(dimension, index);
         return Math.incrementExact(Math.subtractExact(ordinates[dimension + index], ordinates[index]));
@@ -284,6 +389,33 @@ public class GridExtent implements Serializable {
     }
 
     /**
+     * Returns the type (vertical, temporal, …) of grid axis at given dimension.
+     * This information is provided because the grid axis type can not always be inferred from the context.
+     * Some examples are:
+     *
+     * <ul>
+     *   <li>{@code getAxisType(0)} may return {@link DimensionNameType#COLUMN},
+     *       {@link DimensionNameType#TRACK TRACK} or {@link DimensionNameType#LINE LINE}.</li>
+     *   <li>{@code getAxisType(1)} may return {@link DimensionNameType#ROW},
+     *       {@link DimensionNameType#CROSS_TRACK CROSS_TRACK} or {@link DimensionNameType#SAMPLE SAMPLE}.</li>
+     *   <li>{@code getAxisType(2)} may return {@link DimensionNameType#VERTICAL}.</li>
+     *   <li>{@code getAxisType(3)} may return {@link DimensionNameType#TIME}.</li>
+     * </ul>
+     *
+     * Above are only examples; there is no constraint on axis order. In particular grid axes do not need to be in the same
+     * order than the corresponding {@linkplain GridGeometry#getCoordinateReferenceSystem() coordinate reference system} axes.
+     *
+     * @param  index  the dimension for which to obtain the axis type.
+     * @return the axis type at the given dimension. May be absent if the type is unknown.
+     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     *         than the {@linkplain #getDimension() grid dimension}.
+     */
+    public Optional<DimensionNameType> getAxisType(final int index) {
+        ArgumentChecks.ensureValidIndex(getDimension(), index);
+        return Optional.ofNullable((types != null) ? types[index] : null);
+    }
+
+    /**
      * Transforms this grid extent to a "real world" envelope using the given transform.
      * The transform shall map <em>pixel corner</em> to real world coordinates.
      *
@@ -307,18 +439,21 @@ public class GridExtent implements Serializable {
      *
      * @param  lower  the first dimension to copy, inclusive.
      * @param  upper  the last  dimension to copy, exclusive.
-     * @return the sub grid envelope.
+     * @return the sub-envelope, or {@code this} if [{@code lower} … {@code upper}] is [0 … {@link #getDimension() dimension}].
      * @throws IndexOutOfBoundsException if an index is out of bounds.
      */
     public GridExtent subExtent(final int lower, final int upper) {
         final int dimension = getDimension();
-        if (lower == 0 && upper == dimension) return this;
         ArgumentChecks.ensureValidIndexRange(dimension, lower, upper);
         final int newDim = upper - lower;
-        if (newDim == dimension && getClass() == GridExtent.class) {
+        if (newDim == dimension) {
             return this;
         }
-        final GridExtent sub = new GridExtent(newDim);
+        DimensionNameType[] axisTypes = types;
+        if (axisTypes != null) {
+            axisTypes = Arrays.copyOfRange(axisTypes, lower, upper);
+        }
+        final GridExtent sub = new GridExtent(newDim, axisTypes);
         System.arraycopy(ordinates, lower,           sub.ordinates, 0,      newDim);
         System.arraycopy(ordinates, lower+dimension, sub.ordinates, newDim, newDim);
         return sub;
@@ -332,7 +467,7 @@ public class GridExtent implements Serializable {
      */
     @Override
     public int hashCode() {
-        return Arrays.hashCode(ordinates) ^ (int) serialVersionUID;
+        return Arrays.hashCode(ordinates) + Arrays.hashCode(types) ^ (int) serialVersionUID;
     }
 
     /**
@@ -343,8 +478,9 @@ public class GridExtent implements Serializable {
      */
     @Override
     public boolean equals(final Object object) {
-        if (object instanceof GridExtent) {
-            return Arrays.equals(ordinates, ((GridExtent) object).ordinates);
+        if (object != null && object.getClass() == GridExtent.class) {
+            final GridExtent other = (GridExtent) object;
+            return Arrays.equals(ordinates, other.ordinates) && Arrays.equals(types, other.types);
         }
         return false;
     }
@@ -355,12 +491,20 @@ public class GridExtent implements Serializable {
      */
     @Override
     public String toString() {
+        final TableAppender table = new TableAppender(" ");
         final int dimension = getDimension();
-        final StringBuilder buffer = new StringBuilder("GridEnvelope").append('[');
         for (int i=0; i<dimension; i++) {
-            if (i != 0) buffer.append(", ");
-            buffer.append(ordinates[i]).append('…').append(ordinates[i + dimension]);
+            String name;
+            if ((types == null) || (name = Types.getCodeLabel(types[i])) == null) {
+                name = Integer.toString(i);
+            }
+            table.append(name).append(':').nextColumn();
+            table.setCellAlignment(TableAppender.ALIGN_RIGHT);
+            table.append(Long.toString(ordinates[i])).nextColumn();
+            table.append("to").nextColumn();
+            table.append(Long.toString(ordinates[i + dimension])).nextLine();
+            table.setCellAlignment(TableAppender.ALIGN_LEFT);
         }
-        return buffer.append(']').toString();
+        return table.toString();
     }
 }
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index de064a0..7804e5d 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
@@ -27,6 +27,7 @@ import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.cs.CoordinateSystem;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.ImmutableEnvelope;
@@ -35,6 +36,8 @@ import org.apache.sis.referencing.operation.transform.PassThroughTransform;
 import org.apache.sis.internal.raster.Resources;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.Debug;
 
 
 /**
@@ -711,9 +714,102 @@ public class GridGeometry implements Serializable {
     /**
      * Returns a string representation of this grid geometry. The returned string
      * is implementation dependent. It is provided for debugging purposes only.
+     * Current implementation is equivalent to the following:
+     *
+     * {@preformat java
+     *   return toString(EXTENT | CRS | GRID_TO_CRS | RESOLUTION);
+     * }
      */
     @Override
     public String toString() {
-        return getClass().getSimpleName() + '[' + extent + ", " + gridToCRS + ']';
+        return toString(EXTENT | CRS | GRID_TO_CRS | RESOLUTION);
+    }
+
+    /**
+     * Returns a string representation of some elements of this grid geometry.
+     * The string representation is for debugging purpose only and may change
+     * in any future SIS version.
+     *
+     * @param  bitmask  any combination of {@link #EXTENT}, {@link #CRS},
+     *         {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
+     * @return a string representation of the given elements.
+     */
+    @Debug
+    public String toString(final int bitmask) {
+        if ((bitmask & ~(EXTENT | CRS | GRID_TO_CRS | RESOLUTION)) != 0) {
+            throw new IllegalArgumentException(Errors.format(
+                    Errors.Keys.IllegalArgumentValue_2, "bitmask", bitmask));
+        }
+        final boolean visible = Integer.bitCount(bitmask) >= 2;
+        final int dimension = (extent != null) ? extent.getDimension() : 0;
+        final StringBuilder buffer = new StringBuilder();
+        if ((bitmask & EXTENT) != 0) {
+            appendLabel(buffer, "Grid size", visible);
+            if (dimension == 0) {
+                buffer.append("unspecified");
+            } else for (int i=0; i<dimension; i++) {
+                if (i != 0) buffer.append(" × ");
+                buffer.append(extent.getSize(i));
+            }
+            appendLabel(buffer, "Grid low", visible);
+            if (dimension == 0) {
+                buffer.append("unspecified");
+            } else for (int i=0; i<dimension; i++) {
+                if (i != 0) buffer.append(", ");
+                buffer.append(extent.getLow(i));
+            }
+        }
+        CoordinateSystem cs = null;
+        if ((bitmask & CRS) != 0) {
+            appendLabel(buffer, "CRS", visible);
+            CoordinateReferenceSystem crs;
+            if (envelope == null || (crs = envelope.getCoordinateReferenceSystem()) == null) {
+                buffer.append("unspecified");
+            } else {
+                buffer.append(crs.getName());
+                cs = crs.getCoordinateSystem();
+            }
+        }
+        if ((bitmask & GRID_TO_CRS) != 0) {
+            appendLabel(buffer, "Conversion", visible);
+            if (gridToCRS == null) {
+                buffer.append("unspecified");
+            } else {
+                buffer.append(gridToCRS.getSourceDimensions()).append("D → ")
+                      .append(gridToCRS.getTargetDimensions()).append('D');
+                long nonLinearDimensions = nonLinears;
+                String separator = " non linear in ";
+                while (nonLinearDimensions != 0) {
+                    final int i = Long.numberOfTrailingZeros(nonLinearDimensions);
+                    nonLinearDimensions &= ~(1L << i);
+                    buffer.append(separator).append(cs != null ? cs.getAxis(i).getName() : String.valueOf(i));
+                    separator = ", ";
+                }
+            }
+        }
+        if ((bitmask & RESOLUTION) != 0) {
+            appendLabel(buffer, "Resolution", visible);
+            if (resolution == null) {
+                buffer.append("unspecified");
+            } for (int i=0; i<resolution.length; i++) {
+                if (i != 0) buffer.append(" × ");
+                buffer.append((float) resolution[i]);
+                if (cs != null) {
+                    buffer.append(' ').append(cs.getAxis(i).getUnit());
+                }
+            }
+        }
+        return buffer.append(System.lineSeparator()).toString();
+    }
+
+    /**
+     * Appends the given text to the given buffer, followed by colon and spaces.
+     * Adjusted for the specific needs of {@link #toString()} implementation.
+     */
+    private static void appendLabel(final StringBuilder appendTo, final String label, final boolean visible) {
+        if (appendTo.length() != 0) appendTo.append(System.lineSeparator());
+        if (visible) {
+            appendTo.append(label).append(':').append(CharSequences.spaces(12 - label.length()));
+        }
     }
 }
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 164907e..f660f76 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
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.coverage.grid;
 
+import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
@@ -56,7 +57,7 @@ public final strictfp class GridGeometryTest extends TestCase {
     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);
+        final GridExtent    extent   = new GridExtent(null, low, high, true);
         final MathTransform identity = MathTransforms.identity(4);
         final GridGeometry  grid     = new GridGeometry(extent, PixelInCell.CELL_CORNER, identity, null);
         /*
@@ -102,7 +103,7 @@ public final strictfp class GridGeometryTest extends TestCase {
     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);
+        final GridExtent    extent   = new GridExtent(null, low, high, true);
         final MathTransform identity = MathTransforms.identity(3);
         final GridGeometry  grid     = new GridGeometry(extent, PixelInCell.CELL_CENTER, identity, null);
         /*
@@ -148,7 +149,7 @@ public final strictfp class GridGeometryTest extends TestCase {
     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 GridExtent    extent   = new GridExtent(null, low, high, true);
         final MathTransform identity = MathTransforms.linear(new Matrix3(
                 1, 0, 0.5,
                 0, 1, 0.5,
@@ -165,6 +166,12 @@ public final strictfp class GridGeometryTest extends TestCase {
     @Test
     public void testNonLinear() throws TransformException {
         final GridExtent extent = new GridExtent(
+                new DimensionNameType[] {
+                    DimensionNameType.COLUMN,
+                    DimensionNameType.ROW,
+                    DimensionNameType.VERTICAL,
+                    DimensionNameType.TIME
+                },
                 new long[] {0,     0, 2, 6},
                 new long[] {100, 200, 3, 9}, false);
         final MathTransform horizontal = MathTransforms.linear(Matrices.create(3, 3, new double[] {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
index 6dce9dc..2b15ca1 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
@@ -86,6 +86,11 @@ public final class Errors extends IndexedResourceBundle {
         public static final short CanNotAssignUnitToDimension_2 = 3;
 
         /**
+         * Can not assign units “{1}” to variable “{0}”.
+         */
+        public static final short CanNotAssignUnitToVariable_2 = 183;
+
+        /**
          * Can not assign “{1}” to “{0}”.
          */
         public static final short CanNotAssign_2 = 4;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
index 3a4e67a..864ce7c 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
@@ -29,6 +29,7 @@ CanIterateOnlyOnce                = This object can iterate only once.
 CanNotAddToExclusiveSet_2         = No element can be added to this set because properties \u2018{0}\u2019 and \u2018{1}\u2019 are mutually exclusive.
 CanNotAssign_2                    = Can not assign \u201c{1}\u201d to \u201c{0}\u201d.
 CanNotAssignUnitToDimension_2     = Can not assign units \u201c{1}\u201d to dimension \u201c{0}\u201d.
+CanNotAssignUnitToVariable_2      = Can not assign units \u201c{1}\u201d to variable \u201c{0}\u201d.
 CanNotConnectTo_1                 = Can not connect to \u201c{0}\u201d.
 CanNotConvertFromType_2           = Can not convert from type \u2018{0}\u2019 to type \u2018{1}\u2019.
 CanNotConvertValue_2              = Can not convert value \u201c{0}\u201d to type \u2018{1}\u2019.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
index e4cbcef..15f2d1d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
@@ -26,6 +26,7 @@ CanIterateOnlyOnce                = Cet objet ne peut it\u00e9rer qu\u2019une se
 CanNotAddToExclusiveSet_2         = Aucun \u00e9l\u00e9ment ne peut \u00eatre ajout\u00e9 \u00e0 cet ensemble car les propri\u00e9t\u00e9s \u2018{0}\u2019 et \u2018{1}\u2019 sont mutuellement exclusives.
 CanNotAssign_2                    = Ne peut pas assigner \u00ab\u202f{1}\u202f\u00bb \u00e0 \u00ab\u202f{0}\u202f\u00bb.
 CanNotAssignUnitToDimension_2     = Ne peut pas assigner les unit\u00e9s \u00ab\u202f{1}\u202f\u00bb \u00e0 la dimension \u00ab\u202f{0}\u202f\u00bb.
+CanNotAssignUnitToVariable_2      = Ne peut pas assigner les unit\u00e9s \u00ab\u202f{1}\u202f\u00bb \u00e0 la variable \u00ab\u202f{0}\u202f\u00bb.
 CanNotConnectTo_1                 = Ne peut pas se connecter \u00e0 \u00ab\u202f{0}\u202f\u00bb.
 CanNotConvertFromType_2           = Ne peut pas convertir du type \u2018{0}\u2019 vers le type \u2018{1}\u2019.
 CanNotConvertValue_2              = La valeur \u00ab\u202f{0}\u202f\u00bb ne peut pas \u00eatre convertie vers le type \u2018{1}\u2019.
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 278ed2d..800f153 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,7 +61,7 @@ public final class Resources extends IndexedResourceBundle {
         }
 
         /**
-         * Can not compute the grid geometry of “{0}” TIFF file.
+         * Can not compute the grid geometry of “{0}” GeoTIFF file.
          */
         public static final short CanNotComputeGridGeometry_1 = 26;
 
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 219bc40..b88f5aa 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,7 +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.
+CanNotComputeGridGeometry_1       = Can not compute the grid geometry of \u201c{0}\u201d GeoTIFF 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 b778c54..dd94cf9 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,7 +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.
+CanNotComputeGridGeometry_1       = Ne peut pas calculer la g\u00e9om\u00e9trie de la grille du fichier GeoTIFF \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 237fa2d..6a2061d 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
@@ -271,7 +271,7 @@ final class CRSBuilder {
      */
     CRSBuilder(final Reader reader) {
         this.reader = reader;
-        geoKeys = new HashMap<>();
+        geoKeys = new HashMap<>(32);
     }
 
     /**
@@ -292,6 +292,7 @@ final class CRSBuilder {
      * The factory is fetched when first needed.
      *
      * @return the EPSG factory (never {@code null}).
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-102">SIS-102</a>
      */
     private GeodeticAuthorityFactory epsgFactory() throws FactoryException {
         if (epsgFactory == null) {
@@ -305,6 +306,7 @@ final class CRSBuilder {
      * The factory is fetched when first needed.
      *
      * @return the object factory (never {@code null}).
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-102">SIS-102</a>
      */
     private GeodeticObjectFactory objectFactory() {
         if (objectFactory == null) {
@@ -318,6 +320,7 @@ final class CRSBuilder {
      * The factory is fetched when first needed.
      *
      * @return the operation factory (never {@code null}).
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-102">SIS-102</a>
      */
     private CoordinateOperationFactory operationFactory() {
         if (operationFactory == null) {
@@ -572,7 +575,7 @@ final class CRSBuilder {
      * @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.
      * @param  asciiParameters    the sequence of characters from which to build strings, or {@code null} if none.
-     * @return the coordinate reference system created from the given GeoTIFF keys.
+     * @return the coordinate reference system created from the given GeoTIFF keys, or {@code null} if undefined.
      *
      * @throws NoSuchElementException if a mandatory value is missing.
      * @throws NumberFormatException if a numeric value was stored as a string and can not be parsed.
@@ -746,10 +749,12 @@ final class CRSBuilder {
         }
         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);
+            if (vertical != null) {
+                if (crs == null) {
+                    missingValue(GeoKeys.GeographicType);
+                } else {
+                    crs = objectFactory().createCompoundCRS(Collections.singletonMap(IdentifiedObject.NAME_KEY, crs.getName()), crs, vertical);
+                }
             }
         }
         /*
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
index 498424d..24cff6a 100644
--- 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
@@ -21,16 +21,18 @@ 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.metadata.spatial.DimensionNameType;
 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.MathTransformFactory;
 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.system.DefaultFactories;
 import org.apache.sis.internal.geotiff.Resources;
 import org.apache.sis.internal.util.DoubleDouble;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -177,20 +179,20 @@ final class GridGeometryBuilder {
     ////////////////////////////////////////////////////////////////////////////////////////
 
     /**
-     * The grid geometry to be created by {@link #build(GridExtent)}.
+     * The grid geometry to be created by {@link #build(long, long)}.
      */
     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.
+     * This information is obtained as a side-effect of {@link #build(long, long)} 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.
+     * This information is obtained as a side-effect of {@link #build(long, long)} call.
      */
     private CellGeometry cellGeometry;
 
@@ -272,11 +274,13 @@ final class GridGeometryBuilder {
      * 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.
+     * @param  width   the image width in pixels.
+     * @param  height  the image height in pixels.
      * @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 {
+    @SuppressWarnings("fallthrough")
+    public GridGeometry build(final long width, final long height) throws FactoryException {
         CoordinateReferenceSystem crs = null;
         if (keyDirectory != null) {
             final CRSBuilder helper = new CRSBuilder(reader);
@@ -296,14 +300,31 @@ final class GridGeometryBuilder {
                 }
             }
         }
+        /*
+         * If the CRS is non-null, then it is either two- or three-dimensional.
+         * The 'affine' matrix may be for a greater number of dimensions, so it
+         * may need to be reduced.
+         */
+        int n = (crs != null) ? crs.getCoordinateSystem().getDimension() : 2;
+        final DimensionNameType[] axisTypes = new DimensionNameType[n];
+        final long[] high = new long[n];
+        switch (n) {
+            default: axisTypes[2] = DimensionNameType.VERTICAL; // Fallthrough everywhere.
+            case 2:  axisTypes[1] = DimensionNameType.ROW;      high[1] = height - 1;
+            case 1:  axisTypes[0] = DimensionNameType.COLUMN;   high[0] = width  - 1;
+            case 0:  break;
+        }
+        final GridExtent extent = new GridExtent(axisTypes, null, high, true);
         boolean pixelIsPoint = CellGeometry.POINT.equals(cellGeometry);
+        final MathTransformFactory factory = DefaultFactories.forBuildin(MathTransformFactory.class);
         try {
-            final MathTransform gridToCRS;
+            MathTransform gridToCRS;
             if (affine != null) {
-                gridToCRS = MathTransforms.linear(affine);
+                gridToCRS = factory.createAffineTransform(Matrices.resizeAffine(affine, ++n, n));
             } else {
-                gridToCRS = Localization.nonLinear(modelTiePoints);
                 pixelIsPoint = true;
+                gridToCRS = Localization.nonLinear(modelTiePoints);
+                gridToCRS = factory.createPassThroughTransform(0, gridToCRS, n - 2);
             }
             gridGeometry = new GridGeometry(extent, pixelIsPoint ? PixelInCell.CELL_CENTER : PixelInCell.CELL_CORNER, gridToCRS, crs);
         } catch (TransformException e) {
@@ -327,7 +348,7 @@ final class GridGeometryBuilder {
      *
      * <p><b>Pre-requite:</b></p>
      * <ul>
-     *   <li>{@link #build(GridExtent)} must have been invoked successfully before this method.</li>
+     *   <li>{@link #build(long, long)} 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>
      *
@@ -338,32 +359,26 @@ final class GridGeometryBuilder {
      * @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;
+        if (metadata.addSpatialRepresentation(description, gridGeometry, true)) {
+            /*
+             * 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);
         }
-        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 2fc880a..5b6bd49 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
@@ -1205,13 +1205,6 @@ final class ImageFileDirectory extends AbstractResource implements GridCoverageR
     }
 
     /**
-     * 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
@@ -1219,13 +1212,13 @@ final class ImageFileDirectory extends AbstractResource implements GridCoverageR
         if (referencing != null) {
             GridGeometry gridGeometry = referencing.gridGeometry;
             if (gridGeometry == null) try {
-                gridGeometry = referencing.build(extent());
+                gridGeometry = referencing.build(imageWidth, imageHeight);
             } catch (FactoryException e) {
                 throw new DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1, filename()), e);
             }
             return gridGeometry;
         } else {
-            return new GridGeometry(extent(), null);
+            return new GridGeometry(new GridExtent(imageWidth, imageHeight), 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 dcd1d79..988db1e 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
@@ -63,6 +63,7 @@ final class Localization {
 
     /**
      * Creates a new localization grid from the information found by {@link ImageFileDirectory}.
+     * Current implementation creates two-dimensional transforms only.
      *
      * @param  modelTiePoints  the tie points to use for computing {@code gridToCRS}.
      * @return the grid geometry created from above properties. Never null.
@@ -96,8 +97,8 @@ final class Localization {
                 sourceToGrid.transform(ordinates, 0, ordinates, 0, 1);
                 grid.setControlPoint(Math.toIntExact(Math.round(ordinates[0])),
                                      Math.toIntExact(Math.round(ordinates[1])),
-                                     modelTiePoints.doubleValue(i+3),
-                                     modelTiePoints.doubleValue(i+4));
+                                     modelTiePoints.doubleValue(i + (RECORD_LENGTH/2)),
+                                     modelTiePoints.doubleValue(i + (RECORD_LENGTH/2 + 1)));
             }
             grid.setDesiredPrecision(PRECISION);
             final MathTransform tr = grid.create(null);
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
index 8d1937d..4f0a4e2 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
@@ -50,6 +50,8 @@ import org.opengis.referencing.crs.VerticalCRS;
 
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.iso.citation.*;
@@ -211,13 +213,14 @@ final class MetadataReader extends MetadataBuilder {
     }
 
     /**
-     * Returns the localized error resource bundle for the locale given by
-     * {@link org.apache.sis.util.logging.WarningListeners#getLocale()}.
+     * Logs a warning using the localized error resource bundle for the locale given by
+     * {@link WarningListeners#getLocale()}.
      *
-     * @return the localized error resource bundle.
+     * @param  key  one of {@link Errors.Keys} values.
      */
-    private Errors errors() {
-        return Errors.getResources(decoder.listeners.getLocale());
+    private void warning(final short key, final Object p1, final Object p2, final Exception e) {
+        final WarningListeners<DataStore> listeners = decoder.listeners;
+        listeners.warning(Errors.getResources(listeners.getLocale()).getString(key, p1, p2), e);
     }
 
     /**
@@ -296,7 +299,7 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
     private <T extends Enum<T>> T forEnumName(final Class<T> enumType, final String name) {
         final T code = Types.forEnumName(enumType, name);
         if (code == null && name != null) {
-            decoder.listeners.warning(errors().getString(Errors.Keys.UnknownEnumValue_2, enumType, name), null);
+            warning(Errors.Keys.UnknownEnumValue_2, enumType, name, null);
         }
         return code;
     }
@@ -312,7 +315,7 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
              * CodeLists are not enums, but using the error message for enums is not completly wrong since
              * if we did not allowed CodeList to create new elements, then we are using it like an enum.
              */
-            decoder.listeners.warning(errors().getString(Errors.Keys.UnknownEnumValue_2, codeType, name), null);
+            warning(Errors.Keys.UnknownEnumValue_2, codeType, name, null);
         }
         return code;
     }
@@ -715,7 +718,8 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
             }
             final AttributeNames.Dimension attributeNames = axis.attributeNames;
             if (attributeNames != null) {
-                setAxisName(dim, attributeNames.DEFAULT_NAME_TYPE);
+                final DimensionNameType name = attributeNames.DEFAULT_NAME_TYPE;
+                setAxisName(dim, name);
                 final String res = stringValue(attributeNames.RESOLUTION);
                 if (res != null) try {
                     /*
@@ -724,13 +728,19 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
                      */
                     final int s = res.indexOf(' ');
                     final double value;
+                    Unit<?> units = null;
                     if (s < 0) {
                         value = numericValue(attributeNames.RESOLUTION);
                     } else {
-                        value = Double.parseDouble(res.substring(0, s));
-                        // TODO: parse units and build a Quantity object.
+                        value = Double.parseDouble(res.substring(0, s).trim());
+                        final String symbol = res.substring(s+1).trim();
+                        if (!symbol.isEmpty()) try {
+                            units = Units.valueOf(symbol);
+                        } catch (ParserException e) {
+                            warning(Errors.Keys.CanNotAssignUnitToDimension_2, name, units, e);
+                        }
                     }
-                    setAxisResolution(dim, value);
+                    setAxisResolution(dim, value, units);
                 } catch (NumberFormatException e) {
                     warning(e);
                 }
@@ -940,7 +950,7 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
         if (units != null) try {
             setSampleUnits(Units.valueOf(units));
         } catch (ParserException e) {
-            decoder.listeners.warning(errors().getString(Errors.Keys.CanNotAssignUnitToDimension_2, name, units), e);
+            warning(Errors.Keys.CanNotAssignUnitToVariable_2, name, units, e);
         }
         double scale  = Double.NaN;
         double offset = Double.NaN;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
index 12f9f1e..f61006b 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
@@ -19,6 +19,7 @@ package org.apache.sis.internal.storage;
 import java.time.LocalDate;
 import java.util.Date;
 import java.util.Locale;
+import java.util.Optional;
 import java.util.Iterator;
 import java.util.Collection;
 import java.util.Collections;
@@ -28,6 +29,7 @@ import java.util.Map;
 import java.net.URI;
 import java.nio.charset.Charset;
 import javax.measure.Unit;
+import javax.measure.quantity.Length;
 import org.opengis.util.MemberName;
 import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
@@ -58,7 +60,9 @@ import org.opengis.metadata.distribution.Format;
 import org.opengis.metadata.quality.Element;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.ReferenceSystem;
+import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.crs.VerticalCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.metadata.iso.DefaultMetadata;
@@ -108,10 +112,13 @@ import org.apache.sis.metadata.iso.lineage.DefaultProcessStep;
 import org.apache.sis.metadata.iso.lineage.DefaultProcessing;
 import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.metadata.sql.MetadataSource;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.iso.Names;
 import org.apache.sis.util.iso.Types;
+import org.apache.sis.measure.Units;
 
 import static java.util.Collections.singleton;
 import static org.apache.sis.internal.util.StandardDateFormat.MILLISECONDS_PER_DAY;
@@ -129,7 +136,7 @@ import org.opengis.metadata.citation.Responsibility;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rémi Maréchal (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -1865,6 +1872,73 @@ parse:      for (int i = 0; i < length;) {
     }
 
     /**
+     * Adds and populates a "spatial representation info" node using the given grid geometry.
+     * If this method invokes implicitly {@link #newGridRepresentation(GridType)}, unless this
+     * method returns {@code false} in which case nothing has been done.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/spatialRepresentationInfo/transformationDimensionDescription}</li>
+     *   <li>{@code metadata/spatialRepresentationInfo/transformationParameterAvailability}</li>
+     *   <li>{@code metadata/spatialRepresentationInfo/axisDimensionProperties/dimensionName}</li>
+     *   <li>{@code metadata/spatialRepresentationInfo/axisDimensionProperties/dimensionSize}</li>
+     *   <li>{@code metadata/spatialRepresentationInfo/axisDimensionProperties/resolution}</li>
+     *   <li>{@code metadata/identificationInfo/spatialRepresentationType}</li>
+     *   <li>{@code metadata/referenceSystemInfo}</li>
+     * </ul>
+     *
+     * @param  description    a general description of the "grid to CRS" transformation, or {@code null} if none.
+     * @param  grid           the grid extent, "grid to CRS" transform and target CRS, or {@code null} if none.
+     * @param  addResolution  whether to declare the resolutions. Callers should set this argument to {@code false} if they intend
+     *                        to provide the resolution themselves, or if grid axes are not in the same order than CRS axes.
+     * @return whether a "spatial representation info" node has been added.
+     */
+    public final boolean addSpatialRepresentation(final String description, final GridGeometry grid, final boolean addResolution) {
+        final GridType type;
+        if (grid == null) {
+            if (description == null) {
+                return false;
+            }
+            type = GridType.UNSPECIFIED;
+        } else {
+            type = grid.isConversionLinear(0, 1) ? GridType.GEORECTIFIED : GridType.GEOREFERENCEABLE;
+        }
+        addSpatialRepresentation(SpatialRepresentationType.GRID);
+        newGridRepresentation(type);
+        setGridToCRS(description);
+        if (grid != null) {
+            setGeoreferencingAvailability(grid.isDefined(GridGeometry.GRID_TO_CRS), false, false);
+            CoordinateSystem cs = null;
+            if (grid.isDefined(GridGeometry.CRS)) {
+                final CoordinateReferenceSystem crs = grid.getCoordinateReferenceSystem();
+                cs = crs.getCoordinateSystem();
+                addReferenceSystem(crs);
+            }
+            if (grid.isDefined(GridGeometry.EXTENT)) {
+                final GridExtent extent = grid.getExtent();
+                final int dimension = extent.getDimension();
+                for (int i=0; i<dimension; i++) {
+                    final Optional<DimensionNameType> axisType = extent.getAxisType(i);
+                    if (axisType.isPresent()) {
+                        setAxisName(i, axisType.get());
+                    }
+                    final long size = extent.getSize(i);
+                    if (size >= 0 && size <= Integer.MAX_VALUE) {
+                        setAxisLength(i, (int) size);
+                    }
+                }
+            }
+            if (addResolution && grid.isDefined(GridGeometry.RESOLUTION)) {
+                final double[] resolution = grid.getResolution(false);
+                for (int i=0; i<resolution.length; i++) {
+                    setAxisResolution(i, resolution[i], (cs != null) ? cs.getAxis(i).getUnit() : null);
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
      * Adds a linear resolution in metres.
      * Storage location is:
      *
@@ -2006,7 +2080,7 @@ parse:      for (int i = 0; i < length;) {
     }
 
     /**
-     * Sets a general description of the transformation form grid coordinates to "real world" coordinates.
+     * Sets a general description of the transformation from grid coordinates to "real world" coordinates.
      * Storage location is:
      *
      * <ul>
@@ -2071,6 +2145,7 @@ parse:      for (int i = 0; i < length;) {
 
     /**
      * Sets the degree of detail in the given dimension.
+     * This method does nothing if the given resolution if NaN or infinite.
      * Storage location is:
      *
      * <ul>
@@ -2079,9 +2154,18 @@ parse:      for (int i = 0; i < length;) {
      *
      * @param  dimension   the axis dimension.
      * @param  resolution  the degree of detail in the grid dataset, or NaN for no-operation.
+     * @param  unit        the resolution unit, of {@code null} if unknown.
      */
-    public final void setAxisResolution(final int dimension, final double resolution) {
-        if (!Double.isNaN(resolution)) {
+    public final void setAxisResolution(final int dimension, double resolution, final Unit<?> unit) {
+        if (Double.isFinite(resolution)) {
+            /*
+             * Value should be a Quantity<?>. Since GeoAPI does not yet allow that,
+             * we convert to metres for now. Future version should store the value
+             * as-is with its unit of measurement (TODO).
+             */
+            if (Units.isLinear(unit)) {
+                resolution = unit.asType(Length.class).getConverterTo(Units.METRE).convert(resolution);
+            }
             axis(dimension).setResolution(shared(resolution));
         }
     }


Mime
View raw message