sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/03: Support time axis and leverage pre-defined coordinate system when possible.
Date Fri, 16 Nov 2018 19:31:14 GMT
This is an automated email from the ASF dual-hosted git repository.

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

commit 3613eef6ec225781624e3d36fa722c8411335abe
Author: Martin Desruisseaux <martin.desruisseaux@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.


Mime
View raw message