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

[sis] branch geoapi-4.0 updated (1b47d2f -> bc9df39)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from 1b47d2f  Fix recent files ordering.
     new 22cef57  Report JavaFX version.
     new bc9df39  `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.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../sis/internal/feature/MovingFeatures.java       |  76 ++++++--
 .../src/main/java/org/apache/sis/setup/About.java  |  53 +++---
 .../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 +-
 17 files changed, 481 insertions(+), 228 deletions(-)


[sis] 01/02: Report JavaFX version.

Posted by de...@apache.org.
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 22cef572d574fbb4052043016363740211e2b1ad
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Oct 24 17:39:20 2020 +0200

    Report JavaFX version.
---
 .../src/main/java/org/apache/sis/setup/About.java  | 53 ++++++++++++----------
 1 file changed, 30 insertions(+), 23 deletions(-)

diff --git a/core/sis-utility/src/main/java/org/apache/sis/setup/About.java b/core/sis-utility/src/main/java/org/apache/sis/setup/About.java
index 4ec110c..7cdeca1 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/setup/About.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/setup/About.java
@@ -206,16 +206,16 @@ public enum About {
         root.setValue(NAME, resources.getString(Vocabulary.Keys.LocalConfiguration));
         table.setRoot(root);
         /*
-         * Begin with the "Versions" section. The 'newSection' variable will be updated in the
-         * switch statement when new section will begin, and reset to 'null' after the 'section'
+         * Begin with the "Versions" section. The `newSection` variable will be updated in the
+         * switch statement when new section will begin, and reset to `null` after the `section`
          * variable has been updated accordingly.
          */
         TreeTable.Node section = null;
         About newSection = VERSIONS;
 fill:   for (int i=0; ; i++) {
-            short    nameKey  = 0;          // The Vocabulary.Key for 'name', used only if name is null.
-            String   name     = null;       // The value to put in the 'Name' column of the table.
-            Object   value    = null;       // The value to put in the 'Value' column of the table.
+            short    nameKey  = 0;          // The Vocabulary.Key for `name`, used only if name is null.
+            String   name     = null;       // The value to put in the "Name" column of the table.
+            Object   value    = null;       // The value to put in the "Value" column of the table.
             String[] children = null;       // Optional children to write below the node.
             switch (i) {
                 case 0: {
@@ -234,27 +234,34 @@ fill:   for (int i=0; ; i++) {
                 }
                 case 2: {
                     if (sections.contains(VERSIONS)) {
+                        name  = "JavaFX";
+                        value = getProperty("javafx.version");
+                    }
+                    break;
+                }
+                case 3: {
+                    if (sections.contains(VERSIONS)) {
                         nameKey = Vocabulary.Keys.OperatingSystem;
                         value = concatenate(concatenate(getProperty("os.name"),
                                 getProperty("os.version"), false), getProperty("os.arch"), true);
                     }
                     break;
                 }
-                case 3: {
+                case 4: {
                     if (sections.contains(VERSIONS)) {
                         nameKey = Vocabulary.Keys.Container;
                         value = Shutdown.getContainer();        // Sometime contains version information.
                     }
                     break;
                 }
-                case 4: {
+                case 5: {
                     if (sections.contains(VERSIONS)) {
                         nameKey = Vocabulary.Keys.GeodeticDataset;
                         value = MetadataServices.getInstance().getInformation(Constants.EPSG, locale);
                     }
                     break;
                 }
-                case 5: {
+                case 6: {
                     newSection = LOCALIZATION;
                     if (sections.contains(LOCALIZATION)) {
                         final Locale current = Locale.getDefault();
@@ -269,7 +276,7 @@ fill:   for (int i=0; ; i++) {
                     }
                     break;
                 }
-                case 6: {
+                case 7: {
                     if (sections.contains(LOCALIZATION)) {
                         final TimeZone current = TimeZone.getDefault();
                         if (current != null) {
@@ -290,7 +297,7 @@ fill:   for (int i=0; ; i++) {
                     }
                     break;
                 }
-                case 7: {
+                case 8: {
                     if (sections.contains(LOCALIZATION)) {
                         nameKey = Vocabulary.Keys.CurrentDateTime;
                         final DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, formatLocale);
@@ -301,7 +308,7 @@ fill:   for (int i=0; ; i++) {
                     }
                     break;
                 }
-                case 8: {
+                case 9: {
                     if (sections.contains(LOCALIZATION)) {
                         final Charset current = Charset.defaultCharset();
                         if (current != null) {
@@ -321,7 +328,7 @@ fill:   for (int i=0; ; i++) {
                     }
                     break;
                 }
-                case 9: {
+                case 10: {
                     newSection = PLUGINS;
                     if (sections.contains(PLUGINS)) try {
                         children = (String[]) Class.forName("org.apache.sis.internal.storage.Capability")
@@ -336,7 +343,7 @@ fill:   for (int i=0; ; i++) {
                     nameKey = Vocabulary.Keys.DataFormats;
                     break;
                 }
-                case 10: {
+                case 11: {
                     newSection = LOGGING;
                     if (sections.contains(LOGGING)) {
                         nameKey = Vocabulary.Keys.Implementation;
@@ -345,7 +352,7 @@ fill:   for (int i=0; ; i++) {
                     }
                     break;
                 }
-                case 11: {
+                case 12: {
                     if (sections.contains(LOGGING)) {
                         nameKey = Vocabulary.Keys.Level;
                         final Level level = Logging.getLogger("").getLevel();   // Root logger level.
@@ -367,7 +374,7 @@ fill:   for (int i=0; ; i++) {
                     }
                     break;
                 }
-                case 12: {
+                case 13: {
                     newSection = PATHS;
                     if (sections.contains(PATHS)) {
                         nameKey = Vocabulary.Keys.UserHome;
@@ -375,14 +382,14 @@ fill:   for (int i=0; ; i++) {
                     }
                     break;
                 }
-                case 13: {
+                case 14: {
                     if (sections.contains(PATHS)) {
                         nameKey = Vocabulary.Keys.CurrentDirectory;
                         value = getProperty("user.dir");
                     }
                     break;
                 }
-                case 14: {
+                case 15: {
                     if (sections.contains(PATHS)) {
                         nameKey = Vocabulary.Keys.DataDirectory;
                         try {
@@ -403,28 +410,28 @@ fill:   for (int i=0; ; i++) {
                     }
                     break;
                 }
-                case 15: {
+                case 16: {
                     if (sections.contains(PATHS)) {
                         nameKey = Vocabulary.Keys.DataBase;
                         value = MetadataServices.getInstance().getInformation("DataSource", locale);
                     }
                     break;
                 }
-                case 16: {
+                case 17: {
                     if (sections.contains(PATHS)) {
                         nameKey = Vocabulary.Keys.TemporaryFiles;
                         value = getProperty("java.io.tmpdir");
                     }
                     break;
                 }
-                case 17: {
+                case 18: {
                     if (sections.contains(PATHS)) {
                         nameKey = Vocabulary.Keys.JavaHome;
                         value = javaHome = getProperty("java.home");
                     }
                     break;
                 }
-                case 18: {
+                case 19: {
                     newSection = LIBRARIES;
                     if (sections.contains(LIBRARIES)) {
                         nameKey = Vocabulary.Keys.JavaExtensions;
@@ -432,7 +439,7 @@ fill:   for (int i=0; ; i++) {
                     }
                     break;
                 }
-                case 19: {
+                case 20: {
                     if (sections.contains(LIBRARIES)) {
                         nameKey = Vocabulary.Keys.Classpath;
                         value = classpath(getProperty("java.class.path"), false);
@@ -443,7 +450,7 @@ fill:   for (int i=0; ; i++) {
             }
             /*
              * At this point, we have the information about one node to create.
-             * If the 'newSection' variable is non-null, then this new node shall
+             * If the `newSection` variable is non-null, then this new node shall
              * appear in a new section.
              */
             if (value == null) {


[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.

Posted by de...@apache.org.
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 <ma...@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;