sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: `FeatureSet` use `CRSBuilder` for constructing an horizontal and temporal CRS from the variables. The temporal CRS allows construction of the "datetimes" characteristics on the "trajectory" property.
Date Tue, 27 Oct 2020 14:45:49 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 bc9df3941fdfd324c401ed09bec7687b1f47b246
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Oct 27 13:12:27 2020 +0100

    `FeatureSet` use `CRSBuilder` for constructing an horizontal and temporal CRS from the variables.
    The temporal CRS allows construction of the "datetimes" characteristics on the "trajectory" property.
---
 .../sis/internal/feature/MovingFeatures.java       |  76 ++++++--
 .../java/org/apache/sis/internal/netcdf/Axis.java  | 193 +++++++++++++--------
 .../org/apache/sis/internal/netcdf/CRSBuilder.java | 129 ++++++++++----
 .../org/apache/sis/internal/netcdf/Decoder.java    |   2 +-
 .../org/apache/sis/internal/netcdf/FeatureSet.java | 114 ++++++++----
 .../java/org/apache/sis/internal/netcdf/Grid.java  |  59 +++----
 .../org/apache/sis/internal/netcdf/Resources.java  |   4 +-
 .../sis/internal/netcdf/Resources.properties       |   2 +-
 .../sis/internal/netcdf/Resources_fr.properties    |   2 +-
 .../apache/sis/internal/netcdf/impl/GridInfo.java  |   2 +-
 .../sis/internal/netcdf/ucar/GridWrapper.java      |   2 +-
 .../apache/sis/storage/netcdf/MetadataReader.java  |   5 +-
 .../apache/sis/internal/netcdf/FeatureSetTest.java |  36 +++-
 .../org/apache/sis/internal/netcdf/GridTest.java   |  24 +--
 .../internal/storage/csv/MovingFeatureBuilder.java |   4 +-
 .../org/apache/sis/internal/storage/csv/Store.java |   2 +-
 16 files changed, 451 insertions(+), 205 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/MovingFeatures.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/MovingFeatures.java
index 9e4647f..603ba0e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/MovingFeatures.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/MovingFeatures.java
@@ -18,11 +18,14 @@ package org.apache.sis.internal.feature;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.Collections;
 import java.time.Instant;
 import org.opengis.util.LocalName;
 import org.apache.sis.util.iso.Names;
 import org.apache.sis.math.Vector;
 import org.apache.sis.feature.DefaultAttributeType;
+import org.apache.sis.referencing.crs.DefaultTemporalCRS;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
 
 // Branch-dependent imports
 import org.opengis.feature.Attribute;
