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 2018/11/16 19:31:13 UTC

[sis] branch geoapi-4.0 updated (ee56b82 -> 108d1f3)

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 ee56b82  First draft of GridGeometry.getCoordinateReferenceSystem() method, except for the temporal axis and projected CRS.
     new 3613eef  Support time axis and leverage pre-defined coordinate system when possible.
     new feec75a  Deprecate the DefaultTemporalCRS methods working on java.util.Date. Replaced by java.time.Instant.
     new 108d1f3  When using the UCAR library for reading netCDF file, also use that library for parsing units.

The 3 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:
 .../internal/referencing/ServicesForMetadata.java  |   5 +-
 .../java/org/apache/sis/referencing/CommonCRS.java |  27 ++-
 .../sis/referencing/crs/DefaultTemporalCRS.java    |  57 ++++-
 .../org/apache/sis/referencing/CommonCRSTest.java  |  11 +-
 .../sis/internal/util/StandardDateFormat.java      |  93 +++++++-
 .../sis/internal/util/TemporalUtilities.java       |  10 +
 .../main/java/org/apache/sis/measure/Units.java    |   4 +-
 .../sis/internal/util/StandardDateFormatTest.java  |  39 +++-
 ide-project/NetBeans/nbproject/project.properties  |   2 +
 .../java/org/apache/sis/internal/netcdf/Axis.java  |  21 +-
 .../org/apache/sis/internal/netcdf/CRSBuilder.java | 235 +++++++++++++++++----
 .../org/apache/sis/internal/netcdf/Variable.java   |  31 ++-
 .../sis/internal/netcdf/impl/VariableInfo.java     |  25 ++-
 .../sis/internal/netcdf/ucar/VariableWrapper.java  |  44 ++++
 14 files changed, 538 insertions(+), 66 deletions(-)


[sis] 03/03: When using the UCAR library for reading netCDF file, also use that library for parsing units.

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 108d1f36106545893b50ba11cd14140404ce349e
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Nov 16 20:30:33 2018 +0100

    When using the UCAR library for reading netCDF file, also use that library for parsing units.
---
 .../main/java/org/apache/sis/measure/Units.java    |  4 +-
 ide-project/NetBeans/nbproject/project.properties  |  2 +
 .../org/apache/sis/internal/netcdf/CRSBuilder.java |  8 ++--
 .../org/apache/sis/internal/netcdf/Variable.java   | 51 ++++++++--------------
 .../sis/internal/netcdf/impl/VariableInfo.java     | 25 ++++++++++-
 .../sis/internal/netcdf/ucar/VariableWrapper.java  | 44 +++++++++++++++++++
 6 files changed, 94 insertions(+), 40 deletions(-)

diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
index 5bfde64..81f353e 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
@@ -1177,7 +1177,7 @@ public final class Units extends Static {
         /*
          * All Unit<Temperature>.
          */
-        K.related(1);
+        K.related(2);
         KELVIN       = K;
         CELSIUS      = add(K, LinearConverter.offset(  27315, 100), "°C", SI,    (short) 0);
         FAHRENHEIT   = add(K, new LinearConverter(100, 45967, 180), "°F", OTHER, (short) 0);
@@ -1267,7 +1267,7 @@ public final class Units extends Static {
     private static <Q extends Quantity<Q>> ConventionalUnit<Q> add(SystemUnit<Q> target, UnitConverter toTarget, String symbol, byte scope, short epsg) {
         final ConventionalUnit<Q> unit = UnitRegistry.init(new ConventionalUnit<>(target, toTarget, symbol, scope, epsg));
         final ConventionalUnit<Q>[] related = target.related();
-        if (related != null && unit.scope != UnitRegistry.SI) {
+        if (related != null && (unit.scope != UnitRegistry.SI || !toTarget.isLinear())) {
             // Search first empty slot. This algorithm is inefficient, but the length of those arrays is small (<= 7).
             int i = 0;
             while (related[i] != null) i++;
diff --git a/ide-project/NetBeans/nbproject/project.properties b/ide-project/NetBeans/nbproject/project.properties
index 7b55c26..3746e8e 100644
--- a/ide-project/NetBeans/nbproject/project.properties
+++ b/ide-project/NetBeans/nbproject/project.properties
@@ -164,7 +164,9 @@ javac.test.processorpath=\
 run.classpath=\
     ${javac.classpath}:\
     ${build.classes.dir}:\
+    ${maven.repository}/edu/ucar/udunits/${netcdf.version}/udunits-${netcdf.version}.jar:\
     ${maven.repository}/org/apache/derby/derby/${derby.version}/derby-${derby.version}.jar:\
+    ${maven.repository}/joda-time/joda-time/${joda-time.version}/joda-time-${joda-time.version}.jar:\
     ${maven.repository}/org/webjars/material-design-icons/${icons.version}/material-design-icons-${icons.version}.jar
 run.test.classpath=\
     ${javac.test.classpath}:\
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 79000bb..b614187 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
@@ -22,7 +22,6 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.StringJoiner;
 import java.util.function.Supplier;
-import java.util.Date;
 import java.time.Instant;
 import javax.measure.Unit;
 import org.opengis.util.FactoryException;
@@ -35,6 +34,7 @@ import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.DefaultSphericalCS;
 import org.apache.sis.referencing.cs.DefaultEllipsoidalCS;
 import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.internal.util.TemporalUtilities;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.measure.Units;
 
@@ -534,13 +534,15 @@ previous:   for (int i=components.size(); --i >= 0;) {
 
         /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on …"</cite>. */
         @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
-            final Instant epoch = getFirstAxis().coordinates.getEpoch();
+            final Axis axis = getFirstAxis();
+            axis.getUnit();                                     // Force epoch parsing if not already done.
+            Instant epoch = axis.coordinates.epoch;
             final CommonCRS.Temporal c = CommonCRS.Temporal.forEpoch(epoch);
             if (c != null) {
                 datum = c.datum();
             } else {
                 properties = properties("Time since " + epoch);
-                datum = factory.createTemporalDatum(properties, (epoch != null) ? Date.from(epoch) : null);
+                datum = factory.createTemporalDatum(properties, TemporalUtilities.toDate(epoch));
             }
         }
 
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
index 3b01060..e3b4b59 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
@@ -19,16 +19,11 @@ package org.apache.sis.internal.netcdf;
 import java.util.Locale;
 import java.util.Collection;
 import java.util.regex.Pattern;
-import java.util.regex.Matcher;
 import java.io.IOException;
 import java.awt.image.DataBuffer;
 import java.time.Instant;
 import javax.measure.Unit;
-import javax.measure.format.ParserException;
-import java.time.format.DateTimeParseException;
-import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.math.Vector;
-import org.apache.sis.measure.Units;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.util.resources.Errors;
@@ -47,7 +42,7 @@ public abstract class Variable extends NamedElement {
     /**
      * The pattern to use for parsing temporal units of the form "days since 1970-01-01 00:00:00".
      */
-    private static final Pattern TIME_PATTERN = Pattern.compile("(.+)\\Wsince\\W(.+)", Pattern.CASE_INSENSITIVE);
+    protected static final Pattern TIME_PATTERN = Pattern.compile("(.+)\\Wsince\\W(.+)", Pattern.CASE_INSENSITIVE);
 
     /**
      * Minimal number of dimension for accepting a variable as a coverage variable.
@@ -63,9 +58,9 @@ public abstract class Variable extends NamedElement {
 
     /**
      * If the unit is a temporal unit of the form "days since 1970-01-01 00:00:00", the epoch.
-     * Otherwise {@code null}.
+     * Otherwise {@code null}. This value can be set
      */
-    private Instant epoch;
+    protected Instant epoch;
 
     /**
      * Whether an attempt to parse the unit has already be done. This is used for avoiding
@@ -124,28 +119,28 @@ public abstract class Variable extends NamedElement {
     protected abstract String getUnitsString();
 
     /**
+     * Parses the given unit symbol and set the {@link #epoch} if the parsed unit is a temporal unit.
+     *
+     * @param  symbols  the unit symbol to parse.
+     * @return the parsed unit.
+     * @throws Exception if the unit can not be parsed. This wide exception type is used by the UCAR library.
+     */
+    protected abstract Unit<?> parseUnit(String symbols) throws Exception;
+
+    /**
      * Returns the unit of measurement for this variable, or {@code null} if unknown.
-     * This method parse the units from {@link #getUnitsString()} when first needed.
+     * This method parse the units from {@link #getUnitsString()} when first needed
+     * and sets {@link #epoch} as a side-effect if the unit is temporal.
      *
      * @return the unit of measurement, or {@code null}.
      */
     public final Unit<?> getUnit() {
         if (!unitParsed) {
             unitParsed = true;                          // Set first for avoiding to report errors many times.
-            String symbols = getUnitsString();
+            final String symbols = getUnitsString();
             if (symbols != null && !symbols.isEmpty()) try {
-                final Matcher parts = TIME_PATTERN.matcher(symbols);
-                if (parts.matches()) {
-                    /*
-                     * If we enter in this block, the unit is of the form "days since 1970-01-01 00:00:00".
-                     * The TIME_PATTERN splits the string in two parts, "days" and "1970-01-01 00:00:00".
-                     * The parse method will replace the space between date and time by 'T' letter.
-                     */
-                    epoch = StandardDateFormat.parseInstantUTC(parts.group(2));
-                    symbols = parts.group(1);
-                }
-                unit = Units.valueOf(symbols);
-            } catch (ParserException | DateTimeParseException ex) {
+                unit = parseUnit(symbols);
+            } catch (Exception ex) {
                 warning(listeners, Variable.class, "getUnit", ex, Errors.getResources(listeners.getLocale()),
                         Errors.Keys.CanNotAssignUnitToVariable_2, getName(), symbols);
             }
@@ -154,18 +149,6 @@ public abstract class Variable extends NamedElement {
     }
 
     /**
-     * Returns the epoch of the temporal unit, or {@code null} if none.
-     *
-     * @return the epoch, or {@code null}.
-     */
-    final Instant getEpoch() {
-        if (epoch == null) {
-            getUnit();          // Epoch calculation as a side-effect.
-        }
-        return epoch;
-    }
-
-    /**
      * Returns the variable data type.
      *
      * @return the variable data type, or {@link DataType#UNKNOWN} if unknown.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
index 6a0de7f..e321dde 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
@@ -19,8 +19,10 @@ package org.apache.sis.internal.netcdf.impl;
 import java.util.Map;
 import java.util.Collection;
 import java.util.Collections;
-import java.io.IOException;
+import java.util.regex.Matcher;
 import java.lang.reflect.Array;
+import java.io.IOException;
+import javax.measure.Unit;
 import ucar.nc2.constants.CF;
 import ucar.nc2.constants.CDM;
 import ucar.nc2.constants._Coordinate;
@@ -31,10 +33,12 @@ import org.apache.sis.internal.netcdf.Resources;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.storage.io.HyperRectangleReader;
 import org.apache.sis.internal.storage.io.Region;
+import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.netcdf.AttributeNames;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.measure.Units;
 import org.apache.sis.math.Vector;
 
 
@@ -349,6 +353,25 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     }
 
     /**
+     * Parses the given unit symbol and set the {@link #epoch} if the parsed unit is a temporal unit.
+     * This method is called by {@link #getUnit()}.
+     */
+    @Override
+    protected Unit<?> parseUnit(String symbols) {
+        final Matcher parts = TIME_PATTERN.matcher(symbols);
+        if (parts.matches()) {
+            /*
+             * If we enter in this block, the unit is of the form "days since 1970-01-01 00:00:00".
+             * The TIME_PATTERN splits the string in two parts, "days" and "1970-01-01 00:00:00".
+             * The parse method will replace the space between date and time by 'T' letter.
+             */
+            epoch = StandardDateFormat.parseInstantUTC(parts.group(2));
+            symbols = parts.group(1);
+        }
+        return Units.valueOf(symbols);
+    }
+
+    /**
      * Returns the type of data, or {@code UNKNOWN} if the data type is unknown to this method.
      * If this variable has a {@code "_Unsigned = true"} attribute, then the returned data type
      * will be a unsigned variant.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
index 3a2aaf7..f2a8f5a 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.io.File;
 import java.io.IOException;
 import java.util.Collection;
+import javax.measure.Unit;
 import ucar.ma2.Array;
 import ucar.ma2.Section;
 import ucar.ma2.InvalidRangeException;
@@ -27,12 +28,15 @@ import ucar.nc2.Attribute;
 import ucar.nc2.Dimension;
 import ucar.nc2.VariableIF;
 import ucar.nc2.dataset.VariableEnhanced;
+import ucar.nc2.units.SimpleUnit;
+import ucar.nc2.units.DateUnit;
 import org.apache.sis.math.Vector;
 import org.apache.sis.internal.netcdf.DataType;
 import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.measure.Units;
 
 
 /**
@@ -115,6 +119,46 @@ final class VariableWrapper extends Variable {
     }
 
     /**
+     * Parses the given unit symbol and set the {@link #epoch} if the parsed unit is a temporal unit.
+     * This method is called by {@link #getUnit()}. This implementation delegates the work to the UCAR
+     * library and converts the result to {@link Unit} and {@link java.time.Instant} objects.
+     */
+    @Override
+    protected Unit<?> parseUnit(final String symbols) throws Exception {
+        if (TIME_PATTERN.matcher(symbols).matches()) {
+            /*
+             * UCAR library has to methods for getting epoch: getDate() and getDateOrigin().
+             * The former takes in account numbers that may appear before the unit, for example
+             * "2 hours since 1970-01-01 00:00:00". If there is no such number, then the two methods
+             * are equivalent.
+             */
+            final DateUnit temporal = new DateUnit(symbols);
+            epoch = temporal.getDate().toInstant();
+            return Units.SECOND.multiply(temporal.getTimeUnit().getValueInSeconds());
+        } else {
+            /*
+             * For all other units, we get the base unit (meter, radian, Kelvin, etc.) and multiply by the scale factor.
+             * We also need to take the offset in account for constructing the °C unit as a unit shifted from its Kelvin
+             * base. The UCAR library does not provide method giving directly this information, so we infer it indirectly
+             * by converting value zero.
+             */
+            final SimpleUnit ucar = SimpleUnit.factoryWithExceptions(symbols);
+            if (ucar.isUnknownUnit()) {
+                return Units.valueOf(symbols);
+            }
+            final String baseUnit = ucar.getUnitString();
+            Unit<?> unit = Units.valueOf(baseUnit);
+            final double scale  = ucar.getValue();
+            final double offset = ucar.convertTo(0, SimpleUnit.factoryWithExceptions(baseUnit));
+            unit = unit.shift(offset);
+            if (!Double.isNaN(scale)) {
+                unit = unit.multiply(scale);
+            }
+            return unit;
+        }
+    }
+
+    /**
      * Returns the variable data type.
      * This method may return {@code UNKNOWN} if the datatype is unknown.
      */


[sis] 01/03: Support time axis and leverage pre-defined coordinate system when possible.

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 3613eef6ec225781624e3d36fa722c8411335abe
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Nov 16 16:41:11 2018 +0100

    Support time axis and leverage pre-defined coordinate system when possible.
---
 .../java/org/apache/sis/referencing/CommonCRS.java |  27 ++-
 .../org/apache/sis/referencing/CommonCRSTest.java  |  11 +-
 .../sis/internal/util/StandardDateFormat.java      |  93 +++++++-
 .../sis/internal/util/StandardDateFormatTest.java  |  39 +++-
 .../java/org/apache/sis/internal/netcdf/Axis.java  |  21 +-
 .../org/apache/sis/internal/netcdf/CRSBuilder.java | 233 +++++++++++++++++----
 .../org/apache/sis/internal/netcdf/Variable.java   |  42 +++-
 7 files changed, 412 insertions(+), 54 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java
index 88fdd4e..71b647c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java
@@ -21,6 +21,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import java.time.Instant;
 import javax.measure.Unit;
 import javax.measure.quantity.Time;
 import org.opengis.metadata.Identifier;
@@ -559,7 +560,7 @@ public enum CommonCRS {
         GeographicCRS object = cachedNormalized;
         if (object == null) {
             DefaultGeographicCRS crs = DefaultGeographicCRS.castOrCopy(geographic());
-            crs = crs.forConvention(AxesConvention.RIGHT_HANDED); // Equivalent to NORMALIZED in our cases, but faster.
+            crs = crs.forConvention(AxesConvention.RIGHT_HANDED);       // Equivalent to NORMALIZED in our cases, but faster.
             synchronized (this) {
                 object = cachedNormalized;
                 if (object == null) {
@@ -1366,6 +1367,7 @@ public enum CommonCRS {
 
         /**
          * Creates the coordinate system associated to this vertical object.
+         * This is used only for CRS not identified by an EPSG code.
          * This method does not cache the coordinate system.
          */
         private VerticalCS cs() {
@@ -1479,7 +1481,7 @@ public enum CommonCRS {
      * </table></blockquote>
      *
      * @author  Martin Desruisseaux (Geomatys)
-     * @version 0.4
+     * @version 1.0
      * @since   0.4
      * @module
      */
@@ -1578,6 +1580,27 @@ public enum CommonCRS {
         }
 
         /**
+         * Returns the enumeration value for the given epoch, or {@code null} if none.
+         * If the epoch is January 1st, 1970, then this method returns {@link #UNIX}.
+         *
+         * @param  epoch  the epoch for which to get an enumeration value, or {@code null}.
+         * @return the enumeration value for the given epoch, or {@code null} if none.
+         *
+         * @since 1.0
+         */
+        public static Temporal forEpoch(final Instant epoch) {
+            if (epoch != null) {
+                final long e = epoch.toEpochMilli();
+                for (final Temporal candidate : values()) {
+                    if (candidate.epoch == e) {
+                        return candidate;
+                    }
+                }
+            }
+            return null;
+        }
+
+        /**
          * Returns the coordinate reference system associated to this temporal object.
          * The following table summarizes the CRS known to this class,
          * together with an enumeration value that can be used for fetching that CRS:
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java
index c293179..5a22731 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java
@@ -19,6 +19,7 @@ package org.apache.sis.referencing;
 import java.util.Date;
 import java.util.Map;
 import java.util.HashMap;
+import java.time.Instant;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.referencing.crs.TemporalCRS;
 import org.opengis.referencing.crs.VerticalCRS;
@@ -50,7 +51,7 @@ import static org.apache.sis.test.TestUtilities.*;
  * Tests the {@link CommonCRS} class.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.4
  * @module
  */
@@ -328,4 +329,12 @@ public final strictfp class CommonCRSTest extends TestCase {
         assertSame("WGS84", CommonCRS.WGS84, CommonCRS.forDatum(CommonCRS.WGS84.geographic()));
         assertSame("WGS72", CommonCRS.WGS72, CommonCRS.forDatum(CommonCRS.WGS72.geographic()));
     }
+
+    /**
+     * Tests {@link CommonCRS.Temporal#forEpoch(Instant)}.
+     */
+    @Test
+    public void testForEpoch() {
+        assertSame(CommonCRS.Temporal.UNIX, CommonCRS.Temporal.forEpoch(Instant.ofEpochMilli(0)));        // As specified in Javadoc.
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/StandardDateFormat.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/StandardDateFormat.java
index a0f7abe..921d3ee 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/StandardDateFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/StandardDateFormat.java
@@ -25,13 +25,12 @@ import java.text.NumberFormat;
 import java.text.FieldPosition;
 import java.text.ParsePosition;
 import java.text.ParseException;
-
-// Branch-dependent imports
 import java.time.DateTimeException;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.OffsetDateTime;
+import java.time.OffsetTime;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.temporal.Temporal;
@@ -43,13 +42,15 @@ import java.time.format.DateTimeFormatterBuilder;
 import java.time.format.DateTimeParseException;
 import java.time.format.SignStyle;
 
+import org.apache.sis.util.CharSequences;
+
 
 /**
  * A date format used for parsing dates in the {@code "yyyy-MM-dd'T'HH:mm:ss.SSSX"} pattern, but in which
  * the time is optional. For this class, "Standard" is interpreted as "close to ISO 19162 requirements",
  * which is not necessarily identical to other ISO standards.
  *
- * External users should use nothing else than the parsing and formating methods.
+ * External users should use nothing else than the parsing and formatting methods.
  * The methods for configuring the {@code DateFormat} instances may or may not work
  * depending on the branch.
  *
@@ -60,7 +61,7 @@ import java.time.format.SignStyle;
  * but nevertheless allows to specify a timezone.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.6
  * @module
  */
@@ -72,10 +73,17 @@ public final class StandardDateFormat extends DateFormat {
 
     /**
      * The {@value} timezone ID.
+     *
+     * @see ZoneOffset#UTC
      */
     public static final String UTC = "UTC";
 
     /**
+     * Midnight (00:00) UTC.
+     */
+    private static final OffsetTime MIDNIGHT = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC);
+
+    /**
      * The thread-safe instance to use for reading and formatting dates.
      * Only the year is mandatory, all other fields are optional.
      */
@@ -106,6 +114,8 @@ public final class StandardDateFormat extends DateFormat {
     /**
      * Parses the given date and/or time, which may have an optional timezone. This method applies heuristic rules
      * for choosing if the object should be returned as a local date, or a date and time with timezone, <i>etc</i>.
+     * The full date format is of the form "1970-01-01T00:00:00.000Z", but this method also accepts spaces in place
+     * of 'T' as in "1970-01-01 00:00:00".
      *
      * @param  text  the character string to parse, or {@code null}.
      * @return a temporal object for the given text, or {@code null} if the given text was null.
@@ -115,7 +125,75 @@ public final class StandardDateFormat extends DateFormat {
      */
     public static Temporal parseBest(final CharSequence text) {
         // Cast is safe if all QUERIES elements return a Temporal subtype.
-        return (text != null) ? (Temporal) FORMAT.parseBest(text, QUERIES) : null;
+        return (text != null) ? (Temporal) FORMAT.parseBest(toISO(text, 0, text.length()), QUERIES) : null;
+    }
+
+    /**
+     * Parses the given date as an instant, assuming UTC timezone if unspecified.
+     *
+     * @param  text   the text to parse as an instant in UTC timezone by default, or {@code null}.
+     * @return the instant for the given text, or {@code null} if the given text was null.
+     * @throws DateTimeParseException if the text can not be parsed as a date.
+     */
+    public static Instant parseInstantUTC(final CharSequence text) {
+        return (text != null) ? parseInstantUTC(text, 0, text.length()) : null;
+    }
+
+    /**
+     * Parses the given date as an instant, assuming UTC timezone if unspecified.
+     *
+     * @param  text   the text to parse as an instant in UTC timezone by default.
+     * @param  lower  index of the first character to parse.
+     * @param  upper  index after the last character to parse.
+     * @return the instant for the given text.
+     * @throws DateTimeParseException if the text can not be parsed as a date.
+     */
+    public static Instant parseInstantUTC(final CharSequence text, final int lower, final int upper) {
+        TemporalAccessor date = FORMAT.parseBest(toISO(text, lower, upper), QUERIES);
+        if (date instanceof Instant) {
+            return (Instant) date;
+        }
+        final OffsetDateTime time;
+        if (date instanceof LocalDateTime) {
+            time = ((LocalDateTime) date).atOffset(ZoneOffset.UTC);
+        } else {
+            time = ((LocalDate) date).atTime(MIDNIGHT);
+        }
+        return time.toInstant();
+    }
+
+    /**
+     * Modifies the given date and time string for making it more compliant to ISO syntax.
+     * If date and time are separated by spaces, then this method replaces those spaces by
+     * the 'T' letter.
+     *
+     * @param  text   the text to make more compliant with ISO syntax.
+     * @param  lower  index of the first character to examine.
+     * @param  upper  index after the last character to examine.
+     * @return sub-sequence of {@code text} from {@code lower} to {@code upper}, potentially modified.
+     */
+    static CharSequence toISO(CharSequence text, int lower, int upper) {
+        int sep = CharSequences.indexOf(text, ':', lower, upper);
+        if (sep >= lower) {
+            sep = CharSequences.skipTrailingWhitespaces(text, lower, sep);
+            while (sep > lower) {
+                final int c = Character.codePointBefore(text, sep);
+                final int timeStart = sep;
+                sep -= Character.charCount(c);
+                if (!Character.isDigit(c)) {
+                    if (Character.isWhitespace(c)) {
+                        sep = CharSequences.skipTrailingWhitespaces(text, lower, sep);
+                        if (sep > lower && Character.isDigit(Character.codePointBefore(text, sep))) {
+                            text = new StringBuilder(upper - lower).append(text, lower, upper).replace(sep, timeStart, "T");
+                            upper = text.length();
+                            lower = 0;
+                        }
+                    }
+                    break;
+                }
+            }
+        }
+        return CharSequences.trimWhitespaces(text, lower, upper);
     }
 
     /**
@@ -320,6 +398,7 @@ public final class StandardDateFormat extends DateFormat {
 
     /**
      * Parses the given text starting at the given position.
+     * Contrarily to {@link #parse(String)}, this method does not accept spaces as a separator between date and time.
      *
      * @param  text      the text to parse.
      * @param  position  position where to start the parsing.
@@ -336,7 +415,7 @@ public final class StandardDateFormat extends DateFormat {
     }
 
     /**
-     * Parses the given text.
+     * Parses the given text. This method accepts space as a separator between date and time.
      *
      * @param  text  the text to parse.
      * @return the date (never null).
@@ -345,7 +424,7 @@ public final class StandardDateFormat extends DateFormat {
     @Override
     public Date parse(final String text) throws ParseException {
         try {
-            return toDate(format.parse(text));
+            return toDate(format.parse(toISO(text, 0, text.length())));
         } catch (DateTimeException | ArithmeticException e) {
             throw (ParseException) new ParseException(e.getLocalizedMessage(), getErrorIndex(e, null)).initCause(e);
         }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/StandardDateFormatTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/StandardDateFormatTest.java
index cb55e8a..043a7c4 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/StandardDateFormatTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/StandardDateFormatTest.java
@@ -33,7 +33,7 @@ import static org.junit.Assert.*;
  * Tests the {@link StandardDateFormat} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.6
  * @module
  */
@@ -50,6 +50,24 @@ public final strictfp class StandardDateFormatTest extends TestCase {
     }
 
     /**
+     * Tests {@link StandardDateFormat#toISO(CharSequence, int, int)}.
+     */
+    @Test
+    public void testToISO() {
+        assertSame  ("2009-01-01T06:00:00+01:00", toISO("2009-01-01T06:00:00+01:00"));
+        assertEquals("2005-09-22T04:30:15",       toISO("2005-09-22 04:30:15"));
+        assertSame  ("2005-09-22",                toISO("2005-09-22"));
+        assertEquals("2005-09-22T04 : 30 : 15",   toISO("  2005-09-22   04 : 30 : 15 "));
+    }
+
+    /**
+     * Helper method for {@link #testToISO()}.
+     */
+    private static String toISO(final String text) {
+        return StandardDateFormat.toISO(text, 0, text.length()).toString();
+    }
+
+    /**
      * Tests parsing a date.
      *
      * @throws ParseException if an error occurred while parsing the date.
@@ -67,6 +85,7 @@ public final strictfp class StandardDateFormatTest extends TestCase {
         assertEquals(date("2005-09-22 04:30:15"), f.parse("2005-09-22T04:30:15Z"));
         assertEquals(date("2005-09-22 04:30:15"), f.parse("2005-09-22T04:30:15"));
         assertEquals(date("2005-09-22 04:30:00"), f.parse("2005-09-22T04:30"));
+        assertEquals(date("2005-09-22 04:30:00"), f.parse("2005-09-22 04:30"));
         assertEquals(date("2005-09-22 04:00:00"), f.parse("2005-09-22T04"));
         assertEquals(date("2005-09-22 00:00:00"), f.parse("2005-09-22"));
         assertEquals(date("2005-09-22 00:00:00"), f.parse("2005-9-22"));
@@ -86,10 +105,28 @@ public final strictfp class StandardDateFormatTest extends TestCase {
         assertEquals(Instant.ofEpochMilli(day + (( 3*60 +  2)*60 +  1)*1000 + 90), StandardDateFormat.parseBest("2016-06-27T03:02:01.09Z"));
         assertEquals(LocalDateTime.of(2016, 6, 27, 16, 48, 12),                    StandardDateFormat.parseBest("2016-06-27T16:48:12"));
         assertEquals(LocalDateTime.of(2016, 6, 27, 16, 48),                        StandardDateFormat.parseBest("2016-06-27T16:48"));
+        assertEquals(LocalDateTime.of(2016, 6, 27, 16, 48),                        StandardDateFormat.parseBest("2016-06-27 16:48"));
         assertEquals(LocalDate.of(2016, 6, 27),                                    StandardDateFormat.parseBest("2016-06-27"));
     }
 
     /**
+     * Tests parsing a date as an instant, assuming UTC timezone if unspecified.
+     *
+     * @since 1.0
+     */
+    @Test
+    public void testParseInstant() {
+        final long day = 1466985600000L;
+        assertEquals(Instant.ofEpochMilli(day + ((16*60 + 48)*60     )*1000),      StandardDateFormat.parseInstantUTC("2016-06-27T16:48Z"));
+        assertEquals(Instant.ofEpochMilli(day + ((16*60 + 48)*60 + 12)*1000),      StandardDateFormat.parseInstantUTC("2016-06-27T16:48:12Z"));
+        assertEquals(Instant.ofEpochMilli(day + (( 3*60 +  2)*60 +  1)*1000 + 90), StandardDateFormat.parseInstantUTC("2016-06-27T03:02:01.09Z"));
+        assertEquals(Instant.ofEpochMilli(day + ((16*60 + 48)*60 + 12)*1000),      StandardDateFormat.parseInstantUTC("2016-06-27T16:48:12"));
+        assertEquals(Instant.ofEpochMilli(day + ((16*60 + 48)*60     )*1000),      StandardDateFormat.parseInstantUTC("2016-06-27T16:48"));
+        assertEquals(Instant.ofEpochMilli(day + ((16*60 + 48)*60     )*1000),      StandardDateFormat.parseInstantUTC("2016-06-27 16:48"));
+        assertEquals(Instant.ofEpochMilli(day),                                    StandardDateFormat.parseInstantUTC("2016-06-27"));
+    }
+
+    /**
      * Tests formatting and parsing a negative year.
      * This test uses the Julian epoch (January 1st, 4713 BC at 12:00 UTC in proleptic Julian calendar;
      * equivalent to November 24, 4714 BC when expressed in the proleptic Gregorian calendar instead).
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 c8383d7..7544912 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
@@ -82,7 +82,7 @@ public final class Axis extends NamedElement {
     /**
      * The axis direction, or {@code null} if unknown.
      */
-    private final AxisDirection direction;
+    final AxisDirection direction;
 
     /**
      * The indices of the grid dimension associated to this axis. The length of this array is often 1.
@@ -220,6 +220,23 @@ public final class Axis extends NamedElement {
     }
 
     /**
+     * Returns the unit of measurement of this axis, or {@code null} if unknown.
+     *
+     * @return the unit of measurement, or {@code null} if unknown.
+     */
+    public final Unit<?> getUnit() {
+        return coordinates.getUnit();
+    }
+
+    /**
+     * Returns {@code true} if the given axis specifies the same direction and unit of measurement than this axis.
+     * This is used for testing is a predefined axis can be used instead than invoking {@link #toISO(CSFactory)}.
+     */
+    final boolean isSameUnitAndDirection(final CoordinateSystemAxis axis) {
+        return axis.getDirection().equals(direction) && axis.getUnit().equals(getUnit());
+    }
+
+    /**
      * Creates an ISO 19111 axis from the information stored in this netCDF axis.
      *
      * @param  factory  the factory to use for creating the coordinate system axis.
@@ -264,7 +281,7 @@ public final class Axis extends NamedElement {
          * We provide default values for the most well-accepted values and leave other values to null.
          * Those null values can be accepted if users specify their own factory.
          */
-        Unit<?> unit = coordinates.getUnit();
+        Unit<?> unit = getUnit();
         if (unit == null) {
             switch (abbreviation) {
                 /*
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 243b2ab..79000bb 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
@@ -21,14 +21,22 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.StringJoiner;
+import java.util.function.Supplier;
+import java.util.Date;
+import java.time.Instant;
+import javax.measure.Unit;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.cs.*;
 import org.opengis.referencing.datum.*;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.CRSFactory;
 import org.apache.sis.referencing.CommonCRS;
-import org.apache.sis.util.resources.Errors;
+import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.cs.DefaultSphericalCS;
+import org.apache.sis.referencing.cs.DefaultEllipsoidalCS;
 import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.measure.Units;
 
 
 /**
@@ -46,13 +54,19 @@ import org.apache.sis.storage.DataStoreContentException;
  */
 abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
     /**
+     * The coordinate reference system which is presumed the basis of datum on netCDF files.
+     * Note: if this default is changed, search also for "WGS 84" strings in this class.
+     */
+    private static final CommonCRS DEFAULT = CommonCRS.WGS84;
+
+    /**
      * The type of datum as a GeoAPI sub-interface of {@link Datum}.
      * Used for verifying the type of cached datum at {@link #datumIndex}.
      */
     private final Class<D> datumType;
 
     /**
-     * Name of the datum on which the CRS is presumed to be based, or {@code null}. This is used
+     * Name of the datum on which the CRS is presumed to be based, or {@code ""}. This is used
      * for building a datum name like <cite>"Unknown datum presumably based on WGS 84"</cite>.
      */
     private final String datumBase;
@@ -79,6 +93,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
     /**
      * The axes to use for creating the coordinate reference system.
      * They are information about netCDF axes, not yet ISO 19111 axes.
+     * The axis are listed in "natural" order (reverse of netCDF order).
      */
     private Axis[] axes;
 
@@ -96,7 +111,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
      * Creates a new CRS builder based on datum of the given type.
      *
      * @param  datumType   the type of datum as a GeoAPI sub-interface of {@link Datum}.
-     * @param  datumBase   name of the datum on which the CRS is presumed to be based, or {@code null}.
+     * @param  datumBase   name of the datum on which the CRS is presumed to be based, or {@code ""}.
      * @param  datumIndex  index of the cached datum in a {@code Datum[]} array.
      * @param  minDim      minimum number of dimensions (usually 1, 2 or 3).
      * @param  maxDim      maximum number of dimensions (usually 1, 2 or 3).
@@ -122,6 +137,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
     @SuppressWarnings("fallthrough")
     public static void dispatch(final List<CRSBuilder<?,?>> components, final Axis axis) throws DataStoreContentException {
         final Class<? extends CRSBuilder<?,?>> addTo;
+        final Supplier<CRSBuilder<?,?>> constructor;
         int alternative = -1;
         switch (axis.abbreviation) {
             case 'h': for (int i=components.size(); --i >= 0;) {        // Can apply to either Geographic or Projected.
@@ -130,12 +146,12 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
                               break;
                           }
                       }                    // Fallthrough
-            case 'λ': case 'φ':            addTo = Geographic.class;  break;
-            case 'θ': case 'Ω': case 'r':  addTo = Spherical.class;   break;
-            case 'E': case 'N':            addTo = Projected.class;   break;
-            case 'H': case 'D':            addTo = Vertical.class;    break;
-            case 't':                      addTo = Temporal.class;    break;
-            default:                       addTo = Engineering.class; break;
+            case 'λ': case 'φ':            addTo =  Geographic.class; constructor =  Geographic::new; break;
+            case 'θ': case 'Ω': case 'r':  addTo =   Spherical.class; constructor =   Spherical::new; break;
+            case 'E': case 'N':            addTo =   Projected.class; constructor =   Projected::new; break;
+            case 'H': case 'D':            addTo =    Vertical.class; constructor =    Vertical::new; break;
+            case 't':                      addTo =    Temporal.class; constructor =    Temporal::new; break;
+            default:                       addTo = Engineering.class; constructor = Engineering::new; break;
         }
         /*
          * If a builder of 'addTo' class already exists, add the axis in the existing builder.
@@ -149,12 +165,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
                 return;
             }
         }
-        final CRSBuilder<?,?> builder;
-        try {
-            builder = addTo.getConstructor((Class<?>[]) null).newInstance((Object[]) null);
-        } catch (ReflectiveOperationException e) {
-            throw new AssertionError(e);                            // Should never happen.
-        }
+        final CRSBuilder<?,?> builder = constructor.get();
         /*
          * Before to add the axis to a newly created builder, verify if we wrongly associated
          * the ellipsoidal height to Geographic builder before. The issue is that ellipsoidal
@@ -190,7 +201,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
      */
     private void add(final Axis axis) throws DataStoreContentException {
         if (dimension == Byte.MAX_VALUE) {
-            throw new DataStoreContentException(Errors.getResources(axes[0].coordinates.getLocale())
+            throw new DataStoreContentException(Errors.getResources(getFirstAxis().coordinates.getLocale())
                     .getString(Errors.Keys.ExcessiveListSize_2, "axes", (short) (Byte.MAX_VALUE + 1)));
         }
         if (dimension >= axes.length) {
@@ -200,6 +211,21 @@ previous:   for (int i=components.size(); --i >= 0;) {
     }
 
     /**
+     * Returns whether the coordinate system has at least 3 axes.
+     */
+    final boolean is3D() {
+        return dimension >= 3;
+    }
+
+    /**
+     * Returns the first axis. This method is invoked for coordinate reference systems that are known
+     * to contain only one axis, for example temporal coordinate systems.
+     */
+    final Axis getFirstAxis() {
+        return axes[0];
+    }
+
+    /**
      * Creates the coordinate reference system.
      * This method can be invoked after all axes have been dispatched.
      *
@@ -207,25 +233,49 @@ previous:   for (int i=components.size(); --i >= 0;) {
      */
     public final SingleCRS build(final Decoder decoder) throws FactoryException, DataStoreContentException {
         if (dimension < minDim || dimension > maxDim) {
-            final Variable axis = axes[0].coordinates;
+            final Variable axis = getFirstAxis().coordinates;
             throw new DataStoreContentException(axis.resources().getString(Resources.Keys.UnexpectedAxisCount_4,
                     axis.getFilename(), getClass().getSimpleName(), dimension, NamedElement.listNames(axes, dimension, ", ")));
         }
         datum = datumType.cast(decoder.datumCache[datumIndex]);
         if (datum == null) {
-            createDatum(decoder.getDatumFactory(), properties("Unknown datum presumably based on ".concat(datumBase)));
+            // 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;
         }
-        final StringJoiner joiner = new StringJoiner(" ");
-        final CSFactory csFactory = decoder.getCSFactory();
-        final CoordinateSystemAxis[] iso = new CoordinateSystemAxis[dimension];
-        for (int i=0; i<iso.length; i++) {
-            final Axis axis = axes[i];
-            joiner.add(axis.getName());
-            iso[i] = axis.toISO(csFactory);
+        /*
+         * Verify if a pre-defined coordinate system can be used. This is often the case, for example
+         * the EPSG::6424 coordinate system can be used for (longitude, latitude) axes in degrees.
+         * Using a pre-defined CS allows us to get more complete definitions (minimum and maximum values, etc.).
+         *
+         * TODO: verify minimum and maximum longitude values for making sure we have a -180 … 180° range.
+         */
+        candidateCS();
+        if (coordinateSystem != null) {
+            for (int i=dimension; --i >= 0;) {
+                final Axis expected = axes[i];
+                if (expected == null || !expected.isSameUnitAndDirection(coordinateSystem.getAxis(i))) {
+                    coordinateSystem = null;
+                    break;
+                }
+            }
+        }
+        final Map<String,?> properties;
+        if (coordinateSystem == null) {
+            // Fallback if the coordinate system is not common.
+            final StringJoiner joiner = new StringJoiner(" ");
+            final CSFactory csFactory = decoder.getCSFactory();
+            final CoordinateSystemAxis[] iso = new CoordinateSystemAxis[dimension];
+            for (int i=0; i<iso.length; i++) {
+                final Axis axis = axes[i];
+                joiner.add(axis.getName());
+                iso[i] = axis.toISO(csFactory);
+            }
+            properties = properties(joiner.toString());
+            createCS(csFactory, properties, iso);
+        } else {
+            properties = properties(NamedElement.listNames(axes, dimension, " "));
         }
-        final Map<String,?> properties = properties(joiner.toString());
-        createCS(csFactory, properties, iso);
         return createCRS(decoder.getCRSFactory(), properties);
     }
 
@@ -239,6 +289,14 @@ previous:   for (int i=components.size(); --i >= 0;) {
     }
 
     /**
+     * If a brief inspection of unit and direction of the {@linkplain #getFirstAxis() first axis} suggests
+     * that a predefined coordinate system could be used, sets the {@link #coordinateSystem} field to that CS.
+     * The coordinate system does not need to be a full match since all axes will be verified by the caller.
+     * This method is invoked before to fallback on {@link #createCS(CSFactory, Map, CoordinateSystemAxis[])}.
+     */
+    abstract void candidateCS();
+
+    /**
      * Creates the datum for the coordinate reference system to build. The datum are generally not specified in netCDF files.
      * To make that clearer, this method builds datum with names like <cite>"Unknown datum presumably based on WGS 84"</cite>.
      * The newly created datum is assigned to the {@link #datum} field.
@@ -272,6 +330,9 @@ previous:   for (int i=components.size(); --i >= 0;) {
      * They all have in common to be based on a {@link GeodeticDatum}.
      */
     private abstract static class Geodetic<CS extends CoordinateSystem> extends CRSBuilder<GeodeticDatum, CS> {
+        /** Whether the coordinate system has longitude before latitude. */
+        boolean isLongitudeFirst;
+
         /** For subclasses constructors. */
         Geodetic(final byte minDim) {
             super(GeodeticDatum.class, "WGS 84", (byte) 0, minDim, (byte) 3);
@@ -279,20 +340,49 @@ previous:   for (int i=components.size(); --i >= 0;) {
 
         /** Creates a {@link GeodeticDatum} for <cite>"Unknown datum based on WGS 84"</cite>. */
         @Override final void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
-            final GeodeticDatum template = CommonCRS.WGS84.datum();
+            final GeodeticDatum template = DEFAULT.datum();
             datum = factory.createGeodeticDatum(properties, template.getEllipsoid(), template.getPrimeMeridian());
         }
+
+        /**
+         * Returns {@code true} if the coordinate system may be one of the predefined CS. A returns value of {@code true}
+         * is not a guarantee that the coordinate system in the netCDF file matches the predefined CS; it only tells that
+         * this is reasonable chances to be the case based on a brief inspection of the first coordinate system axis.
+         * If {@code true}, then {@link #isLongitudeFirst} will have been set to an indication of axis order.
+         *
+         * @param  expected  the expected unit of measurement of the first axis.
+         */
+        final boolean isPredefined(final Unit<?> expected) {
+            final Axis axis = getFirstAxis();
+            if (expected.equals(axis.getUnit())) {
+                isLongitudeFirst = AxisDirection.EAST.equals(axis.direction);
+                if (isLongitudeFirst || AxisDirection.NORTH.equals(axis.direction)) {
+                    return true;
+                }
+            }
+            return false;
+        }
     }
 
     /**
      * Builder for geocentric CRS with (θ,Ω,r) axes.
      */
     private static final class Spherical extends Geodetic<SphericalCS> {
-        /** Creates a new builder (invoked by reflection). */
+        /** Creates a new builder (invoked by lambda function). */
         public Spherical() {
             super((byte) 3);
         }
 
+        /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
+        @Override void candidateCS() {
+            if (isPredefined(Units.DEGREE)) {
+                coordinateSystem = (SphericalCS) DEFAULT.spherical().getCoordinateSystem();
+                if (isLongitudeFirst) {
+                    coordinateSystem = DefaultSphericalCS.castOrCopy(coordinateSystem).forConvention(AxesConvention.RIGHT_HANDED);
+                }
+            }
+        }
+
         /** Creates the three-dimensional {@link SphericalCS} from given axes. */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
             coordinateSystem = factory.createSphericalCS(properties, axes[0], axes[1], axes[2]);
@@ -309,11 +399,21 @@ previous:   for (int i=components.size(); --i >= 0;) {
      * The height, if present, is ellipsoidal height.
      */
     private static final class Geographic extends Geodetic<EllipsoidalCS> {
-        /** Creates a new builder (invoked by reflection). */
+        /** Creates a new builder (invoked by lambda function). */
         public Geographic() {
             super((byte) 2);
         }
 
+        /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
+        @Override void candidateCS() {
+            if (isPredefined(Units.DEGREE)) {
+                coordinateSystem = (is3D() ? DEFAULT.geographic3D() : DEFAULT.geographic()).getCoordinateSystem();
+                if (isLongitudeFirst) {
+                    coordinateSystem = DefaultEllipsoidalCS.castOrCopy(coordinateSystem).forConvention(AxesConvention.RIGHT_HANDED);
+                }
+            }
+        }
+
         /** Creates the two- or three-dimensional {@link EllipsoidalCS} from given axes. */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
             if (axes.length > 2) {
@@ -333,11 +433,18 @@ previous:   for (int i=components.size(); --i >= 0;) {
      * Projected CRS with (E,N,h) axes.
      */
     private static final class Projected extends Geodetic<CartesianCS> {
-        /** Creates a new builder (invoked by reflection). */
+        /** Creates a new builder (invoked by lambda function). */
         public Projected() {
             super((byte) 2);
         }
 
+        /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
+        @Override void candidateCS() {
+            if (isPredefined(Units.METRE)) {
+                coordinateSystem = DEFAULT.universal(0,0).getCoordinateSystem();
+            }
+        }
+
         /** Creates the two- or three-dimensional {@link CartesianCS} from given axes. */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
             if (axes.length > 2) {
@@ -358,11 +465,30 @@ previous:   for (int i=components.size(); --i >= 0;) {
      * Used for mean sea level (not for ellipsoidal height).
      */
     private static final class Vertical extends CRSBuilder<VerticalDatum, VerticalCS> {
-        /** Creates a new builder (invoked by reflection). */
+        /** Creates a new builder (invoked by lambda function). */
         public Vertical() {
             super(VerticalDatum.class, "Mean Sea Level", (byte) 1, (byte) 1, (byte) 1);
         }
 
+        /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
+        @Override void candidateCS() {
+            final Axis axis = getFirstAxis();
+            final Unit<?> unit = axis.getUnit();
+            final CommonCRS.Vertical predefined;
+            if (Units.METRE.equals(unit)) {
+                if (AxisDirection.UP.equals(axis.direction)) {
+                    predefined = CommonCRS.Vertical.MEAN_SEA_LEVEL;
+                } else {
+                    predefined = CommonCRS.Vertical.DEPTH;
+                }
+            } else if (Units.HECTOPASCAL.equals(unit)) {
+                predefined = CommonCRS.Vertical.BAROMETRIC;
+            } else {
+                return;
+            }
+            coordinateSystem = predefined.crs().getCoordinateSystem();
+        }
+
         /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on Mean Sea Level"</cite>. */
         @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
             datum = factory.createVerticalDatum(properties, VerticalDatumType.GEOIDAL);
@@ -384,23 +510,48 @@ previous:   for (int i=components.size(); --i >= 0;) {
      * in a special way since it contains the time origin.
      */
     private static final class Temporal extends CRSBuilder<TemporalDatum, TimeCS> {
-        /** Creates a new builder (invoked by reflection). */
+        /** Creates a new builder (invoked by lambda function). */
         public Temporal() {
-            super(TemporalDatum.class, null, (byte) 2, (byte) 1, (byte) 1);
+            super(TemporalDatum.class, "", (byte) 2, (byte) 1, (byte) 1);
+        }
+
+        /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
+        @Override void candidateCS() {
+            final Axis axis = getFirstAxis();
+            final Unit<?> unit = axis.getUnit();
+            final CommonCRS.Temporal predefined;
+            if (Units.DAY.equals(unit)) {
+                predefined = CommonCRS.Temporal.JULIAN;
+            } else if (Units.SECOND.equals(unit)) {
+                predefined = CommonCRS.Temporal.UNIX;
+            } else if (Units.MILLISECOND.equals(unit)) {
+                predefined = CommonCRS.Temporal.JAVA;
+            } else {
+                return;
+            }
+            coordinateSystem = predefined.crs().getCoordinateSystem();
         }
 
         /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on …"</cite>. */
         @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
-            throw new UnsupportedOperationException();  // TODO
+            final Instant epoch = getFirstAxis().coordinates.getEpoch();
+            final CommonCRS.Temporal c = CommonCRS.Temporal.forEpoch(epoch);
+            if (c != null) {
+                datum = c.datum();
+            } else {
+                properties = properties("Time since " + epoch);
+                datum = factory.createTemporalDatum(properties, (epoch != null) ? Date.from(epoch) : null);
+            }
         }
 
-         /** Creates the one-dimensional {@link TimeCS} from given axes. */
+        /** Creates the one-dimensional {@link TimeCS} from given axes. */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
-            throw new UnsupportedOperationException();  // TODO
+            coordinateSystem = factory.createTimeCS(properties, axes[0]);
         }
 
         /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */
         @Override SingleCRS createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
+            properties = properties(getFirstAxis().coordinates.getUnitsString());
             return factory.createTemporalCRS(properties, datum, coordinateSystem);
         }
     };
@@ -409,17 +560,21 @@ previous:   for (int i=components.size(); --i >= 0;) {
      * Unknown CRS with (x,y,z) axes.
      */
     private static final class Engineering extends CRSBuilder<EngineeringDatum, AffineCS> {
-        /** Creates a new builder (invoked by reflection). */
+        /** Creates a new builder (invoked by lambda function). */
         public Engineering() {
             super(EngineeringDatum.class, "affine coordinate system", (byte) 3, (byte) 2, (byte) 3);
         }
 
+        /** No-op since we have no predefined engineering CRS. */
+        @Override void candidateCS() {
+        }
+
         /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on affine coordinate system"</cite>. */
         @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
             datum = factory.createEngineeringDatum(properties);
         }
 
-         /** Creates two- or three-dimensional {@link AffineCS} from given axes. */
+        /** Creates two- or three-dimensional {@link AffineCS} from given axes. */
         @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
             if (axes.length > 2) {
                 coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1], axes[2]);
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
index 06d168e..3b01060 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
@@ -18,10 +18,15 @@ package org.apache.sis.internal.netcdf;
 
 import java.util.Locale;
 import java.util.Collection;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
 import java.io.IOException;
 import java.awt.image.DataBuffer;
+import java.time.Instant;
 import javax.measure.Unit;
 import javax.measure.format.ParserException;
+import java.time.format.DateTimeParseException;
+import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
 import org.apache.sis.storage.DataStoreException;
@@ -40,6 +45,11 @@ import org.apache.sis.util.resources.Errors;
  */
 public abstract class Variable extends NamedElement {
     /**
+     * The pattern to use for parsing temporal units of the form "days since 1970-01-01 00:00:00".
+     */
+    private static final Pattern TIME_PATTERN = Pattern.compile("(.+)\\Wsince\\W(.+)", Pattern.CASE_INSENSITIVE);
+
+    /**
      * Minimal number of dimension for accepting a variable as a coverage variable.
      */
     public static final int MIN_DIMENSION = 2;
@@ -52,6 +62,12 @@ public abstract class Variable extends NamedElement {
     private Unit<?> unit;
 
     /**
+     * If the unit is a temporal unit of the form "days since 1970-01-01 00:00:00", the epoch.
+     * Otherwise {@code null}.
+     */
+    private Instant epoch;
+
+    /**
      * Whether an attempt to parse the unit has already be done. This is used for avoiding
      * to report the same failure many times when {@link #unit} stay null.
      */
@@ -116,10 +132,20 @@ public abstract class Variable extends NamedElement {
     public final Unit<?> getUnit() {
         if (!unitParsed) {
             unitParsed = true;                          // Set first for avoiding to report errors many times.
-            final String symbols = getUnitsString();
+            String symbols = getUnitsString();
             if (symbols != null && !symbols.isEmpty()) try {
+                final Matcher parts = TIME_PATTERN.matcher(symbols);
+                if (parts.matches()) {
+                    /*
+                     * If we enter in this block, the unit is of the form "days since 1970-01-01 00:00:00".
+                     * The TIME_PATTERN splits the string in two parts, "days" and "1970-01-01 00:00:00".
+                     * The parse method will replace the space between date and time by 'T' letter.
+                     */
+                    epoch = StandardDateFormat.parseInstantUTC(parts.group(2));
+                    symbols = parts.group(1);
+                }
                 unit = Units.valueOf(symbols);
-            } catch (ParserException ex) {
+            } catch (ParserException | DateTimeParseException ex) {
                 warning(listeners, Variable.class, "getUnit", ex, Errors.getResources(listeners.getLocale()),
                         Errors.Keys.CanNotAssignUnitToVariable_2, getName(), symbols);
             }
@@ -128,6 +154,18 @@ public abstract class Variable extends NamedElement {
     }
 
     /**
+     * Returns the epoch of the temporal unit, or {@code null} if none.
+     *
+     * @return the epoch, or {@code null}.
+     */
+    final Instant getEpoch() {
+        if (epoch == null) {
+            getUnit();          // Epoch calculation as a side-effect.
+        }
+        return epoch;
+    }
+
+    /**
      * Returns the variable data type.
      *
      * @return the variable data type, or {@link DataType#UNKNOWN} if unknown.


[sis] 02/03: Deprecate the DefaultTemporalCRS methods working on java.util.Date. Replaced by java.time.Instant.

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 feec75a19f44b8c3e895fdd804a783b6c199e13c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Nov 16 18:49:01 2018 +0100

    Deprecate the DefaultTemporalCRS methods working on java.util.Date. Replaced by java.time.Instant.
---
 .../internal/referencing/ServicesForMetadata.java  |  5 +-
 .../sis/referencing/crs/DefaultTemporalCRS.java    | 57 ++++++++++++++++++++--
 .../sis/internal/util/TemporalUtilities.java       | 10 ++++
 3 files changed, 66 insertions(+), 6 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
index 9a2000d..b563dd4 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
@@ -92,6 +92,7 @@ import org.apache.sis.internal.metadata.ReferencingServices;
 import org.apache.sis.internal.referencing.provider.Affine;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.util.Constants;
+import org.apache.sis.internal.util.TemporalUtilities;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.resources.Vocabulary;
@@ -231,8 +232,8 @@ public final class ServicesForMetadata extends ReferencingServices {
         final int dim = AxisDirections.indexOfColinear(crs.getCoordinateSystem(), temporalCRS.getCoordinateSystem());
         assert dim >= 0 : crs; // Should not fail since 'temporalCRS' has been extracted from 'crs' by the caller.
         final DefaultTemporalCRS converter = DefaultTemporalCRS.castOrCopy(temporalCRS);
-        target.setBounds(converter.toDate(envelope.getMinimum(dim)),
-                         converter.toDate(envelope.getMaximum(dim)));
+        target.setBounds(TemporalUtilities.toDate(converter.toInstant(envelope.getMinimum(dim))),
+                         TemporalUtilities.toDate(converter.toInstant(envelope.getMaximum(dim))));
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultTemporalCRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
index fe4657e..ae30ab5 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultTemporalCRS.java
@@ -18,6 +18,7 @@ package org.apache.sis.referencing.crs;
 
 import java.util.Map;
 import java.util.Date;
+import java.time.Instant;
 import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
@@ -42,8 +43,8 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
  * The Apache SIS implementation provides the following methods in addition to the OGC/ISO properties:
  *
  * <ul>
- *   <li>{@link #toDate(double)} for converting a temporal position to a {@link Date}.</li>
- *   <li>{@link #toValue(Date)} for converting a {@link Date} to a temporal position.</li>
+ *   <li>{@link #toInstant(double)} for converting a temporal position to a {@link Date}.</li>
+ *   <li>{@link #toValue(Instant)} for converting a {@link Instant} to a temporal position.</li>
  * </ul>
  *
  * <p><b>Used with datum type:</b>
@@ -58,7 +59,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
  * in the javadoc, this condition holds if all components were created using only SIS factories and static constants.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see org.apache.sis.referencing.datum.DefaultTemporalDatum
  * @see org.apache.sis.referencing.cs.DefaultTimeCS
@@ -209,7 +210,7 @@ public class DefaultTemporalCRS extends AbstractCRS implements TemporalCRS {
     }
 
     /**
-     * Initialize the fields required for {@link #toDate} and {@link #toValue} operations.
+     * Initialize the fields required for {@link #toInstant(double)} and {@link #toValue(Instant)} operations.
      */
     private void initializeConverter() {
         origin   = datum.getOrigin().getTime();
@@ -257,6 +258,27 @@ public class DefaultTemporalCRS extends AbstractCRS implements TemporalCRS {
     }
 
     /**
+     * Convert the given value into an instant object.
+     * If the given value is {@link Double#NaN NaN} or infinite, then this method returns {@code null}.
+     *
+     * <p>This method is the converse of {@link #toValue(Instant)}.</p>
+     *
+     * @param  value  a value in this axis unit.
+     * @return the value as an instant, or {@code null} if the given value is NaN or infinite.
+     *
+     * @since 1.0
+     */
+    public Instant toInstant(final double value) {
+        if (Double.isNaN(value) || Double.isInfinite(value)) {
+            return null;
+        }
+        if (toMillis == null) {
+            initializeConverter();
+        }
+        return Instant.ofEpochMilli(Math.round(toMillis.convert(value)) + origin);
+    }
+
+    /**
      * Convert the given value into a {@link Date} object.
      * If the given value is {@link Double#NaN NaN} or infinite, then this method returns {@code null}.
      *
@@ -264,7 +286,10 @@ public class DefaultTemporalCRS extends AbstractCRS implements TemporalCRS {
      *
      * @param  value  a value in this axis unit.
      * @return the value as a {@linkplain Date date}, or {@code null} if the given value is NaN or infinite.
+     *
+     * @deprecated Replaced by {@link #toInstant(double)}.
      */
+    @Deprecated
     public Date toDate(final double value) {
         if (Double.isNaN(value) || Double.isInfinite(value)) {
             return null;
@@ -276,6 +301,27 @@ public class DefaultTemporalCRS extends AbstractCRS implements TemporalCRS {
     }
 
     /**
+     * Convert the given instant into a value in this axis unit.
+     * If the given instant is {@code null}, then this method returns {@link Double#NaN NaN}.
+     *
+     * <p>This method is the converse of {@link #toInstant(double)}.</p>
+     *
+     * @param  time  the value as an instant, or {@code null}.
+     * @return the value in this axis unit, or {@link Double#NaN NaN} if the given instant is {@code null}.
+     *
+     * @since 1.0
+     */
+    public double toValue(final Instant time) {
+        if (time == null) {
+            return Double.NaN;
+        }
+        if (toMillis == null) {
+            initializeConverter();
+        }
+        return toMillis.inverse().convert(time.toEpochMilli() - origin);
+    }
+
+    /**
      * Convert the given {@linkplain Date date} into a value in this axis unit.
      * If the given time is {@code null}, then this method returns {@link Double#NaN NaN}.
      *
@@ -283,7 +329,10 @@ public class DefaultTemporalCRS extends AbstractCRS implements TemporalCRS {
      *
      * @param  time  the value as a {@linkplain Date date}, or {@code null}.
      * @return the value in this axis unit, or {@link Double#NaN NaN} if the given time is {@code null}.
+     *
+     * @deprecated Replaced by {@link #toValue(Instant)}.
      */
+    @Deprecated
     public double toValue(final Date time) {
         if (time == null) {
             return Double.NaN;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/TemporalUtilities.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/TemporalUtilities.java
index 9e55e7f..f741f2a 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/TemporalUtilities.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/TemporalUtilities.java
@@ -124,4 +124,14 @@ public final class TemporalUtilities extends Static {
         }
         return instant.getDate();
     }
+
+    /**
+     * Temporary method, to be removed after we upgraded metadata to {@link java.time}.
+     *
+     * @param  instant  the Java instant, or {@code null}.
+     * @return the legacy Java date, or {@code null}.
+     */
+    public static Date toDate(final java.time.Instant instant) {
+        return (instant != null) ? Date.from(instant) : null;
+    }
 }