@@ -42,20 +45,40 @@ import org.opengis.feature.AttributeType;
  */
 public class MovingFeatures {
     /**
-     * Definition of characteristics containing a list of time instants in chronological order, without duplicates.
+     * Definition of characteristics containing a list of instants, without duplicates.
+     * Should be in chronological order, but this is not verified.
      */
-    public static final AttributeType<Instant> TIME;
+    public static final AttributeType<Instant> TIME_AS_INSTANTS;
+
+    /**
+     * An alternative to {@link #TIME_AS_INSTANTS} used when times can not be mapped to calendar dates.
+     * This characteristic uses the same name than {@code TIME_AS_INSTANTS}. Consequently at most one
+     * of {@code TIME_AS_INSTANTS} and {@code TIME_AS_NUMBERS} can be used on the same property.
+     */
+    private static final AttributeType<Number> TIME_AS_NUMBERS;
     static {
         final LocalName scope = Names.createLocalName("OGC", null, "MF");
-        final Map<String,Object> properties = new HashMap<>(4);
-        properties.put(DefaultAttributeType.NAME_KEY, Names.createScopedName(scope, null, "datetimes"));
-        TIME = new DefaultAttributeType<>(properties, Instant.class, 0, Integer.MAX_VALUE, null);
+        final Map<String,Object> properties = Collections.singletonMap(
+                DefaultAttributeType.NAME_KEY, Names.createScopedName(scope, null, "datetimes"));
+        TIME_AS_INSTANTS = new DefaultAttributeType<>(properties, Instant.class, 0, Integer.MAX_VALUE, null);
+        TIME_AS_NUMBERS  = new DefaultAttributeType<>(properties,  Number.class, 0, Integer.MAX_VALUE, null);
+    }
+
+    /**
+     * Returns the "datetimes" characteristic to add on an attribute type.
+     * The characteristic will expect either {@link Instant} or {@link Number} values,
+     * depending on whether a temporal CRS is available or not.
+     *
+     * @param  hasCRS  whether a temporal CRS is available.
+     * @return the "datetimes" characteristic.
+     */
+    public static AttributeType<?> characteristic(final boolean hasCRS) {
+        return hasCRS ? TIME_AS_INSTANTS : TIME_AS_NUMBERS;
     }
 
     /**
-     * Caches of list of instants, used for sharing existing instances.
-     * We do this sharing because it is common to have many properties
-     * having the same time characteristics.
+     * Caches of list of times or instants, used for sharing existing instances.
+     * We do this sharing because it is common to have many properties having the same time characteristics.
      */
     private final Map<Vector,InstantList> cache;
 
@@ -69,14 +92,45 @@ public class MovingFeatures {
     }
 
     /**
-     * Set the time characteristic on the given attribute.
+     * Sets the "datetimes" characteristic on the given attribute as a list of {@link Instant} instances.
+     * Should be in chronological order, but this is not verified.
      *
      * @param  dest    the attribute on which to set time characteristic.
      * @param  millis  times in milliseconds since the epoch.
      */
-    public final void setTime(final Attribute<?> dest, final long[] millis) {
-        final Attribute<Instant> c = TIME.newInstance();
+    public final void setInstants(final Attribute<?> dest, final long[] millis) {
+        final Attribute<Instant> c = TIME_AS_INSTANTS.newInstance();
         c.setValues(cache.computeIfAbsent(InstantList.vectorize(millis), InstantList::new));
         dest.characteristics().values().add(c);
     }
+
+    /**
+     * Sets the "datetimes" characteristic on the given attribute.
+     * If the {@code converter} is non-null, it will be used for converting values to {@link Instant} instances.
+     * Otherwise values are stored as-is as time elapsed in arbitrary units since an arbitrary epoch.
+     *
+     * <p>Values should be in chronological order, but this is not verified.
+     * Current implementation does not cache the values, but this policy may be revisited in a future version.</p>
+     *
+     * @param  dest       the attribute on which to set time characteristic.
+     * @param  values     times in arbitrary units since an arbitrary epoch.
+     * @param  converter  the CRS to use for converting values to {@link Instant} instances, or {@code null}.
+     */
+    public static void setTimes(final Attribute<?> dest, final Vector values, final DefaultTemporalCRS converter) {
+        final Attribute<?> ct;
+        if (converter != null) {
+            final Instant[] instants = new Instant[values.size()];
+            for (int i=0; i<instants.length; i++) {
+                instants[i] = converter.toInstant(values.doubleValue(i));
+            }
+            final Attribute<Instant> c = TIME_AS_INSTANTS.newInstance();
+            c.setValues(UnmodifiableArrayList.wrap(instants));
+            ct = c;
+        } else {
+            final Attribute<Number> c = TIME_AS_NUMBERS.newInstance();
+            c.setValues(values);
+            ct = c;
+        }
+        dest.characteristics().values().add(ct);
+    }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
index 1822c2e..281754d 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
@@ -22,6 +22,7 @@ import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Arrays;
+import java.util.OptionalLong;
 import java.io.IOException;
 import javax.measure.Unit;
 import javax.measure.UnitConverter;
@@ -57,7 +58,7 @@ import ucar.nc2.constants.CF;
 
 /**
  * Information about a coordinate system axes. In netCDF files, all axes can be related to 1 or more dimensions
- * of the grid domain. Those grid domain dimensions are specified by the {@link #sourceDimensions} array.
+ * of the grid domain. Those grid domain dimensions are specified by the {@link #gridDimensionIndices} array.
  * Whether the array length is 1 or 2 depends on whether the wrapped netCDF axis is an instance of
  * {@link ucar.nc2.dataset.CoordinateAxis1D} or {@link ucar.nc2.dataset.CoordinateAxis2D} respectively.
  *
@@ -110,18 +111,27 @@ public final class Axis extends NamedElement {
      * for ISO 19115 {@code metadata/spatialRepresentationInfo/axisDimensionProperties/dimensionSize}
      * metadata property.
      *
-     * <p>A given {@link Grid} should not have two {@code Axis} instances with equal {@code sourceDimensions} array.
-     * When {@code sourceDimensions.length} ≧ 2 we may have two {@code Axis} instances with the same indices in their
-     * {@code sourceDimensions} arrays, but those indices should be in different order.</p>
+     * <p>A given {@link Grid} should not have two {@code Axis} instances with equal {@code gridDimensionIndices}.
+     * When {@code gridDimensionIndices.length} ≧ 2 we may have two {@code Axis} instances with the same indices
+     * in their {@code gridDimensionIndices} arrays, but those indices should be in different order.</p>
      *
+     * <p>The array length should be equal to {@link Variable#getNumDimensions()}. However this {@code Axis} class
+     * is tolerant to situations where the array length is shorter, which may happen if some grid dimensions where
+     * not recognized or can not be handled for whatever reason that {@link Grid} decided.</p>
+     *
+     * <p>This field is {@code null} if this {@code Axis} instance is not built for a {@link Grid}.
+     * In particular, this field has no meaning for CRS of geometries in a {@link FeatureSet}.
+     * See {@link #Axis(Variable)} for a list of methods than can not be used in such case.</p>
+     *
+     * @suu #getNumDimensions()
      * @see #getMainDirection()
      */
-    final int[] sourceDimensions;
+    final int[] gridDimensionIndices;
 
     /**
      * The number of cell elements along the source grid dimensions, as unsigned integers. The length of this
-     * array shall be equal to the {@link #sourceDimensions} length. For each element, {@code sourceSizes[i]}
-     * shall be equal to the number of grid cells in the grid dimension at index {@code sourceDimensions[i]}.
+     * array shall be equal to the {@link #gridDimensionIndices} length. For each element, {@code gridSizes[i]}
+     * shall be equal to the number of grid cells in the grid dimension at index {@code gridDimensionIndices[i]}.
      *
      * <p>This array should contain the same information as {@code coordinates.getShape()} but potentially in
      * a different order and with potentially one element (not necessarily the first one) set to a lower value
@@ -130,9 +140,14 @@ public final class Axis extends NamedElement {
      * <p>Note that while we defined those values as unsigned for consistency with {@link Variable} dimensions,
      * not all operations in this {@code Axis} class support values greater than the signed integer range.</p>
      *
+     * <p>This array is {@code null} if {@link #gridDimensionIndices} is {@code null}, i.e. if this axis is not
+     * used for building a {@link Grid}. See {@link #Axis(Variable)} for a list of methods than can not be used.</p>
+     *
+     * @see #getMainSize()
+     * @see #getSizeProduct(int)
      * @see Variable#getGridDimensions()
      */
-    private final int[] sourceSizes;
+    private final int[] gridSizes;
 
     /**
      * Values of coordinates on this axis for given grid indices. This variables is often one-dimensional,
@@ -142,19 +157,41 @@ public final class Axis extends NamedElement {
     final Variable coordinates;
 
     /**
+     * Creates an axis for a {@link FeatureSet}. This constructor leaves the {@link #gridDimensionIndices}
+     * and {@link #gridSizes} array to {@code null}, which forbid the use of following methods:
+     *
+     * <ul>
+     *   <li>{@link #mainDimensionFirst(Axis[], int)}</li>
+     *   <li>{@link #trySetTransform(Matrix, int, int, List)}</li>
+     *   <li>{@link #createLocalizationGrid(Axis)}</li>
+     *   <li>{@link #getSizeProduct(int)} (private method)</li>
+     * </ul>
+     *
+     * All above methods should be used by {@link Grid} only.
+     */
+    Axis(final Variable coordinates) {
+        this.coordinates = coordinates;
+        abbreviation = AxisType.abbreviation(coordinates);
+        final AxisDirection dir = direction(coordinates.getUnitsString());
+        direction = (dir != null) ? dir : AxisDirections.fromAbbreviation(abbreviation);
+        gridDimensionIndices = null;
+        gridSizes = null;
+    }
+
+    /**
      * Constructs a new axis associated to an arbitrary number of grid dimension. The given arrays are stored
      * as-in (not cloned) and their content may be modified after construction by {@link Grid#getAxes(Decoder)}.
      *
-     * @param  abbreviation      axis abbreviation, also identifying its type. This is a controlled vocabulary.
-     * @param  direction         direction of positive values ("up" or "down"), or {@code null} if unknown.
-     * @param  sourceDimensions  the index of the grid dimension associated to this axis, initially in netCDF order.
-     * @param  sourceSizes       the number of cell elements along that axis, as unsigned integers.
-     * @param  coordinates       coordinates of the localization grid used by this axis.
+     * @param  abbreviation          axis abbreviation, also identifying its type. This is a controlled vocabulary.
+     * @param  direction             direction of positive values ("up" or "down"), or {@code null} if unknown.
+     * @param  gridDimensionIndices  indices of grid dimension associated to this axis, initially in netCDF order.
+     * @param  gridSizes             number of cell elements along above grid dimensions, as unsigned integers.
+     * @param  coordinates           coordinates of the localization grid used by this axis.
      * @throws IOException if an I/O operation was necessary but failed.
      * @throws DataStoreException if a logical error occurred.
      * @throws ArithmeticException if the size of an axis exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
      */
-    public Axis(final char abbreviation, final String direction, final int[] sourceDimensions, final int[] sourceSizes,
+    public Axis(final char abbreviation, final String direction, final int[] gridDimensionIndices, final int[] gridSizes,
                 final Variable coordinates) throws IOException, DataStoreException
     {
         /*
@@ -172,7 +209,7 @@ public final class Axis extends NamedElement {
          */
         AxisDirection dir = Types.forCodeName(AxisDirection.class, direction, false);
         AxisDirection check = AxisDirections.fromAbbreviation(abbreviation);
-        final boolean isSigned = (dir != null);     // Whether 'dir' takes in account the direction of positive values.
+        final boolean isSigned = (dir != null);     // Whether `dir` takes in account the direction of positive values.
         boolean isConsistent = true;
         if (dir == null) {
             dir = check;
@@ -192,21 +229,21 @@ public final class Axis extends NamedElement {
                     Resources.Keys.AmbiguousAxisDirection_4, coordinates.getFilename(), coordinates.getName(), dir, check);
             if (isSigned) {
                 if (AxisDirections.isOpposite(dir)) {
-                    check = AxisDirections.opposite(check);         // Apply the sign of 'dir' on 'check'.
+                    check = AxisDirections.opposite(check);         // Apply the sign of `dir` on `check`.
                 }
                 dir = check;
             }
         }
-        this.direction        = dir;
-        this.abbreviation     = abbreviation;
-        this.sourceDimensions = sourceDimensions;
-        this.sourceSizes      = sourceSizes;
-        this.coordinates      = coordinates;
+        this.direction            = dir;
+        this.abbreviation         = abbreviation;
+        this.gridDimensionIndices = gridDimensionIndices;
+        this.gridSizes            = gridSizes;
+        this.coordinates          = coordinates;
         /*
          * If the variable for localization grid declares a fill value, maybe the last rows are all NaN.
          * We need to trim them from this axis, otherwise it will confuse the grid geometry calculation.
          * Following operation must be done before mainDimensionFirst(…) is invoked, otherwise the order
-         * of elements in 'sourceSizes' would not be okay anymore.
+         * of elements in `gridSizes` would not be okay anymore.
          */
         if (coordinates.getAttributeType(CDM.FILL_VALUE) != null) {
             final int page = getSizeProduct(1);            // Must exclude first dimension from computation.
@@ -214,8 +251,8 @@ public final class Axis extends NamedElement {
             int n = data.size();
             while (--n >= 0 && data.isNaN(n)) {}
             final int nr = Numerics.ceilDiv(++n, page);
-            assert nr <= sourceSizes[0] : nr;
-            sourceSizes[0] = nr;
+            assert nr <= gridSizes[0] : nr;
+            gridSizes[0] = nr;
             assert getSizeProduct(0) == n : n;
         }
     }
@@ -261,11 +298,11 @@ public final class Axis extends NamedElement {
      * @see #getMainDirection()
      */
     final void mainDimensionFirst(final Axis[] axes, final int count) throws IOException, DataStoreException {
-        final int d0 = sourceDimensions[0];
-        final int d1 = sourceDimensions[1];
+        final int d0 = gridDimensionIndices[0];
+        final int d1 = gridDimensionIndices[1];
         boolean s = false;
         for (int i=0; i<count; i++) {
-            final int[] other = axes[i].sourceDimensions;
+            final int[] other = axes[i].gridDimensionIndices;
             if (other.length != 0) {
                 final int first = other[0];
                 if  (first == d1) return;           // Swapping would cause a collision.
@@ -288,8 +325,8 @@ public final class Axis extends NamedElement {
          *  (6)              (7)              (8)
          */
         if (!s) {
-            final int[] x = sampleIndices(sourceSizes[0]);
-            final int[] y = sampleIndices(sourceSizes[1]);
+            final int[] x = sampleIndices(gridSizes[0]);
+            final int[] y = sampleIndices(gridSizes[1]);
             double xInc = 0, yInc = 0;
             for (int c=x.length * y.length; --c >= 0;) {
                 final int i = x[c % y.length];
@@ -302,8 +339,8 @@ public final class Axis extends NamedElement {
                 return;
             }
         }
-        ArraysExt.swap(sourceSizes,      0, 1);
-        ArraysExt.swap(sourceDimensions, 0, 1);
+        ArraysExt.swap(gridSizes,            0, 1);
+        ArraysExt.swap(gridDimensionIndices, 0, 1);
     }
 
     /**
@@ -340,9 +377,11 @@ public final class Axis extends NamedElement {
      * value returned by this method is the index of the "main" dimension in this array of length 2.
      *
      * @return 0 or 1, depending on whether coordinates vary mostly on columns or on rows respectively.
+     *
+     * @see #getMainSize()
      */
     final int getMainDirection() {
-        return (sourceDimensions.length < 2 || sourceDimensions[0] <= sourceDimensions[1]) ? 0 : 1;
+        return (getNumDimensions() < 2 || gridDimensionIndices[0] <= gridDimensionIndices[1]) ? 0 : 1;
     }
 
     /**
@@ -352,49 +391,57 @@ public final class Axis extends NamedElement {
      *
      * @return number of dimension of the localization grid used by this axis.
      */
-    public final int getDimension() {
-        return sourceDimensions.length;
+    final int getNumDimensions() {
+        return (gridDimensionIndices != null) ? gridDimensionIndices.length : coordinates.getNumDimensions();
     }
 
     /**
-     * Returns the product of all {@link #sourceSizes} values starting at the given index.
+     * Returns the product of all {@link #gridSizes} values starting at the given index.
      * The product of all sizes given by {@code getSizeProduct(0)} shall be the length of
      * the vector returned by {@link #read()}.
      *
      * @param  i  index of the first size to include in the product.
-     * @return the product of all {@link #sourceSizes} values starting at the given index.
+     * @return the product of all {@link #gridSizes} values starting at the given index.
      * @throws ArithmeticException if the product can not be represented as a signed 32 bits integer.
      */
     private int getSizeProduct(int i) {
         int length = 1;
-        while (i < sourceSizes.length) {
+        while (i < gridSizes.length) {
             length = Math.multiplyExact(length, getSize(i++));
         }
         return length;
     }
 
     /**
-     * Returns the {@link #sourceSizes} value at the given index, making sure it is representable as a
+     * Returns the {@link #gridSizes} value at the given index, making sure it is representable as a
      * signed integer value. This method is invoked by operations not designed for unsigned integers.
      *
-     * @param  i  index of the desired dimension, in the same order than {@link #sourceDimensions}.
+     * @param  i  index of the desired dimension, in the same order than {@link #gridDimensionIndices}.
      * @throws ArithmeticException if the size can not be represented as a signed 32 bits integer.
      */
     private int getSize(final int i) {
-        final int n = sourceSizes[i];
+        final int n = gridSizes[i];
         if (n >= 0) return n;
         throw new ArithmeticException(coordinates.errors().getString(Errors.Keys.IntegerOverflow_1, Integer.SIZE));
     }
 
     /**
      * Returns the number of cells in the first dimension of the localization grid used by this axis.
-     * If the localization grid has more than one dimension ({@link #getDimension()} {@literal > 1}),
+     * If the localization grid has more than one dimension ({@link #getNumDimensions()} {@literal > 1}),
      * then all additional dimensions are ignored. The first dimension should be the main one.
      *
      * @return number of cells in the first (main) dimension of the localization grid.
      */
-    public final long getSize() {
-        return (sourceSizes.length != 0) ? Integer.toUnsignedLong(sourceSizes[0]) : 0;
+    public final OptionalLong getMainSize() {
+        final int m = getMainDirection();
+        if (gridSizes != null && gridSizes.length > m) {
+            return OptionalLong.of(Integer.toUnsignedLong(gridSizes[m]));
+        }
+        final List<Dimension> dimensions = coordinates.getGridDimensions();
+        if (dimensions.size() > m) {
+            return OptionalLong.of(dimensions.get(m).length());
+        }
+        return OptionalLong.empty();
     }
 
     /**
@@ -418,7 +465,8 @@ public final class Axis extends NamedElement {
 
     /**
      * Returns {@code true} if the given axis specifies the same direction and unit of measurement than this axis.
-     * This is used for testing if a predefined axis can be used instead than invoking {@link #toISO(CSFactory, int)}.
+     * This is used for testing if a predefined axis can be used instead than invoking
+     * {@link #toISO(CSFactory, int, boolean)}.
      */
     final boolean isSameUnitAndDirection(final CoordinateSystemAxis axis) {
         if (!axis.getDirection().equals(direction)) {
@@ -502,9 +550,11 @@ public final class Axis extends NamedElement {
      *
      * @param  factory  the factory to use for creating the coordinate system axis.
      * @param  order    0 if creating the first axis, 1 if creating the second axis, <i>etc</i>.
+     * @param  grid     {@code true} if building a CRS for a grid, or {@code false} for features.
      * @return the ISO axis.
      */
-    final CoordinateSystemAxis toISO(final CSFactory factory, final int order)
+    @SuppressWarnings("fallthrough")
+    final CoordinateSystemAxis toISO(final CSFactory factory, final int order, final boolean grid)
             throws DataStoreException, FactoryException, IOException
     {
         /*
@@ -558,15 +608,16 @@ public final class Axis extends NamedElement {
                 case 'E': case 'N': unit = Units.METRE;  break;     // Projected easting and northing.
                 case 't':           unit = Units.SECOND; break;     // Time.
                 case 'x': case 'y': {
-                    final Vector values = coordinates.read();
-                    final Number increment = values.increment(0);
-                    if (increment != null && increment.doubleValue() == 1) {
-                        // Do not test values.doubleValue(0) since different conventions exit (0-based, 1-based, etc).
-                        unit = Units.PIXEL;
-                    } else {
-                        unit = Units.UNITY;
+                    if (grid) {
+                        final Vector values = read();
+                        final Number increment = values.increment(0);
+                        if (increment != null && increment.doubleValue() == 1) {
+                            // Do not test values.doubleValue(0) since different conventions exit (0-based, 1-based, etc).
+                            unit = Units.PIXEL;
+                            break;
+                        }
                     }
-                    break;
+                    // Else fallthrough.
                 }
                 default: unit = Units.UNITY; break;
             }
@@ -616,7 +667,7 @@ public final class Axis extends NamedElement {
     final boolean trySetTransform(final Matrix gridToCRS, final int lastSrcDim, final int tgtDim,
             final List<MathTransform> nonLinears) throws IOException, DataStoreException
     {
-        switch (getDimension()) {
+        switch (getNumDimensions()) {
             /*
              * Defined as a matter of principle, but should never happen.
              */
@@ -626,7 +677,7 @@ public final class Axis extends NamedElement {
              */
             case 1: {
                 final Vector data = read();
-                final int srcDim = lastSrcDim - sourceDimensions[0];                // Convert from netCDF to "natural" order.
+                final int srcDim = lastSrcDim - gridDimensionIndices[0];    // Convert from netCDF to "natural" order.
                 if (coordinates.trySetTransform(gridToCRS, srcDim, tgtDim, data)) {
                     return true;
                 } else {
@@ -645,7 +696,7 @@ public final class Axis extends NamedElement {
              *    20 20 20 20                  10 12 15 20
              *
              * can be reduced to a one-dimensional {10 12 15 20} vector (orientation matter however).
-             * We detect those cases by the call to data.repetitions(sourceSizes). In above examples,
+             * We detect those cases by the call to data.repetitions(gridSizes). In above examples,
              * we would get {4} for the case illustrated on left side, and {1,4} for the right side.
              * The array length tells us if the variation is horizontal or vertical, and the product
              * of all numbers gives us the variation width. That width must match the grid width,
@@ -656,7 +707,7 @@ public final class Axis extends NamedElement {
              */
             case 2: {
                 Vector data = read();
-                final int[] repetitions = data.repetitions(sourceSizes);        // Detects repetitions as illustrated above.
+                final int[] repetitions = data.repetitions(gridSizes);      // Detects repetitions as illustrated above.
                 long repetitionLength = 1;
                 for (int r : repetitions) {
                     repetitionLength = Math.multiplyExact(repetitionLength, r);
@@ -665,7 +716,7 @@ public final class Axis extends NamedElement {
                 for (int i=0; i<=1; i++) {
                     final int width  = getSize(ri ^ i    );
                     final int height = getSize(ri ^ i ^ 1);
-                    if (repetitionLength % width == 0) {            // Repetition length shall be grid width (or a divisor).
+                    if (repetitionLength % width == 0) {        // Repetition length shall be grid width (or a divisor).
                         final int length, step;
                         if (repetitions.length >= 2) {
                             length = height;
@@ -710,28 +761,28 @@ public final class Axis extends NamedElement {
      * @throws DataStoreException if a logical error occurred.
      */
     final MathTransform createLocalizationGrid(final Axis other) throws IOException, FactoryException, DataStoreException {
-        if (getDimension() != 2 || other.getDimension() != 2) {
+        if (getNumDimensions() != 2 || other.getNumDimensions() != 2) {
             return null;
         }
-        final int xd =  this.sourceDimensions[0];
-        final int yd =  this.sourceDimensions[1];
-        final int xo = other.sourceDimensions[0];
-        final int yo = other.sourceDimensions[1];
+        final int xd =  this.gridDimensionIndices[0];
+        final int yd =  this.gridDimensionIndices[1];
+        final int xo = other.gridDimensionIndices[0];
+        final int yo = other.gridDimensionIndices[1];
         if ((xo != xd | yo != yd) & (xo != yd | yo != xd)) {
             return null;
         }
         /*
          * Found two axes for the same set of dimensions, which implies that they have the same
          * shape (width and height) unless the two axes ignored a different amount of NaN values.
-         * Negative width and height means that their actual values overflow the 'int' capacity,
+         * Negative width and height means that their actual values overflow the `int` capacity,
          * which we can not process here.
          */
         final int ri = (xd <= yd) ? 0 : 1;          // Take in account that mainDimensionFirst(…) may have reordered values.
         final int ro = (xo <= yo) ? 0 : 1;
         final int width  = getSize(ri ^ 1);         // Fastest varying is right-most dimension (when in netCDF order).
         final int height = getSize(ri    );         // Slowest varying is left-most dimension (when in netCDF order).
-        if (other.sourceSizes[ro ^ 1] != width ||
-            other.sourceSizes[ro    ] != height)
+        if (other.gridSizes[ro ^ 1] != width ||
+            other.gridSizes[ro    ] != height)
         {
             warning(null, Errors.Keys.MismatchedGridGeometry_2, getName(), other.getName());
             return null;
@@ -834,7 +885,9 @@ public final class Axis extends NamedElement {
         final TransferFunction tr = coordinates.getTransferFunction();
         if (TransferFunctionType.LINEAR.equals(tr.getType())) {
             Vector data = coordinates.read();
-            data = data.subList(0, getSizeProduct(0));                  // Trim trailing NaN values.
+            if (gridSizes != null) {
+                data = data.subList(0, getSizeProduct(0));              // Trim trailing NaN values.
+            }
             data = data.transform(tr.getScale(), tr.getOffset());       // Apply scale and offset attributes, if any.
             return data;
         } else {
@@ -853,8 +906,8 @@ public final class Axis extends NamedElement {
         if (other instanceof Axis) {
             final Axis that = (Axis) other;
             return that.abbreviation == abbreviation && that.direction == direction
-                    && Arrays.equals(that.sourceDimensions, sourceDimensions)
-                    && Arrays.equals(that.sourceSizes, sourceSizes)
+                    && Arrays.equals(that.gridDimensionIndices, gridDimensionIndices)
+                    && Arrays.equals(that.gridSizes, gridSizes)
                     && coordinates.equals(that.coordinates);
         }
         return false;
@@ -867,6 +920,6 @@ public final class Axis extends NamedElement {
      */
     @Override
     public int hashCode() {
-        return abbreviation + Arrays.hashCode(sourceDimensions) + Arrays.hashCode(sourceSizes);
+        return abbreviation + Arrays.hashCode(gridDimensionIndices) + Arrays.hashCode(gridSizes);
     }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
index 7fb9c1c..1329169 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
@@ -19,6 +19,7 @@ package org.apache.sis.internal.netcdf;
 import java.util.Map;
 import java.util.List;
 import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.StringJoiner;
 import java.util.function.Supplier;
@@ -26,6 +27,7 @@ import java.util.logging.Level;
 import java.io.IOException;
 import java.time.Instant;
 import javax.measure.Unit;
+import org.apache.sis.internal.referencing.EllipsoidalHeightCombiner;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.cs.*;
@@ -34,6 +36,7 @@ import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.CRSFactory;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.GeocentricCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.operation.CoordinateOperationFactory;
 import org.opengis.referencing.operation.OperationMethod;
@@ -52,6 +55,7 @@ import org.apache.sis.internal.util.TemporalUtilities;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.measure.Units;
 import org.apache.sis.math.Vector;
 
@@ -67,7 +71,7 @@ import org.apache.sis.math.Vector;
  * <ol>
  *   <li>Invoke {@link #dispatch(List, Axis)} for all axes in a grid.
  *       Builders for CRS components will added in the given list.</li>
- *   <li>Invoke {@link #build(Decoder)} on each builder prepared in above step.</li>
+ *   <li>Invoke {@link #build(Decoder, boolean)} on each builder prepared in above step.</li>
  *   <li>Assemble the CRS components created in above step in a {@code CompoundCRS}.</li>
  * </ol>
  *
@@ -163,6 +167,67 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
     }
 
     /**
+     * Infers a new CRS for a {@link Grid}.
+     *
+     * @param  decoder  the decoder of the netCDF from which the CRS are constructed.
+     * @param  grid     the grid for which the CRS are constructed.
+     * @return coordinate reference system from the given axes, or {@code null}.
+     */
+    public static CoordinateReferenceSystem assemble(final Decoder decoder, final Grid grid)
+            throws DataStoreException, FactoryException, IOException
+    {
+        final List<CRSBuilder<?,?>> builders = new ArrayList<>();
+        for (final Axis axis : grid.getAxes(decoder)) {
+            dispatch(builders, axis);
+        }
+        final SingleCRS[] components = new SingleCRS[builders.size()];
+        for (int i=0; i < components.length; i++) {
+            components[i] = builders.get(i).build(decoder, true);
+        }
+        switch (components.length) {
+            case 0: return null;
+            case 1: return components[0];
+        }
+        return new EllipsoidalHeightCombiner(decoder).createCompoundCRS(properties(grid.getName()), components);
+    }
+
+    /**
+     * Infers a new horizontal and vertical CRS for a {@link FeatureSet}.
+     * The CRS returned by this method does not include a temporal component.
+     * Instead the temporal component, if found, is stored in the {@code time} array.
+     * Note that the temporal component is not necessarily a {@link org.opengis.referencing.crs.TemporalCRS} instance;
+     * it can also be an {@link org.opengis.referencing.crs.EngineeringCRS} instance if the datum epoch is unknown.
+     *
+     * @param  decoder  the decoder of the netCDF from which the CRS are constructed.
+     * @param  axes     the axes to use for creating a CRS.
+     * @param  time     an array of length 1 where to store the temporal CRS.
+     * @return coordinate reference system from the given axes, or {@code null}.
+     */
+    static CoordinateReferenceSystem assemble(final Decoder decoder, final Iterable<Variable> axes, final SingleCRS[] time)
+            throws DataStoreException, FactoryException, IOException
+    {
+        final List<CRSBuilder<?,?>> builders = new ArrayList<>();
+        for (final Variable axis : axes) {
+            dispatch(builders, new Axis(axis));
+        }
+        final SingleCRS[] components = new SingleCRS[builders.size()];
+        int n = 0;
+        for (final CRSBuilder<?, ?> cb : builders) {
+            final SingleCRS c = cb.build(decoder, false);
+            if (cb instanceof Temporal) {
+                time[0] = c;
+            } else {
+                components[n++] = c;
+            }
+        }
+        switch (n) {
+            case 0: return null;
+            case 1: return components[0];
+        }
+        return new EllipsoidalHeightCombiner(decoder).createCompoundCRS(ArraysExt.resize(components, n));
+    }
+
+    /**
      * Dispatches the given axis to a {@code CRSBuilder} appropriate for the axis type. The axis type is determined
      * from {@link Axis#abbreviation}, taken as a controlled vocabulary. If no suitable {@code CRSBuilder} is found
      * in the given list, then a new one will be created and added to the list.
@@ -172,7 +237,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
      * @throws DataStoreContentException if the given axis can not be added in a builder.
      */
     @SuppressWarnings("fallthrough")
-    public static void dispatch(final List<CRSBuilder<?,?>> components, final Axis axis) throws DataStoreContentException {
+    private static void dispatch(final List<CRSBuilder<?,?>> components, final Axis axis) throws DataStoreContentException {
         final Class<? extends CRSBuilder<?,?>> addTo;
         final Supplier<CRSBuilder<?,?>> constructor;
         int alternative = -1;
@@ -191,7 +256,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
             default:                       addTo = Engineering.class; constructor = Engineering::new; break;
         }
         /*
-         * If a builder of 'addTo' class already exists, add the axis in the existing builder.
+         * If a builder of `addTo` class already exists, add the axis in the existing builder.
          * We should have at most one builder of each class. But if we nevertheless have more,
          * add to the most recently used builder. If there is no builder, create a new one.
          */
@@ -267,8 +332,11 @@ previous:   for (int i=components.size(); --i >= 0;) {
      * This method can be invoked after all axes have been dispatched.
      *
      * @param  decoder  the decoder of the netCDF from which the CRS are constructed.
+     * @param  grid     {@code true} if building a CRS for a grid, or {@code false} for features.
      */
-    public final SingleCRS build(final Decoder decoder) throws FactoryException, DataStoreException, IOException {
+    private SingleCRS build(final Decoder decoder, final boolean grid)
+            throws FactoryException, DataStoreException, IOException
+    {
         if (dimension < minDim || dimension > maxDim) {
             final Variable axis = getFirstAxis().coordinates;
             throw new DataStoreContentException(axis.resources().getString(Resources.Keys.UnexpectedAxisCount_4,
@@ -279,7 +347,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
          * set the datum, CS and CRS field values to those candidate. Those values do not need to be exact; they
          * will be overwritten later if they do not match the netCDF file content.
          */
-        datum = datumType.cast(decoder.datumCache[datumIndex]);         // Should be before 'setPredefinedComponents' call.
+        datum = datumType.cast(decoder.datumCache[datumIndex]);         // Should be before `setPredefinedComponents` call.
         setPredefinedComponents(decoder);
         /*
          * If `setPredefinedComponents(decoder)` offers a datum, we will used it as-is. Otherwise create the datum now.
@@ -287,7 +355,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
          * EPSG::6019 — "Not specified (based on GRS 1980 ellipsoid)". If not, we build a similar name.
          */
         if (datum == null) {
-            // Not localized because stored as a String, possibly exported in WKT or GML, and 'datumBase' is in English.
+            // Not localized because stored as a String, possibly exported in WKT or GML, and `datumBase` is in English.
             createDatum(decoder.getDatumFactory(), properties("Unknown datum presumably based upon ".concat(datumBase)));
         }
         decoder.datumCache[datumIndex] = datum;
@@ -307,7 +375,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
             }
         }
         /*
-         * If 'setPredefinedComponents(decoder)' did not proposed a coordinate system, or if it proposed a CS
+         * If `setPredefinedComponents(decoder)` did not proposed a coordinate system, or if it proposed a CS
          * but its axes do not match the axes in the netCDF file, then create a new coordinate system here.
          */
         if (referenceSystem == null) {
@@ -320,7 +388,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
                 for (int i=0; i<iso.length; i++) {
                     final Axis axis = axes[i];
                     joiner.add(axis.getName());
-                    iso[i] = axis.toISO(csFactory, i);
+                    iso[i] = axis.toISO(csFactory, i, grid);
                 }
                 createCS(csFactory, properties(joiner.toString()), iso);
                 properties = properties(coordinateSystem.getName());
@@ -330,22 +398,25 @@ previous:   for (int i=components.size(); --i >= 0;) {
             createCRS(decoder.getCRSFactory(), properties);
         }
         /*
-         * Creates the coordinate reference system using current value of 'datum' and 'coordinateSystem' fields.
+         * Creates the coordinate reference system using current value of `datum` and `coordinateSystem` fields.
          * The coordinate system initially have a [-180 … +180]° longitude range. If the actual coordinate values
          * are outside that range, switch the longitude range to [0 … 360]°.
          */
-        final CoordinateSystem cs = referenceSystem.getCoordinateSystem();
-        for (int i=cs.getDimension(); --i >= 0;) {
-            final CoordinateSystemAxis axis = cs.getAxis(i);
-            if (RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) {
-                final Vector coordinates = axes[i].read();                          // Typically a cached vector.
-                final int length = coordinates.size();
-                if (length != 0) {
-                    final double first = coordinates.doubleValue(0);
-                    final double last  = coordinates.doubleValue(length - 1);
-                    if (Math.min(first, last) >= 0 && Math.max(first, last) > axis.getMaximumValue()) {
-                        referenceSystem = (SingleCRS) AbstractCRS.castOrCopy(referenceSystem).forConvention(AxesConvention.POSITIVE_RANGE);
-                        break;
+        if (grid) {
+            final CoordinateSystem cs = referenceSystem.getCoordinateSystem();
+            for (int i=cs.getDimension(); --i >= 0;) {
+                final CoordinateSystemAxis axis = cs.getAxis(i);
+                if (RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) {
+                    final Vector coordinates = axes[i].read();                          // Typically a cached vector.
+                    final int length = coordinates.size();
+                    if (length != 0) {
+                        final double first = coordinates.doubleValue(0);
+                        final double last  = coordinates.doubleValue(length - 1);
+                        if (Math.min(first, last) >= 0 && Math.max(first, last) > axis.getMaximumValue()) {
+                            referenceSystem = (SingleCRS) AbstractCRS.castOrCopy(referenceSystem)
+                                                .forConvention(AxesConvention.POSITIVE_RANGE);
+                            break;
+                        }
                     }
                 }
             }
@@ -473,7 +544,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
         }
 
         /**
-         * Initializes this builder before {@link #build(Decoder)} execution.
+         * Initializes this builder before {@link #build(Decoder, boolean)} execution.
          */
         @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
             defaultCRS = decoder.convention().defaultHorizontalCRS(false);
@@ -546,7 +617,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
 
         /**
          * Creates the three-dimensional {@link SphericalCS} from given axes. This method is invoked only
-         * if {@link #setPredefinedComponents(Decoder)} failed to assign a CS or if {@link #build(Decoder)}
+         * if {@link #setPredefinedComponents(Decoder)} failed to assign a CS or if {@link #build(Decoder, boolean)}
          * found that the {@link #coordinateSystem} does not have compatible axes.
          */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
@@ -611,8 +682,8 @@ previous:   for (int i=components.size(); --i >= 0;) {
 
         /**
          * Creates the two- or three-dimensional {@link EllipsoidalCS} from given axes. This method is invoked only if
-         * {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder)}
-         * found that the {@link #coordinateSystem} does not have compatible axes.
+         * {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder,
+         * boolean)} found that the {@link #coordinateSystem} does not have compatible axes.
          */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
             if (axes.length > 2) {
@@ -685,8 +756,8 @@ previous:   for (int i=components.size(); --i >= 0;) {
 
         /**
          * Creates the two- or three-dimensional {@link CartesianCS} from given axes. This method is invoked only if
-         * {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder)}
-         * found that the {@link #coordinateSystem} does not have compatible axes.
+         * {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if {@link #build(Decoder,
+         * boolean)} found that the {@link #coordinateSystem} does not have compatible axes.
          */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
             if (axes.length > 2) {
@@ -756,7 +827,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
         /**
          * Creates the one-dimensional {@link VerticalCS} from given axes. This method is invoked
          * only if {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system
-         * or if {@link #build(Decoder)} found that the axis or direction are not compatible.
+         * or if {@link #build(Decoder, boolean)} found that the axis or direction are not compatible.
          */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
             coordinateSystem = factory.createVerticalCS(properties, axes[0]);
@@ -827,7 +898,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
         /**
          * Creates the one-dimensional {@link TimeCS} from given axes. This method is invoked only
          * if {@link #setPredefinedComponents(Decoder)} failed to assign a coordinate system or if
-         * {@link #build(Decoder)} found that the axis or direction are not compatible.
+         * {@link #build(Decoder, boolean)} found that the axis or direction are not compatible.
          */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
             coordinateSystem = factory.createTimeCS(properties, axes[0]);
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
index 4d2b69a..669d1e4 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
@@ -105,7 +105,7 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
      * The geodetic datum, created when first needed. The datum are generally not specified in netCDF files.
      * To make that clearer, we will build datum with names like "Unknown datum presumably based on GRS 1980".
      *
-     * @see CRSBuilder#build(Decoder)
+     * @see CRSBuilder#build(Decoder, boolean)
      */
     final Datum[] datumCache;
 
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
index cdb3a00..c6cd189 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
@@ -31,7 +31,11 @@ import java.util.OptionalLong;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import javax.measure.Unit;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.crs.TemporalCRS;
 import org.opengis.metadata.acquisition.GeometryType;
+import org.apache.sis.referencing.crs.DefaultTemporalCRS;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.internal.feature.MovingFeatures;
 import org.apache.sis.internal.util.Strings;
@@ -47,6 +51,7 @@ import ucar.nc2.constants.CF;
 // Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
+import org.opengis.feature.Attribute;
 
 
 /**
@@ -149,6 +154,13 @@ final class FeatureSet extends DiscreteSampling {
     private final boolean hasTime;
 
     /**
+     * The temporal component of the coordinate reference system (CRS), or {@code null} if none.
+     * Note that this field may be {@code null} even if {@link #hasTime} is {@code true},
+     * if the CRS can not be expressed as a {@link TemporalCRS}.
+     */
+    private final DefaultTemporalCRS timeCRS;
+
+    /**
      * The type of all features to be read by this {@code FeatureSet}.
      */
     private final FeatureType type;
@@ -167,25 +179,26 @@ final class FeatureSet extends DiscreteSampling {
      * @param  counts                the count of instances per feature, or {@code null} if none.
      * @param  properties            variables providing a single value per feature instance (e.g. "mfIdRef").
      * @param  dynamicProperties     variables that contain time-varying properties other than coordinates.
-     * @param  referencingDimension  number of coordinate variables.
+     * @param  selectedAxes          variables storing the coordinates of all geometries (trajectories or points).
      * @param  isTrajectory          whether coordinates are stored in {@code properties} or {@code dynamicProperties}.
      * @param  hasTime               whether coordinates include a temporal variable.
      * @param  lock                  the lock to use in {@code synchronized(lock)} statements.
      * @throws IllegalArgumentException if the given library is non-null but not available.
      */
     private FeatureSet(final Decoder decoder, String name, final Vector counts, final Variable[] properties,
-                       final Variable[] dynamicProperties, final int referencingDimension, final boolean isTrajectory,
-                       final boolean hasTime, final Object lock)
+                       final Variable[] dynamicProperties, final Map<AxisType,Variable> selectedAxes,
+                       final boolean isTrajectory, final boolean hasTime, final Object lock)
+            throws DataStoreException, IOException
     {
         super(decoder.geomlib, decoder.listeners, lock);
         this.counts               = counts;
         this.properties           = properties;
         this.dynamicProperties    = dynamicProperties;
-        this.referencingDimension = referencingDimension;
+        this.referencingDimension = selectedAxes.size();
         this.hasTime              = hasTime;
         this.isTrajectory         = isTrajectory;
         /*
-         * Creates a description of the features to be read with following properties:
+         * We will create a description of the features to be read with following properties:
          *
          *    - Identifier and other properties having a single value per feature instance.
          *    - Trajectory as a geometric object, potentially with a time characteristic.
@@ -193,7 +206,9 @@ final class FeatureSet extends DiscreteSampling {
          */
         final FeatureTypeBuilder builder = new FeatureTypeBuilder(
                 decoder.nameFactory, decoder.geomlib, decoder.listeners.getLocale());
-
+        /*
+         * Identifier and other static properties (one value per feature instance).
+         */
         for (int i = getReferencingDimension(false); i < properties.length; i++) {
             final Variable v = properties[i];
             final Class<?> type;
@@ -202,24 +217,42 @@ final class FeatureSet extends DiscreteSampling {
             } else {
                 type = v.getDataType().getClass(v.getNumDimensions() > 1);
             }
-            describe(v, builder.addAttribute(type), false);
+            describe(v, builder.addAttribute(type));
         }
+        /*
+         * Geometry object as a single point or a trajectory, associated with:
+         *   - A Coordinate Reference System (CRS) characteristic.
+         *   - A "datetimes" characteristic if a time axis exists.
+         */
+        DefaultTemporalCRS timeCRS = null;
         if (referencingDimension != 0) {
             final AttributeTypeBuilder<?> geometry;
             geometry = builder.addAttribute(isTrajectory ? GeometryType.LINEAR : GeometryType.POINT);
             geometry.setName(TRAJECTORY).addRole(AttributeRole.DEFAULT_GEOMETRY);
+            try {
+                final SingleCRS[] time = new SingleCRS[1];
+                geometry.setCRS(CRSBuilder.assemble(decoder, selectedAxes.values(), time));
+                if (time[0] instanceof TemporalCRS) {
+                    timeCRS = DefaultTemporalCRS.castOrCopy((TemporalCRS) time[0]);
+                }
+            } catch (FactoryException ex) {
+                decoder.listeners.warning(decoder.resources().getString(Resources.Keys.CanNotCreateCRS_3,
+                                          decoder.getFilename(), name, ex.getLocalizedMessage()), ex);
+            }
             if (hasTime) {
-                geometry.addCharacteristic(MovingFeatures.TIME);
+                geometry.addCharacteristic(MovingFeatures.characteristic(timeCRS != null));
             }
         }
+        this.timeCRS = timeCRS;
+        /*
+         * Dynamic properties (many values by feature instances).
+         * Use `Number` type instead than a more specialized subclass because values
+         * will be stored in `Vector` objects and that class implements `List<Number>`.
+         */
         for (int i = getReferencingDimension(true); i < dynamicProperties.length; i++) {
-            /*
-             * Use `Number` type instead than a more specialized subclass because values
-             * will be stored in `Vector` objects and that class implements `List<Number>`.
-             */
             final Variable v = dynamicProperties[i];
             final Class<?> type = (v.isEnumeration() || v.isString()) ? String.class : Number.class;
-            describe(v, builder.addAttribute(type).setMaximumOccurs(Integer.MAX_VALUE), hasTime);
+            describe(v, builder.addAttribute(type).setMaximumOccurs(Integer.MAX_VALUE));
         }
         /*
          * By default, `name` is a netCDF dimension name (see method javadoc), usually all lower-cases.
@@ -236,9 +269,8 @@ final class FeatureSet extends DiscreteSampling {
      *
      * @param  variable   the variable from which to get metadata.
      * @param  attribute  the attribute to configure with variable metadata.
-     * @param  hasTime    whether to add a "time" characteristic on the attribute.
      */
-    private static void describe(final Variable variable, final AttributeTypeBuilder<?> attribute, final boolean hasTime) {
+    private static void describe(final Variable variable, final AttributeTypeBuilder<?> attribute) {
         final String name = variable.getName();
         attribute.setName(name);
         final String desc = variable.getDescription();
@@ -249,9 +281,6 @@ final class FeatureSet extends DiscreteSampling {
         if (unit != null) {
             attribute.setUnit(unit);
         }
-        if (hasTime) {
-            attribute.addCharacteristic(MovingFeatures.TIME);
-        }
         if (CF.TRAJECTORY_ID.equalsIgnoreCase(variable.getAttributeAsString(CF.CF_ROLE))) {
             attribute.addRole(AttributeRole.IDENTIFIER_COMPONENT);
         }
@@ -406,12 +435,17 @@ final class FeatureSet extends DiscreteSampling {
                 }
             }
         }
+        /*
+         * Choose whether coordinates are taken in static or dynamic properties. Current implementation does not
+         * support mixing both modes (e.g. X and Y coordinates as static properties and T as dynamic property).
+         * The variables are reordered for making sure that X, Y, Z, T are first and in that order.
+         */
         final Reorder r = new Reorder();
         features.add(new FeatureSet(decoder, featureName,
                      (counts != null) ? counts.read() : null,
                      r.toArray(properties, coordinates, false),
                      r.toArray(dynamicProperties, trajectory, true),
-                     r.referencingDimension, r.isTrajectory, r.hasTime, lock));     // Those arguments must be last.
+                     r.selectedAxes, r.isTrajectory, r.hasTime, lock));         // Those arguments must be last.
     }
 
     /**
@@ -470,10 +504,10 @@ final class FeatureSet extends DiscreteSampling {
      */
     private static final class Reorder {
         /**
-         * Number of variables storing the coordinates of all geometries (trajectories or points).
-         * This is the value to assign to {@link FeatureSet#referencingDimension}.
+         * Variables storing the coordinates of all geometries (trajectories or points).
+         * Those variables are taken either from static properties or from dynamic properties.
          */
-        int referencingDimension;
+        Map<AxisType,Variable> selectedAxes;
 
         /**
          * The kind of geometry described by coordinates.
@@ -488,21 +522,31 @@ final class FeatureSet extends DiscreteSampling {
         boolean hasTime;
 
         /**
+         * Creates an initially empty builder of variable arrays.
+         */
+        Reorder() {
+            selectedAxes = Collections.emptyMap();
+        }
+
+        /**
          * Returns the content of given property list as an array, potentially with coordinate variables first.
          *
          * @param  properties   the list to return as an array, not necessarily with elements in same order.
          * @param  coordinates  {@code properties} variables to consider as coordinate values.
          * @param  dynamic      value to assign to {@link #isTrajectory} if coordinate axes have been found.
          */
+        @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
         Variable[] toArray(final List<Variable> properties, final EnumMap<AxisType,Variable> coordinates, final boolean dynamic) {
             Variable[] array = new Variable[properties.size()];
-            if (referencingDimension == 0 && coordinates.containsKey(AxisType.X) && coordinates.containsKey(AxisType.Y)) {
-                isTrajectory = dynamic;
-                hasTime      = coordinates.containsKey(AxisType.T);
-                array        = coordinates.values().toArray(array);     // Put coordinates at array beginning.
-                int n        = referencingDimension = coordinates.size();
+            if (selectedAxes.isEmpty() && coordinates.containsKey(AxisType.X) && coordinates.containsKey(AxisType.Y)) {
+                isTrajectory  = dynamic;
+                selectedAxes  = coordinates;
+                hasTime       = coordinates.containsKey(AxisType.T);
+                array         = coordinates.values().toArray(array);     // Put coordinates at array beginning.
+                final int dim = coordinates.size();
+                int n = dim;
 skip:           for (final Variable v : properties) {
-                    for (int i=referencingDimension; --i >= 0;) {
+                    for (int i=dim; --i >= 0;) {
                         if (array[i] == v) continue skip;               // Skip already added coordinates.
                     }
                     array[n++] = v;                                     // Add property after coordinates.
@@ -736,7 +780,6 @@ skip:           for (final Variable v : properties) {
                     read(dynamicProperties, dynamicPropertyPosition, length, target);
                     for (int i=getReferencingDimension(true); i<n; i++) {
                         feature.setPropertyValue(dynamicProperties[i].getName(), target[i]);
-                        // TODO: set time characteristic.
                     }
                     if (isTrajectory) {
                         values = target;
@@ -749,9 +792,9 @@ skip:           for (final Variable v : properties) {
                  *
                  * The following `System.arraycopy(…)` call writes `List<?>` references into a `Vector[]` array,
                  * which seems unsafe. But it should not cause an ArrayStoreException because the elements that
-                 * we copy should be `Vector` instances, even if the remaining elements are not.
+                 * we copy should be `Vector` instances, even if the remaining `values` elements are not.
                  */
-                coordinateValues = new Vector[geometryDimension];
+                coordinateValues = new Vector[referencingDimension];
                 System.arraycopy(values, 0, coordinateValues, 0, coordinateValues.length);
             } catch (IOException e) {
                 throw new UncheckedIOException(canNotReadFile(), e);
@@ -812,6 +855,15 @@ makeGeom:   if (!isEmpty) {
                 }
                 feature.setPropertyValue(TRAJECTORY, geometry);
             }
+            /*
+             * Add time characteristic on the geometry. Actually this characteristic
+             * could be applied to all dynamic properties, but that would be redundancies.
+             * The time vector is the first vector after the geometry dimensions.
+             */
+            if (hasTime) {
+                MovingFeatures.setTimes((Attribute<?>) feature.getProperty(TRAJECTORY),
+                                        coordinateValues[geometryDimension], timeCRS);
+            }
             action.accept(feature);
             dynamicPropertyPosition += length;         // Check for ArithmeticException is already done by `extent(…)` call.
             return true;
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
index 8b65d06..432cef3 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
@@ -19,15 +19,12 @@ package org.apache.sis.internal.netcdf;
 import java.util.List;
 import java.util.Arrays;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.io.IOException;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
-import org.opengis.referencing.cs.CoordinateSystem;
-import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.metadata.spatial.DimensionNameType;
@@ -211,7 +208,7 @@ public abstract class Grid extends NamedElement {
             int i = 0, deferred = workspace.length;
             for (final Axis axis : axes) {
                 // Put one-dimensional axes first, all other axes last.
-                workspace[axis.getDimension() <= 1 ? i++ : --deferred] = axis;
+                workspace[axis.getNumDimensions() <= 1 ? i++ : --deferred] = axis;
             }
             deferred = workspace.length;        // Will become index of the first axis whose examination has been deferred.
             while (i < workspace.length) {      // Start the loop at the first n-dimensional axis (n > 1).
@@ -280,20 +277,7 @@ public abstract class Grid extends NamedElement {
     {
         if (!isCRSDetermined) try {
             isCRSDetermined = true;                             // Set now for avoiding new attempts if creation fail.
-            final List<CRSBuilder<?,?>> builders = new ArrayList<>();
-            for (final Axis axis : getAxes(decoder)) {
-                CRSBuilder.dispatch(builders, axis);
-            }
-            final SingleCRS[] components = new SingleCRS[builders.size()];
-            for (int i=0; i < components.length; i++) {
-                components[i] = builders.get(i).build(decoder);
-            }
-            switch (components.length) {
-                case 0:  break;                                 // Leave 'crs' to null.
-                case 1:  crs = components[0]; break;
-                default: crs = decoder.getCRSFactory().createCompoundCRS(
-                                        Collections.singletonMap(CoordinateSystem.NAME_KEY, getName()), components);
-            }
+            crs = CRSBuilder.assemble(decoder, this);
         } catch (FactoryException | NullArgumentException ex) {
             if (isNewWarning(ex, warnings)) {
                 canNotCreate(decoder, "getCoordinateReferenceSystem", Resources.Keys.CanNotCreateCRS_3, ex);
@@ -342,7 +326,7 @@ public abstract class Grid extends NamedElement {
             case 0:  break;
         }
         for (final Axis axis : axes) {
-            if (axis.getDimension() == 1) {
+            if (axis.getNumDimensions() == 1) {
                 final DimensionNameType name;
                 if (AxisDirections.isVertical(axis.direction)) {
                     name = DimensionNameType.VERTICAL;
@@ -351,7 +335,7 @@ public abstract class Grid extends NamedElement {
                 } else {
                     continue;
                 }
-                int dim = axis.sourceDimensions[0];
+                int dim = axis.gridDimensionIndices[0];
                 dim = names.length - 1 - dim;               // Convert netCDF order to "natural" order.
                 if (dim >= 0) names[dim] = name;
             }
@@ -394,7 +378,7 @@ public abstract class Grid extends NamedElement {
              * If we have not been able to set some coefficients in the matrix (because some transforms are non-linear),
              * set a single scale factor to 1 in the matrix row. The coefficient that we set to 1 is the one for the source
              * dimension which is not already taken by another row. If we have choice, we give preference to the dimension
-             * which seems most closely oriented toward axis direction (i.e. the first element in axis.sourceDimensions).
+             * which seems most closely oriented toward axis direction (i.e. the first element in axis.gridDimensionIndices).
              *
              * Example: if the `axes` array contains (longitude, latitude) in that order, and if the longitude axis said
              * that its preferred dimension is 1 (after conversion to "natural" order) while the latitude axis said that
@@ -406,21 +390,22 @@ public abstract class Grid extends NamedElement {
              *    │ 0  0  1 │
              *    └         ┘
              *
-             * The preferred grid dimensions are stored in the `sourceDimensions` array. In above example this is {1, 0}.
+             * The preferred grid dimensions are stored in the `gridDimensionIndices` array.
+             * In above example this is {1, 0}.
              */
-            final int[] sourceDimensions = new int[nonLinears.size()];
-            Arrays.fill(sourceDimensions, -1);
-            for (int i=0; i<sourceDimensions.length; i++) {
+            final int[] gridDimensionIndices = new int[nonLinears.size()];
+            Arrays.fill(gridDimensionIndices, -1);
+            for (int i=0; i<gridDimensionIndices.length; i++) {
                 final int tgtDim = deferred[i];
                 final Axis axis = axes[tgtDim];
-findFree:       for (int srcDim : axis.sourceDimensions) {                      // In preference order (will take only one).
+findFree:       for (int srcDim : axis.gridDimensionIndices) {                  // In preference order (will take only one).
                     srcDim = lastSrcDim - srcDim;                               // Convert netCDF order to "natural" order.
                     for (int j=affine.getNumRow(); --j>=0;) {
                         if (affine.getElement(j, srcDim) != 0) {
                             continue findFree;
                         }
                     }
-                    sourceDimensions[i] = srcDim;
+                    gridDimensionIndices[i] = srcDim;
                     affine.setElement(tgtDim, srcDim, 1);
                     break;
                 }
@@ -436,16 +421,16 @@ findFree:       for (int srcDim : axis.sourceDimensions) {
                         if (nonLinears.get(j) == null) {
                             /*
                              * Found a pair of axes.  Prepare an array of length 2, to be reordered later in the
-                             * axis order declared in `sourceDimensions`. This is not necessarily the same order
-                             * than iteration order because it depends on values of `axis.sourceDimensions[0]`.
+                             * axis order declared in `gridDimensionIndices`. This is not necessarily the same order
+                             * than iteration order because it depends on values of `axis.gridDimensionIndices[0]`.
                              * Those values take in account what is the "main" dimension of each axis.
                              */
                             final Axis[] gridAxes = new Axis[] {
                                 axes[deferred[i]],
                                 axes[deferred[j]]
                             };
-                            final int srcDim   = sourceDimensions[i];
-                            final int otherDim = sourceDimensions[j];
+                            final int srcDim   = gridDimensionIndices[i];
+                            final int otherDim = gridDimensionIndices[j];
                             switch (srcDim - otherDim) {
                                 case -1: break;
                                 case +1: ArraysExt.swap(gridAxes, 0, 1); break;
@@ -460,10 +445,10 @@ findFree:       for (int srcDim : axis.sourceDimensions) {
                                 nonLinears.set(i, grid);
                                 nonLinears.remove(j);
                                 final int n = nonLinears.size() - j;
-                                System.arraycopy(deferred,         j+1, deferred,         j, n);
-                                System.arraycopy(sourceDimensions, j+1, sourceDimensions, j, n);
+                                System.arraycopy(deferred,             j+1, deferred,             j, n);
+                                System.arraycopy(gridDimensionIndices, j+1, gridDimensionIndices, j, n);
                                 if (otherDim < srcDim) {
-                                    sourceDimensions[i] = otherDim;         // Index of the first dimension.
+                                    gridDimensionIndices[i] = otherDim;     // Index of the first dimension.
                                 }
                                 break;                                      // Continue the 'i' loop.
                             }
@@ -472,13 +457,13 @@ findFree:       for (int srcDim : axis.sourceDimensions) {
                 }
             }
             /*
-             * If at least one `sourceDimensions` is undefined, the variable is maybe not a grid.
+             * If at least one `gridDimensionIndices` is undefined, the variable is maybe not a grid.
              * It happens for example if the variable is a trajectory, in which case we have two
              * CRS dimensions (e.g. latitude and longitude) but only one variable dimension;
              * the first CRS dimension has been associated to that variable and the other CRS
              * dimension is orphan.
              */
-            for (final int s : sourceDimensions) {
+            for (final int s : gridDimensionIndices) {
                 if (s < 0) return null;
             }
             /*
@@ -493,7 +478,7 @@ findFree:       for (int srcDim : axis.sourceDimensions) {
                 MathTransform tr = nonLinears.get(i);
                 if (tr != null) {
                     if (i < nonLinearCount) {
-                        final int srcDim = sourceDimensions[i];
+                        final int srcDim = gridDimensionIndices[i];
                         tr = factory.createPassThroughTransform(srcDim, tr,
                                         (lastSrcDim + 1) - (srcDim + tr.getSourceDimensions()));
                     }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
index 9f9c4ee..55e9ba0 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
@@ -68,8 +68,8 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CanNotComputeVariablePosition_2 = 6;
 
         /**
-         * Can not create the Coordinate Reference System for grid geometry “{1}” in the “{0}” netCDF
-         * file. The reason is: {2}
+         * Can not create the Coordinate Reference System for “{1}” in the “{0}” netCDF file. The
+         * reason is: {2}
          */
         public static final short CanNotCreateCRS_3 = 11;
 
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
index e6a6457..df38cdb 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
@@ -21,7 +21,7 @@
 #
 AmbiguousAxisDirection_4          = NetCDF file \u201c{0}\u201d provides an ambiguous axis direction for variable \u201c{1}\u201d. It could be {2}\u00a0or {3}.
 CanNotComputeVariablePosition_2   = Can not compute data location for \u201c{1}\u201d variable in the \u201c{0}\u201d netCDF file.
-CanNotCreateCRS_3                 = Can not create the Coordinate Reference System for grid geometry \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2}
+CanNotCreateCRS_3                 = Can not create the Coordinate Reference System for \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2}
 CanNotCreateGridGeometry_3        = Can not create the grid geometry \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2}
 CanNotRelateVariableDimension_3   = Can not relate dimension \u201c{2}\u201d of variable \u201c{1}\u201d to a coordinate system dimension in netCDF file \u201c{0}\u201d.
 CanNotRender_2                    = Can not render an image for \u201c{0}\u201d. The reason is: {1}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
index 624d10e..ef58d0b 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
@@ -26,7 +26,7 @@
 #
 AmbiguousAxisDirection_4          = Le fichier netCDF \u00ab\u202f{0}\u202f\u00bb fournit une direction d\u2019axe ambigu\u00eb pour la variable \u00ab\u202f{1}\u202f\u00bb. Elle pourrait \u00eatre {2}\u00a0ou {3}.
 CanNotComputeVariablePosition_2   = Ne peut pas calculer la position des donn\u00e9es de la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
-CanNotCreateCRS_3                 = Ne peut pas cr\u00e9er le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es pour la g\u00e9om\u00e9trie de grille \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {2}
+CanNotCreateCRS_3                 = Ne peut pas cr\u00e9er le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es pour \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {2}
 CanNotCreateGridGeometry_3        = Ne peut pas cr\u00e9er la g\u00e9om\u00e9trie de grille \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {2}
 CanNotRelateVariableDimension_3   = Ne peut pas relier la dimension \u00ab\u202f{2}\u202f\u00bb de la variable \u00ab\u202f{1}\u202f\u00bb \u00e0 une dimension d\u2019un syst\u00e8me de coordonn\u00e9es du fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 CanNotRender_2                    = Ne peut pas produire une image pour \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {1}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java
index 76ca226..a6c3162 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java
@@ -212,7 +212,7 @@ next:       for (final String name : axisNames) {
                 for (int sourceDim = 0; sourceDim < domain.length; sourceDim++) {
                     if (domain[sourceDim] == dimension) {
                         indices[i] = sourceDim;
-                        sizes[i++] = dimension.length;
+                        sizes[i++] = dimension.length;              // Handled as unsigned intengers.
                         break;
                     }
                 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
index bde4422..0896332 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
@@ -277,7 +277,7 @@ next:       for (final String name : axisNames) {
                 /*
                  * If the axis dimension has not been found in the coordinate system (sourceDim < 0),
                  * then there is maybe a problem with the netCDF file. However for the purpose of this
-                 * package, we can proceed as if the dimension does not exist ('i' not incremented).
+                 * package, we can proceed as if the dimension does not exist (`i` not incremented).
                  */
             }
             axes[targetDim] = new Axis(abbreviation, axis.getPositive(),
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 d4cd862..7bd6ae3 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
@@ -702,9 +702,8 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
              * the 'sourceDimensions' and 'sourceSizes' arrays are for the grid dimension which is most closely
              * oriented toward the axis direction.
              */
-            if (axis.getDimension() >= 1) {
-                setAxisSize(i, axis.getSize());
-            }
+            final int d = i;    // Because lambda expressions want final variable.
+            axis.getMainSize().ifPresent((s) -> setAxisSize(d, s));
             final AttributeNames.Dimension attributeNames;
             switch (axis.abbreviation) {
                 case 'λ': case 'θ':           attributeNames = AttributeNames.LONGITUDE; break;
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/FeatureSetTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/FeatureSetTest.java
index 1a57fcf..5d7eefa 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/FeatureSetTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/FeatureSetTest.java
@@ -21,16 +21,21 @@ import java.awt.geom.PathIterator;
 import java.util.Iterator;
 import java.util.Collection;
 import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import org.opengis.referencing.crs.GeographicCRS;
+import org.apache.sis.internal.feature.AttributeConvention;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.test.DependsOn;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
+import static org.apache.sis.test.Assert.*;
 
 // Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
+import org.opengis.feature.Attribute;
 import org.opengis.feature.AttributeType;
 import org.opengis.test.dataset.TestData;
 
@@ -58,6 +63,18 @@ public strictfp class FeatureSetTest extends TestCase {
     private int featureIndex;
 
     /**
+     * Instant from which time are measured.
+     */
+    private final Instant timeOrigin;
+
+    /**
+     * Creates a new test case.
+     */
+    public FeatureSetTest() {
+        timeOrigin = Instant.parse("2014-11-29T00:00:00Z");
+    }
+
+    /**
      * Tests {@link FeatureSet} with a moving features file.
      *
      * @throws IOException if an I/O error occurred while opening the file.
@@ -105,6 +122,7 @@ public strictfp class FeatureSetTest extends TestCase {
     private void verifyInstance(final Feature instance) {
         assertSame(type, instance.getType());
         final float[] longitudes, latitudes;
+        final short[] times;                    // In minutes since 2014-11-29 00:00:00.
         final String[] stations;
         final String identifier;
         switch (featureIndex++) {
@@ -112,6 +130,7 @@ public strictfp class FeatureSetTest extends TestCase {
                 identifier = "a4078a16";
                 longitudes = new float[] {139.622715f, 139.696899f, 139.740440f, 139.759640f, 139.763328f, 139.766084f};
                 latitudes  = new float[] { 35.466188f,  35.531328f,  35.630152f,  35.665498f,  35.675069f,  35.681382f};
+                times      = new short[] {       1068,        1077,        1087,        1094,        1096,        1098};
                 stations   = new String[] {
                     "Yokohama", "Kawasaki", "Shinagawa", "Shinbashi", "Yurakucho", "Tokyo"
                 };
@@ -121,6 +140,7 @@ public strictfp class FeatureSetTest extends TestCase {
                 identifier = "1e146c16";
                 longitudes = new float[] {139.700258f, 139.730667f, 139.763786f, 139.774219f};
                 latitudes  = new float[] { 35.690921f,  35.686014f,  35.699855f,  35.698683f};
+                times      = new short[] {       1075,        1079,        1087,        1090};
                 stations   = new String[] {
                     "Shinjuku", "Yotsuya", "Ochanomizu", "Akihabara"
                 };
@@ -130,6 +150,7 @@ public strictfp class FeatureSetTest extends TestCase {
                 identifier = "f50ff004";
                 longitudes = new float[] {139.649867f, 139.665652f, 139.700258f};
                 latitudes  = new float[] { 35.705385f,  35.706032f,  35.690921f};
+                times      = new short[] {       3480,        3482,        3486};
                 stations   = new String[] {
                     "Koenji", "Nakano", "Shinjuku"
                 };
@@ -140,9 +161,20 @@ public strictfp class FeatureSetTest extends TestCase {
                 return;
             }
         }
+        // Convert the time vector to an array of instants.
+        final Instant[] instants = new Instant[times.length];
+        for (int i=0; i<times.length; i++) {
+            instants[i] = timeOrigin.plus(times[i], ChronoUnit.MINUTES);
+        }
+        /*
+         * Verify property values and characteristics.
+         */
         assertEquals("identifier", identifier, instance.getPropertyValue("features"));
-        asserLineStringEquals((Shape) instance.getPropertyValue("trajectory"), longitudes, latitudes);
+        final Attribute<?> trajectory = (Attribute<?>) instance.getProperty("trajectory");
+        asserLineStringEquals((Shape) trajectory.getValue(), longitudes, latitudes);
         assertArrayEquals("stations", stations, ((Collection<?>) instance.getPropertyValue("stations")).toArray());
+        assertArrayEquals("times", instants, trajectory.characteristics().get("datetimes").getValues().toArray());
+        assertInstanceOf("CRS", GeographicCRS.class, AttributeConvention.getCRSCharacteristic(trajectory));
     }
 
     /**
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
index f9781f6..ed35f9b 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
@@ -89,11 +89,11 @@ public strictfp class GridTest extends TestCase {
         assertEquals('λ', x.abbreviation);
         assertEquals('φ', y.abbreviation);
 
-        assertArrayEquals(new int[] {1}, x.sourceDimensions);
-        assertArrayEquals(new int[] {0}, y.sourceDimensions);
+        assertArrayEquals(new int[] {1}, x.gridDimensionIndices);
+        assertArrayEquals(new int[] {0}, y.gridDimensionIndices);
 
-        assertEquals(73, x.getSize());
-        assertEquals(73, y.getSize());
+        assertEquals(73, x.getMainSize().getAsLong());
+        assertEquals(73, y.getMainSize().getAsLong());
     }
 
     /**
@@ -117,15 +117,15 @@ public strictfp class GridTest extends TestCase {
         assertEquals('H', z.abbreviation);
         assertEquals('t', t.abbreviation);
 
-        assertArrayEquals(new int[] {3}, x.sourceDimensions);
-        assertArrayEquals(new int[] {2}, y.sourceDimensions);
-        assertArrayEquals(new int[] {1}, z.sourceDimensions);
-        assertArrayEquals(new int[] {0}, t.sourceDimensions);
+        assertArrayEquals(new int[] {3}, x.gridDimensionIndices);
+        assertArrayEquals(new int[] {2}, y.gridDimensionIndices);
+        assertArrayEquals(new int[] {1}, z.gridDimensionIndices);
+        assertArrayEquals(new int[] {0}, t.gridDimensionIndices);
 
-        assertEquals(38, x.getSize());
-        assertEquals(19, y.getSize());
-        assertEquals( 4, z.getSize());
-        assertEquals( 1, t.getSize());
+        assertEquals(38, x.getMainSize().getAsLong());
+        assertEquals(19, y.getMainSize().getAsLong());
+        assertEquals( 4, z.getMainSize().getAsLong());
+        assertEquals( 1, t.getMainSize().getAsLong());
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureBuilder.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureBuilder.java
index 50f9c66..9d19f6f 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureBuilder.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureBuilder.java
@@ -179,7 +179,7 @@ final class MovingFeatureBuilder extends MovingFeatures {
             throw new CorruptedObjectException();
         }
         dest.setValues(UnmodifiableArrayList.wrap(values));
-        setTime(dest, times);
+        setInstants(dest, times);
     }
 
     /**
@@ -276,7 +276,7 @@ final class MovingFeatureBuilder extends MovingFeatures {
          * Store the geometry and characteristics in the attribute.
          */
         dest.setValue(factory.createPolyline(false, dimension, vectors));
-        setTime(dest, times);
+        setInstants(dest, times);
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
index e50d129..9829e26 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
@@ -569,7 +569,7 @@ final class Store extends URIDataStore implements FeatureSet {
                                 type = double[].class;
                             } else {
                                 type = geometries.polylineClass;
-                                characteristics = new AttributeType[] {MovingFeatureBuilder.TIME};
+                                characteristics = new AttributeType[] {MovingFeatureBuilder.TIME_AS_INSTANTS};
                             }
                             minOccurrence = 1;
                             maxOccurrence = 1;


Mime
View raw message