sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Share more code between `FeatureSet` and `Grid` regarding axis type inference.
Date Fri, 16 Oct 2020 18:09:28 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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 7a1fec6  Share more code between `FeatureSet` and `Grid` regarding axis type inference.
7a1fec6 is described below

commit 7a1fec6d1752af9a493bcb82f1f611a8363247d1
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri Oct 16 20:08:13 2020 +0200

    Share more code between `FeatureSet` and `Grid` regarding axis type inference.
---
 .../apache/sis/feature/EnvelopeOperationTest.java  |   2 +-
 .../java/org/apache/sis/internal/util/Strings.java |   9 +-
 .../org/apache/sis/internal/util/StringsTest.java  |   8 +-
 .../storage/earthobservation/LandsatReader.java    |   2 +-
 .../org/apache/sis/storage/geotiff/CRSBuilder.java |   2 +-
 .../org/apache/sis/internal/netcdf/AxisType.java   | 180 +++++++++++++++++
 .../org/apache/sis/internal/netcdf/FeatureSet.java | 217 ++++++++++++---------
 .../org/apache/sis/internal/netcdf/Resources.java  |  10 +-
 .../sis/internal/netcdf/Resources.properties       |   2 +-
 .../sis/internal/netcdf/Resources_fr.properties    |   2 +-
 .../org/apache/sis/internal/netcdf/Variable.java   |   7 +
 .../apache/sis/internal/netcdf/impl/GridInfo.java  | 109 +----------
 .../sis/internal/netcdf/impl/VariableInfo.java     |   3 +-
 .../sis/internal/netcdf/ucar/GridWrapper.java      |  10 +-
 .../sis/internal/netcdf/ucar/VariableWrapper.java  |  17 ++
 15 files changed, 363 insertions(+), 217 deletions(-)

diff --git a/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java b/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
index d58e018..a554a37 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
@@ -86,7 +86,7 @@ public final strictfp class EnvelopeOperationTest extends TestCase {
      * if the corresponding {@code declareCRS} flag is true. The first geometry will be the default one.
      *
      * @param defaultCRS1        default CRS of first property (may be {@code null}).
-     * @param defaultCRS1        default CRS of second property (may be {@code null}).
+     * @param defaultCRS2        default CRS of second property (may be {@code null}).
      * @param asCharacteristic1  whether to declare CRS 1 as a characteristic of first property.
      * @param asCharacteristic2  whether to declare CRS 2 as a characteristic of second property.
      */
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
index 1167d1f..2acad54 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
@@ -196,9 +196,11 @@ public final class Strings extends Static {
      *
      * @param  text     the text to filter.
      * @param  filter   the filter to apply.
+     * @param  all      {@code true} for making all the string in upper-cases,
+     *                  or {@code false} for changing only the first character.
      * @return the filtered text.
      */
-    public static String toUpperCase(final String text, final Characters.Filter filter) {
+    public static String toUpperCase(final String text, final Characters.Filter filter, final boolean all) {
         final int length = text.length();
         int c, i = 0;
         while (true) {
@@ -206,7 +208,8 @@ public final class Strings extends Static {
                 return text;
             }
             c = text.codePointAt(i);
-            if (!filter.contains(c) || Character.toUpperCase(c) != c) {
+            if (!filter.contains(c)) break;
+            if ((i == 0 | all) && Character.toUpperCase(c) != c) {
                 break;
             }
             i += Character.charCount(c);
@@ -219,7 +222,7 @@ public final class Strings extends Static {
         while (i < length) {
             c = text.codePointAt(i);
             if (filter.contains(c)) {
-                buffer.appendCodePoint(Character.toUpperCase(c));
+                buffer.appendCodePoint((i == 0 | all) ? Character.toUpperCase(c) : c);
             }
             i += Character.charCount(c);
         }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java
index 3b8e76f..96d12c6 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/StringsTest.java
@@ -41,13 +41,13 @@ public final strictfp class StringsTest extends TestCase {
     }
 
     /**
-     * Tests the {@link Strings#toUpperCase(String, Characters.Filter)} method.
+     * Tests the {@link Strings#toUpperCase(String, Characters.Filter, boolean)} method.
      */
     @Test
     public void testToUpperCase() {
         final String expected = "WGS84";
-        assertSame  (expected, Strings.toUpperCase(expected, Characters.Filter.LETTERS_AND_DIGITS));
-        assertEquals(expected, Strings.toUpperCase("WGS 84", Characters.Filter.LETTERS_AND_DIGITS));
-        assertEquals(expected, Strings.toUpperCase("wgs 84", Characters.Filter.LETTERS_AND_DIGITS));
+        assertSame  (expected, Strings.toUpperCase(expected, Characters.Filter.LETTERS_AND_DIGITS, true));
+        assertEquals(expected, Strings.toUpperCase("WGS 84", Characters.Filter.LETTERS_AND_DIGITS, true));
+        assertEquals(expected, Strings.toUpperCase("wgs 84", Characters.Filter.LETTERS_AND_DIGITS, true));
     }
 }
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
index 0e50216..58983f1 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
@@ -748,7 +748,7 @@ final class LandsatReader extends MetadataBuilder {
              * We ignore the "ELLIPSOID" attribute because it is implied by the datum.
              */
             case "DATUM": {
-                datum = CommonCRS.valueOf(Strings.toUpperCase(value, Characters.Filter.LETTERS_AND_DIGITS));
+                datum = CommonCRS.valueOf(Strings.toUpperCase(value, Characters.Filter.LETTERS_AND_DIGITS, true));
                 break;
             }
             /*
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
index 125df3d..82d1dd6 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
@@ -995,7 +995,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                 final Ellipsoid     ellipsoid = createEllipsoid(names, linearUnit);
                 final PrimeMeridian meridian  = createPrimeMeridian(names, angularUnit);
                 final GeodeticDatum datum     = getDatumFactory().createGeodeticDatum(properties(name), ellipsoid, meridian);
-                name = Strings.toUpperCase(name, Characters.Filter.LETTERS_AND_DIGITS);
+                name = Strings.toUpperCase(name, Characters.Filter.LETTERS_AND_DIGITS, true);
                 lastName = datum.getName();
                 try {
                     final GeodeticDatum predefined = CommonCRS.valueOf(name).datum();
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/AxisType.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/AxisType.java
new file mode 100644
index 0000000..2333557
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/AxisType.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.netcdf;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Locale;
+import org.opengis.referencing.cs.AxisDirection;
+import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.measure.Units;
+import javax.measure.Unit;
+import ucar.nc2.constants.CF;
+
+
+/**
+ * Type of coordinate system axis, in the order they should appears for a "normalized" coordinate reference system.
+ * The enumeration name matches the name of the {@code "axis"} attribute in CF-convention.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public enum AxisType {
+    /**
+     * X (usually longitude) coordinate axis.
+     */
+    X,
+
+    /**
+     * Y (usually latitude) coordinate axis.
+     */
+    Y,
+
+    /**
+     * Z (usually height) coordinate axis.
+     */
+    Z,
+
+    /**
+     * Time coordinate axis.
+     */
+    T;
+
+    /**
+     * Mapping from values of the {@code "_CoordinateAxisType"} attribute or axis name to the abbreviation.
+     * Keys are lower cases and values are controlled vocabulary documented in {@link Axis#abbreviation}.
+     *
+     * <div class="note">"GeoX" and "GeoY" stands for projected coordinates, not geocentric coordinates
+     * (<a href="https://www.unidata.ucar.edu/software/thredds/current/netcdf-java/reference/CoordinateAttributes.html#AxisTypes">source</a>).
+     * </div>
+     *
+     * @see abbreviation(String)
+     */
+    private static final Map<String,Character> TYPES = new HashMap<>(26);
+
+    /**
+     * The enumeration values for given abbreviations.
+     */
+    private static final Map<Character,AxisType> VALUES = new HashMap<>(13);
+    static {
+        addAxisTypes(X, 'λ', "longitude", "lon", "long");
+        addAxisTypes(Y, 'φ', "latitude",  "lat");
+        addAxisTypes(Z, 'H', "pressure", "height", "altitude", "barometric_altitude", "elevation", "elev", "geoz");
+        addAxisTypes(Z, 'D', "depth", "depth_below_geoid");
+        addAxisTypes(X, 'E', "geox", "projection_x_coordinate");
+        addAxisTypes(Y, 'N', "geoy", "projection_y_coordinate");
+        addAxisTypes(T, 't', "t", "time", "runtime");
+        addAxisTypes(X, 'x', "x");
+        addAxisTypes(Y, 'y', "y");
+        addAxisTypes(Z, 'z', "z");
+    }
+
+    /**
+     * Adds a sequence of axis types or variable names for the given abbreviation.
+     */
+    private static void addAxisTypes(final AxisType value, final char abbreviation, final String... names) {
+        final Character c = abbreviation;
+        for (final String name : names) {
+            TYPES.put(name, c);
+        }
+        VALUES.put(c, value);
+    }
+
+    /**
+     * Returns the axis type (identified by its abbreviation) for an axis of the given name, or null if unknown.
+     * The returned code is one of the controlled vocabulary documented in {@link Axis#abbreviation}.
+     *
+     * @param  type  the {@code "_CoordinateAxisType"} attribute value or another description used as fallback.
+     * @return axis abbreviation for the given type or name, or {@code null} if none.
+     */
+    private static Character abbreviation(final String type) {
+        return (type != null) ? TYPES.get(type.toLowerCase(Locale.US)) : null;
+    }
+
+    /**
+     * Returns the axis type (identified by its abbreviation) for the given axis, or 0 if unknown.
+     * The returned code is one of the controlled vocabulary documented in {@link Axis#abbreviation}.
+     *
+     * @param  axis  axis for which to get an abbreviation.
+     * @return abbreviation for the given axis, or 0 if none.
+     */
+    public static char abbreviation(final Variable axis) {
+        /*
+         * In Apache SIS implementation, the abbreviation determines the axis type. If a "_CoordinateAxisType" attribute
+         * exists, il will have precedence over all other heuristic rules in this method because it is the most specific
+         * information about axis type. Otherwise the "standard_name" attribute is our first fallback since valid values
+         * are standardized to "longitude" and "latitude" among others.
+         */
+        Character abbreviation = abbreviation(axis.getAxisType());
+        if (abbreviation == null) {
+            abbreviation = abbreviation(axis.getAttributeAsString(CF.STANDARD_NAME));    // No fallback on variable name.
+            /*
+             * If the abbreviation is still unknown, look at the "long_name", "description" or "title" attribute. Those
+             * attributes are not standardized, so they are less reliable than "standard_name". But they are still more
+             * reliable that the variable name since the long name may be "Longitude" or "Latitude" while the variable
+             * name is only "x" or "y".
+             */
+            if (abbreviation == null) {
+                abbreviation = abbreviation(axis.getDescription());
+                if (abbreviation == null) {
+                    /*
+                     * Actually the "degree_east" and "degree_north" units of measurement are the most reliable way to
+                     * identify geographic system, but we nevertheless check them almost last because the direction is
+                     * already verified by Axis constructor. By checking the variable attributes first, we give a chance
+                     * to Axis constructor to report a warning if there is an inconsistency.
+                     */
+                    if (Units.isAngular(axis.getUnit())) {
+                        final AxisDirection direction = AxisDirections.absolute(Axis.direction(axis.getUnitsString()));
+                        if (AxisDirection.EAST.equals(direction)) {
+                            return 'λ';
+                        } else if (AxisDirection.NORTH.equals(direction)) {
+                            return 'φ';
+                        }
+                    }
+                    /*
+                     * We test the variable name last because that name is more at risk of being an uninformative "x" or "y" name.
+                     * If even the variable name is not sufficient, we use some easy to recognize units.
+                     */
+                    if (abbreviation == null) {
+                        abbreviation = abbreviation(axis.getName());
+                        if (abbreviation == null) {
+                            final Unit<?> unit = axis.getUnit();
+                            if (Units.isTemporal(unit)) {
+                                return 't';
+                            } else if (Units.isPressure(unit)) {
+                                return 'z';
+                            } else {
+                                return 0;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return abbreviation;
+    }
+
+    /**
+     * Returns the enumeration value for the given variable, or {@code null} if none.
+     */
+    static AxisType valueOf(final Variable axis) {
+        final char abbreviation = abbreviation(axis);
+        return (abbreviation != 0) ? VALUES.get(abbreviation) : null;
+    }
+}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
index aa68567..c076d2c 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
@@ -20,6 +20,8 @@ import java.util.Map;
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Spliterator;
 import java.util.stream.Stream;
@@ -31,11 +33,13 @@ import org.opengis.metadata.acquisition.GeometryType;
 import org.apache.sis.math.Vector;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.internal.feature.MovingFeatures;
+import org.apache.sis.internal.util.Strings;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.feature.builder.AttributeRole;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.feature.builder.AttributeTypeBuilder;
 import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.Characters;
 import ucar.nc2.constants.CF;
 
 // Branch-dependent imports
@@ -55,7 +59,8 @@ import org.opengis.feature.FeatureType;
  */
 final class FeatureSet extends DiscreteSampling {
     /**
-     * Kind of features supported by this class. Also used as property name.
+     * Value of {@code "featureType"} global attribute for netCDF files that this class can handle.
+     * Also used as property name for the geometry object.
      */
     static final String TRAJECTORY = "trajectory";
 
@@ -108,7 +113,12 @@ final class FeatureSet extends DiscreteSampling {
      * Creates a new discrete sampling parser for features identified by the given variable.
      * All arrays given to this method are stored by direct reference (they are not cloned).
      *
+     * <p>The {@code name} argument can be anything. A not-too-bad choice (when nothing better is available)
+     * is the name of the first dimension of {@code coordinates} and {@code properties} variables. All those
+     * variables should have that first dimension in common, because {@code create(…)} uses that criterion.</p>
+     *
      * @param  decoder      the source of the features to create.
+     * @param  name         name to give to the feature type.
      * @param  counts       the count of instances per feature, or {@code null} if none.
      * @param  identifiers  the feature identifiers, possibly with other singleton properties.
      * @param  hasTime      whether the {@code coordinates} array contains a temporal variable.
@@ -116,8 +126,8 @@ final class FeatureSet extends DiscreteSampling {
      * @param  properties   the variables that contain custom time-varying properties.
      * @throws IllegalArgumentException if the given library is non-null but not available.
      */
-    private FeatureSet(final Decoder decoder, final Vector counts, final Variable[] identifiers,
-            final boolean hasTime, final Variable[] coordinates, final Variable[] properties)
+    private FeatureSet(final Decoder decoder, String name, final Vector counts, final Variable[] identifiers,
+                       final boolean hasTime, final Variable[] coordinates, final Variable[] properties)
     {
         super(decoder.geomlib, decoder.listeners);
         this.counts      = counts;
@@ -153,11 +163,18 @@ final class FeatureSet extends DiscreteSampling {
             final Class<?> type = v.isEnumeration() ? String.class : Number.class;
             describe(v, builder.addAttribute(type).setMaximumOccurs(Integer.MAX_VALUE), hasTime);
         }
-        type = builder.setName("Features").build();     // TODO: get a better name.
+        /*
+         * By default, `name` is a netCDF dimension name (see method javadoc), usually all lower-cases.
+         * Make the first letter upper-case for consistency with SIS convention used for feature types.
+         */
+        name = Strings.toUpperCase(name, Characters.Filter.UNICODE_IDENTIFIER, false);
+        type = builder.setName(name).build();
     }
 
     /**
      * Sets the attribute name, and potentially its definition, from the given variable.
+     * If the variable has a {@code "cf_role"} attribute set {@code "trajectory_id"},
+     * then the attribute will also be declared as an identifier.
      *
      * @param  variable   the variable from which to get metadata.
      * @param  attribute  the attribute to configure with variable metadata.
@@ -173,19 +190,15 @@ final class FeatureSet extends DiscreteSampling {
         if (hasTime) {
             attribute.addCharacteristic(MovingFeatures.TIME);
         }
-    }
-
-    /**
-     * Returns {@code true} if the given attribute value is one of the {@code cf_role} attribute values
-     * supported by this implementation.
-     */
-    private static boolean hasSupportedRole(final Variable variable) {
-        return CF.TRAJECTORY_ID.equalsIgnoreCase(variable.getAttributeAsString(CF.CF_ROLE));
+        if (CF.TRAJECTORY_ID.equalsIgnoreCase(variable.getAttributeAsString(CF.CF_ROLE))) {
+            attribute.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+        }
     }
 
     /**
      * Creates new discrete sampling parsers from the attribute values found in the given decoder.
      *
+     * @param  decoder  the source of the features to create.
      * @throws IllegalArgumentException if the geometric object library is not available.
      * @throws ArithmeticException if the size of a variable exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
      */
@@ -196,7 +209,7 @@ search: for (final Variable counts : decoder.getVariables()) {
              * Any one-dimensional integer variable having a "sample_dimension" attribute string value
              * will be taken as an indication that we have Discrete Sampling Geometries. That variable
              * shall be counting the number of feature instances, and another variable having the same
-             * dimension (optionally plus a character dimension) shall give the feature identifiers.
+             * dimension (optionally plus a character dimension) should give the feature identifiers.
              * Example:
              *
              *     dimensions:
@@ -208,90 +221,112 @@ search: for (final Variable counts : decoder.getVariables()) {
              *         int counts(identifiers);
              *             counts:sample_dimension = "points";
              */
-            if (counts.getNumDimensions() != 1 || !counts.getDataType().isInteger) {
-                continue;
-            }
-            final String sampleDimName = counts.getAttributeAsString(CF.SAMPLE_DIMENSION);
-            if (sampleDimName == null) {
-                continue;
+            if (counts.getNumDimensions() == 1 && counts.getDataType().isInteger) {
+                final String sampleDimName = counts.getAttributeAsString(CF.SAMPLE_DIMENSION);
+                if (sampleDimName != null) {
+                    final Dimension sampleDimension = decoder.findDimension(sampleDimName);
+                    if (sampleDimension != null) {
+                        addFeatureSet(features, decoder, counts, counts.getGridDimensions().get(0), sampleDimension);
+                    } else {
+                        decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DimensionNotFound_3,
+                                                  decoder.getFilename(), counts.getName(), sampleDimName));
+                    }
+                }
             }
-            final Dimension featureDimension = counts.getGridDimensions().get(0);
-            final Dimension sampleDimension = decoder.findDimension(sampleDimName);
-            if (sampleDimension == null) {
-                decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DimensionNotFound_3,
-                                          decoder.getFilename(), counts.getName(), sampleDimName));
+        }
+        return features.toArray(new FeatureSet[features.size()]);
+    }
+
+    /**
+     * Searches all variables having the expected feature dimension or sample dimension.
+     * Those variable contains the actual data. For example if the sample dimension name
+     * is "points", then we may have:
+     *
+     * {@preformat text
+     *     double longitude(points);
+     *         longitude:axis = "X";
+     *         longitude:standard_name = "longitude";
+     *         longitude:units = "degrees_east";
+     *     double latitude(points);
+     *         latitude:axis = "Y";
+     *         latitude:standard_name = "latitude";
+     *         latitude:units = "degrees_north";
+     *     double time(points);
+     *         time:axis = "T";
+     *         time:standard_name = "time";
+     *         time:units = "minutes since 2014-11-29 00:00:00";
+     *     short myCustomProperty(points);
+     * }
+     *
+     * @param  features          where to add the {@code FeatureSet} instance.
+     * @param  decoder           the source of the features to create.
+     * @param  counts            the count of instances per feature, or {@code null} if none.
+     * @param  featureDimension  dimension of properties having a single value per feature instance.
+     * @param  sampleDimension   dimension of properties having multiple values per feature instance.
+     * @return whether a {@code FeatureSet} has been added to the {@code features} collection.
+     */
+    private static boolean addFeatureSet(final List<FeatureSet> features, final Decoder decoder, final Variable counts,
+            final Dimension featureDimension, final Dimension sampleDimension) throws IOException, DataStoreException
+    {
+        final String                 featureName = featureDimension.getName();
+        final boolean                isPointSet  = sampleDimension.equals(featureDimension);
+        final List<Variable>         singletons  = isPointSet ? Collections.emptyList() : new ArrayList<>();
+        final List<Variable>         properties  = new ArrayList<>();
+        final Map<AxisType,Variable> coordinates = new LinkedHashMap<>();
+        for (final Variable data : decoder.getVariables()) {
+            if (data.equals(counts)) {
                 continue;
             }
             /*
-             * We should have another variable of the same name than the feature dimension name
-             * ("identifiers" in above example). That variable should have a "cf_role" attribute
-             * set to one of the values known to current implementation.  If we do not find such
-             * variable, search among other variables before to give up. That second search is not
-             * part of CF convention and will be accepted only if there is no ambiguity.
+             * We should have another variable of the same name than the feature dimension name.
+             * In SIS implementation, this variable is optional. But if present, it should have
+             * the expected dimension. According CF convention that variable should also have a
+             * "cf_role" attribute set to "trajectory_id", but this is not required by SIS.
              */
-            Variable identifiers = decoder.findVariable(featureDimension.getName());
-            if (identifiers != null && hasSupportedRole(identifiers)) {
-                if (!isScalarOrString(identifiers, featureDimension, decoder)) {
-                    continue;
-                }
-            } else {
-                identifiers = null;
-                for (final Variable alt : decoder.getVariables()) {
-                    if (hasSupportedRole(alt) && isScalarOrString(alt, featureDimension, null)) {
-                        if (identifiers != null) {
-                            identifiers = null;
-                            break;                  // Ambiguity found: consider that we found no replacement.
-                        }
-                        identifiers = alt;
-                    }
+            if (featureName.equalsIgnoreCase(data.getName())) {
+                if (isScalarOrString(data, featureDimension, decoder)) {
+                    (isPointSet ? properties : singletons).add(data);
                 }
-                if (identifiers == null) {
-                    decoder.listeners.warning(decoder.resources().getString(Resources.Keys.VariableNotFound_2,
-                                              decoder.getFilename(), featureDimension.getName()));
-                    continue;
-                }
-            }
-            /*
-             * At this point, all information have been verified as valid. Now search all variables having
-             * the expected sample dimension. Those variable contains the actual data. For example if the
-             * sample dimension name is "points", then we may have:
-             *
-             *     double longitude(points);
-             *         longitude:axis = "X";
-             *         longitude:standard_name = "longitude";
-             *         longitude:units = "degrees_east";
-             *     double latitude(points);
-             *         latitude:axis = "Y";
-             *         latitude:standard_name = "latitude";
-             *         latitude:units = "degrees_north";
-             *     double time(points);
-             *         time:axis = "T";
-             *         time:standard_name = "time";
-             *         time:units = "minutes since 2014-11-29 00:00:00";
-             *     short myCustomProperty(points);
-             */
-            final Map<String,Variable> coordinates = new LinkedHashMap<>();
-            final List<Variable> properties  = new ArrayList<>();
-            for (final Variable data : decoder.getVariables()) {
-                if (isScalarOrString(data, sampleDimension, null)) {
-                    final String axisType = data.getAttributeAsString(CF.AXIS);
-                    if (axisType == null) {
-                        properties.add(data);
-                    } else if (coordinates.put(axisType, data) != null) {
-                        continue search;    // Two axes of the same type: abort.
+            } else if (!isPointSet && isScalarOrString(data, featureDimension, null)) {
+                /*
+                 * Feature property other than identifiers. Should rarely happen.
+                 */
+                singletons.add(data);
+            } else if (isScalarOrString(data, sampleDimension, null)) {
+                /*
+                 * All other sample property (i.e. property having a value for each temporal value).
+                 * If `isPointSet` is true, then sample properties are actually feature properties.
+                 */
+                final AxisType axisType = AxisType.valueOf(data);
+                if (axisType != null) {
+                    final Variable previous = coordinates.putIfAbsent(axisType, data);
+                    if (previous != null) {
+                        decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DuplicatedAxisType_4,
+                                                  decoder.getFilename(), axisType, previous.getName(), data.getName()));
+                        // TODO: give precedence to which axis?
                     }
+                } else {
+                    properties.add(data);
                 }
             }
-            final Variable time = coordinates.remove("T");
-            if (time != null) {
-                coordinates.put("T", time);     // Make sure that time is last.
-                features.add(new FeatureSet(decoder, counts.read(),
-                             new Variable[] {identifiers}, true,
-                             coordinates.values().toArray(new Variable[coordinates.size()]),
-                             properties.toArray(new Variable[properties.size()])));
-            }
         }
-        return features.toArray(new FeatureSet[features.size()]);
+        final Variable time = coordinates.remove(AxisType.T);
+        if (time != null) {
+            coordinates.put(AxisType.T, time);      // Make sure that time is last.
+        }
+        return features.add(new FeatureSet(decoder, featureName,
+                            (counts != null) ? counts.read() : null,
+                            toArray(singletons), time != null,
+                            toArray(coordinates.values()),
+                            toArray(properties)));
+
+    }
+
+    /**
+     * Returns the content of given collection as an array.
+     */
+    private static Variable[] toArray(final Collection<Variable> variables) {
+        return variables.toArray(new Variable[variables.size()]);
     }
 
     /**
@@ -305,9 +340,9 @@ search: for (final Variable counts : decoder.getVariables()) {
      */
     @SuppressWarnings("fallthrough")
     private static boolean isScalarOrString(final Variable data, final Dimension featureDimension, final Decoder decoder) {
-        final List<Dimension> dimensions = data.getGridDimensions();
+        List<Dimension> dimensions = null;
         final int unexpectedDimension;
-        switch (dimensions.size()) {
+        switch (data.getNumDimensions()) {
             default: {                              // Too many dimensions
                 unexpectedDimension = 2;
                 break;
@@ -320,6 +355,7 @@ search: for (final Variable counts : decoder.getVariables()) {
                 // Fall through for checking the first dimension.
             }
             case 1: {
+                dimensions = data.getGridDimensions();
                 if (featureDimension.equals(dimensions.get(0))) {
                     return true;
                 }
@@ -331,6 +367,9 @@ search: for (final Variable counts : decoder.getVariables()) {
             }
         }
         if (decoder != null) {
+            if (dimensions == null) {
+                dimensions = data.getGridDimensions();
+            }
             decoder.listeners.warning(decoder.resources().getString(
                     Resources.Keys.UnexpectedDimensionForVariable_4,
                     decoder.getFilename(), data.getName(),
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
index f6acc63..9f9c4ee 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
@@ -116,6 +116,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short DimensionNotFound_3 = 1;
 
         /**
+         * Axes “{2}” and “{3}” have the same type “{1}” in netCDF file “{0}”.
+         */
+        public static final short DuplicatedAxisType_4 = 3;
+
+        /**
          * Duplicated axis “{1}” in a grid of netCDF file “{0}”.
          */
         public static final short DuplicatedAxis_2 = 7;
@@ -179,11 +184,6 @@ public final class Resources extends IndexedResourceBundle {
          * NetCDF file “{0}” uses unsupported data type {2} for variable “{1}”.
          */
         public static final short UnsupportedDataType_3 = 5;
-
-        /**
-         * Variable “{1}” is not found in the “{0}” file.
-         */
-        public static final short VariableNotFound_2 = 3;
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
index 62b864e..e6a6457 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
@@ -31,6 +31,7 @@ CanNotUseUCAR                     = Can not use UCAR library for netCDF format.
 ComputeLocalizationGrid_2         = Computed localization grid for \u201c{0}\u201d in {1} seconds.
 DimensionNotFound_3               = Dimension \u201c{2}\u201d declared by attribute \u201c{1}\u201d is not found in the \u201c{0}\u201d file.
 DuplicatedAxis_2                  = Duplicated axis \u201c{1}\u201d in a grid of netCDF file \u201c{0}\u201d.
+DuplicatedAxisType_4              = Axes \u201c{2}\u201d and \u201c{3}\u201d have the same type \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
 IllegalAttributeValue_3           = Illegal value \u201c{2}\u201d for attribute \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
 IllegalValueRange_4               = Illegal value range {2,number} \u2026 {3,number} for variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
 MismatchedAttributeLength_5       = Attributes \u201c{1}\u201d and \u201c{2}\u201d on variable \u201c{0}\u201d have different lengths: {3} and {4} respectively.
@@ -42,4 +43,3 @@ UnexpectedAxisCount_4             = Reference system of type \u2018{1}\u2019 can
 UnexpectedDimensionForVariable_4  = Variable \u201c{1}\u201d in file \u201c{0}\u201d has a dimension \u201c{3}\u201d while we expected \u201c{2}\u201d.
 UnmappedDimensions_4              = Variable \u201c{1}\u201d in file \u201c{0}\u201d has {2,number} dimensions but only {3,number} can be associated to a coordinate reference system.
 UnsupportedDataType_3             = NetCDF file \u201c{0}\u201d uses unsupported data type {2} for variable \u201c{1}\u201d.
-VariableNotFound_2                = Variable \u201c{1}\u201d is not found in the \u201c{0}\u201d file.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
index e9dd81c..624d10e 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
@@ -36,6 +36,7 @@ CanNotUseUCAR                     = Ne peut pas utiliser la biblioth\u00e8que de
 ComputeLocalizationGrid_2         = Grille de localisation de \u00ab\u202f{0}\u202f\u00bb calcul\u00e9e en {1} secondes.
 DimensionNotFound_3               = La dimension \u00ab\u202f{2}\u202f\u00bb d\u00e9clar\u00e9e par l\u2019attribut \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans le fichier \u00ab\u202f{0}\u202f\u00bb.
 DuplicatedAxis_2                  = Axe \u00ab\u202f{1}\u202f\u00bb dupliqu\u00e9 dans une grille du fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
+DuplicatedAxisType_4              = Les axes \u00ab\u202f{2}\u202f\u00bb et \u00ab\u202f{3}\u202f\u00bb d\u00e9clarent le m\u00eame type \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 IllegalAttributeValue_3           = La valeur \u00ab\u202f{2}\u202f\u00bb est ill\u00e9gale pour l\u2019attribut \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 IllegalValueRange_4               = Plage de valeurs {2,number} \u2026 {3,number} ill\u00e9gale pour la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 MismatchedAttributeLength_5       = Les attributs \u201c{1}\u201d et \u201c{2}\u201d de la variable \u201c{0}\u201d ont des longueurs diff\u00e9rentes\u00a0: {3} et {4} respectivement.
@@ -47,4 +48,3 @@ UnexpectedAxisCount_4             = Les syst\u00e8mes de r\u00e9f\u00e9rence de
 UnexpectedDimensionForVariable_4  = La variable \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb a une dimension \u00ab\u202f{3}\u202f\u00bb alors qu\u2019on attendait \u00ab\u202f{2}\u202f\u00bb.
 UnmappedDimensions_4              = La variable \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb a {2,number} dimensions mais seulement {3,number} peuvent \u00eatre associ\u00e9es \u00e0 un syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es.
 UnsupportedDataType_3             = Le fichier netCDF \u00ab\u202f{0}\u202f\u00bb utilise un type de donn\u00e9es non-support\u00e9 {2} pour la variable \u00ab\u202f{1}\u202f\u00bb.
-VariableNotFound_2                = La variable \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans le fichier \u00ab\u202f{0}\u202f\u00bb.
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 6285bd9..8f19cae 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
@@ -440,6 +440,13 @@ public abstract class Variable extends Node {
     protected abstract boolean isCoordinateSystemAxis();
 
     /**
+     * Returns the value of {@code "_CoordinateAxisType"} attribute.
+     *
+     * @return Value of {@code "_CoordinateAxisType"} attribute, or {@code null} if none.
+     */
+    protected abstract String getAxisType();
+
+    /**
      * Returns a builder for the grid geometry of this variable, or {@code null} if this variable is not a data cube.
      * Not all variables have a grid geometry. For example collections of features do not have such grid.
      * This method should be invoked only once per variable, but the same builder may be returned by different variables.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java
index e62ddbf..76ca226 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java
@@ -19,23 +19,17 @@ package org.apache.sis.internal.netcdf.impl;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.HashMap;
 import java.util.TreeMap;
 import java.util.SortedMap;
-import javax.measure.Unit;
-import org.opengis.referencing.cs.AxisDirection;
 import org.apache.sis.internal.netcdf.Axis;
+import org.apache.sis.internal.netcdf.AxisType;
 import org.apache.sis.internal.netcdf.Grid;
 import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.Dimension;
 import org.apache.sis.internal.netcdf.Resources;
-import org.apache.sis.internal.referencing.AxisDirections;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.measure.Units;
 import org.apache.sis.util.ArraysExt;
 import ucar.nc2.constants.CF;
 
@@ -54,40 +48,6 @@ import ucar.nc2.constants.CF;
  */
 final class GridInfo extends Grid {
     /**
-     * Mapping from values of the {@code "_CoordinateAxisType"} attribute or axis name to the abbreviation.
-     * Keys are lower cases and values are controlled vocabulary documented in {@link Axis#abbreviation}.
-     *
-     * <div class="note">"GeoX" and "GeoY" stands for projected coordinates, not geocentric coordinates
-     * (<a href="https://www.unidata.ucar.edu/software/thredds/current/netcdf-java/reference/CoordinateAttributes.html#AxisTypes">source</a>).
-     * </div>
-     *
-     * @see #getAxisType(String)
-     */
-    private static final Map<String,Character> AXIS_TYPES = new HashMap<>(26);
-    static {
-        addAxisTypes('λ', "longitude", "lon", "long");
-        addAxisTypes('φ', "latitude",  "lat");
-        addAxisTypes('H', "pressure", "height", "altitude", "barometric_altitude", "elevation", "elev", "geoz");
-        addAxisTypes('D', "depth", "depth_below_geoid");
-        addAxisTypes('E', "geox", "projection_x_coordinate");
-        addAxisTypes('N', "geoy", "projection_y_coordinate");
-        addAxisTypes('t', "t", "time", "runtime");
-        addAxisTypes('x', "x");
-        addAxisTypes('y', "y");
-        addAxisTypes('z', "z");
-    }
-
-    /**
-     * Adds a sequence of axis types or variable names for the given abbreviation.
-     */
-    private static void addAxisTypes(final char abbreviation, final String... names) {
-        final Character c = abbreviation;
-        for (final String name : names) {
-            AXIS_TYPES.put(name, c);
-        }
-    }
-
-    /**
      * Describes the input values expected by the function converting grid indices to geodetic coordinates.
      * They are the dimensions of the grid (<strong>not</strong> the dimensions of the CRS).
      * Dimensions are listed in the order they appear in netCDF file (reverse of "natural" order).
@@ -152,21 +112,6 @@ final class GridInfo extends Grid {
     }
 
     /**
-     * Returns the axis type for an axis of the given name, or 0 if unknown.
-     * If non-zero, then the returned code is one of the controlled vocabulary
-     * documented in {@link Axis#abbreviation}.
-     */
-    private static char getAxisType(final String name) {
-        if (name != null) {
-            final Character abbreviation = AXIS_TYPES.get(name.toLowerCase(Locale.US));
-            if (abbreviation != null) {
-                return abbreviation;
-            }
-        }
-        return 0;
-    }
-
-    /**
      * Returns the number of dimensions of source coordinates in the <cite>"grid to CRS"</cite> conversion.
      * This is the number of dimensions of the <em>grid</em>.
      */
@@ -255,56 +200,6 @@ next:       for (final String name : axisNames) {
             final int targetDim = entry.getValue();
             final VariableInfo axis = entry.getKey();
             /*
-             * In Apache SIS implementation, the abbreviation determines the axis type. If a "_coordinateaxistype" attribute
-             * exists, il will have precedence over all other heuristic rules in this method because it is the most specific
-             * information about axis type. Otherwise the "standard_name" attribute is our first fallback since valid values
-             * are standardized to "longitude" and "latitude" among others.
-             */
-            char abbreviation = getAxisType(axis.getAxisType());
-            if (abbreviation == 0) {
-                abbreviation = getAxisType(axis.getAttributeAsString(CF.STANDARD_NAME));    // No fallback on variable name.
-                /*
-                 * If the abbreviation is still unknown, look at the "long_name", "description" or "title" attribute. Those
-                 * attributes are not standardized, so they are less reliable than "standard_name". But they are still more
-                 * reliable that the variable name since the long name may be "Longitude" or "Latitude" while the variable
-                 * name is only "x" or "y".
-                 */
-                if (abbreviation == 0) {
-                    abbreviation = getAxisType(axis.getDescription());
-                    if (abbreviation == 0) {
-                        /*
-                         * Actually the "degree_east" and "degree_north" units of measurement are the most reliable way to
-                         * identify geographic system, but we nevertheless check them almost last because the direction is
-                         * already verified by Axis constructor. By checking the variable attributes first, we give a chance
-                         * to Axis constructor to report a warning if there is an inconsistency.
-                         */
-                        if (Units.isAngular(axis.getUnit())) {
-                            final AxisDirection direction = AxisDirections.absolute(Axis.direction(axis.getUnitsString()));
-                            if (AxisDirection.EAST.equals(direction)) {
-                                abbreviation = 'λ';
-                            } else if (AxisDirection.NORTH.equals(direction)) {
-                                abbreviation = 'φ';
-                            }
-                        }
-                        /*
-                         * We test the variable name last because that name is more at risk of being an uninformative "x" or "y" name.
-                         * If even the variable name is not sufficient, we use some easy to recognize units.
-                         */
-                        if (abbreviation == 0) {
-                            abbreviation = getAxisType(axis.getName());
-                            if (abbreviation == 0) {
-                                final Unit<?> unit = axis.getUnit();
-                                if (Units.isTemporal(unit)) {
-                                    abbreviation = 't';
-                                } else if (Units.isPressure(unit)) {
-                                    abbreviation = 'z';
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-            /*
              * Get the grid dimensions (part of the "domain" in UCAR terminology) used for computing
              * the coordinate values along the current axis. There is exactly 1 such grid dimension in
              * straightforward netCDF files. However some more complex files may have 2 dimensions.
@@ -322,7 +217,7 @@ next:       for (final String name : axisNames) {
                     }
                 }
             }
-            axes[targetDim] = new Axis(abbreviation, axis.getAttributeAsString(CF.POSITIVE),
+            axes[targetDim] = new Axis(AxisType.abbreviation(axis), axis.getAttributeAsString(CF.POSITIVE),
                                        ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i), axis);
         }
         return axes;
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 4f3f8c8..aab54bd 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
@@ -430,7 +430,8 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     /**
      * Returns the value of the {@code "_CoordinateAxisType"} attribute, or {@code null} if none.
      */
-    final String getAxisType() {
+    @Override
+    protected String getAxisType() {
         final Object value = getAttributeValue(_Coordinate.AxisType, "_coordinateaxistype");
         return (value instanceof String) ? (String) value : null;
     }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
index b8b6df1..bde4422 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
@@ -30,6 +30,7 @@ import ucar.nc2.dataset.CoordinateSystem;
 import org.apache.sis.internal.netcdf.Axis;
 import org.apache.sis.internal.netcdf.Grid;
 import org.apache.sis.internal.netcdf.Decoder;
+import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.util.ArraysExt;
@@ -42,7 +43,7 @@ import org.apache.sis.util.ArraysExt;
  * of the grid geometry information.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.3
  * @module
  */
@@ -234,6 +235,7 @@ next:       for (final String name : axisNames) {
         final Axis[] axes = new Axis[targetDim];
         while (--targetDim >= 0) {
             final CoordinateAxis axis = range.get(targetDim);
+            final Variable wrapper = ((DecoderWrapper) decoder).getWrapperFor(axis);
             /*
              * The AttributeNames are for ISO 19115 metadata. They are not used for locating grid cells
              * on Earth, but we nevertheless get them now for making MetadataReader work easier.
@@ -254,6 +256,9 @@ next:       for (final String name : axisNames) {
                 case RadialElevation: abbreviation = 'Ω'; break;    // Spherical latitude
                 case RadialDistance:  abbreviation = 'r'; break;    // Geocentric radius
             }
+            if (abbreviation == 0) {
+                abbreviation = org.apache.sis.internal.netcdf.AxisType.abbreviation(wrapper);
+            }
             /*
              * Get the grid dimensions (part of the "domain" in UCAR terminology) used for computing
              * the coordinate values along the current axis. There is exactly 1 such grid dimension in
@@ -276,8 +281,7 @@ next:       for (final String name : axisNames) {
                  */
             }
             axes[targetDim] = new Axis(abbreviation, axis.getPositive(),
-                    ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i),
-                    ((DecoderWrapper) decoder).getWrapperFor(axis));
+                    ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i), wrapper);
         }
         /*
          * We want axes in "natural" order. But the netCDF UCAR library sometime provides axes already
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 e447373..95ee819 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
@@ -29,12 +29,14 @@ import ucar.nc2.Attribute;
 import ucar.nc2.VariableIF;
 import ucar.nc2.dataset.Enhancements;
 import ucar.nc2.dataset.VariableEnhanced;
+import ucar.nc2.dataset.CoordinateAxis;
 import ucar.nc2.dataset.CoordinateAxis1D;
 import ucar.nc2.dataset.CoordinateAxis2D;
 import ucar.nc2.dataset.CoordinateSystem;
 import ucar.nc2.dataset.EnhanceScaleMissing;
 import ucar.nc2.units.SimpleUnit;
 import ucar.nc2.units.DateUnit;
+import ucar.nc2.constants._Coordinate;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.math.Vector;
@@ -50,6 +52,7 @@ import org.apache.sis.storage.netcdf.AttributeNames;
 import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
+import ucar.nc2.constants.AxisType;
 
 
 /**
@@ -255,6 +258,20 @@ final class VariableWrapper extends Variable {
     }
 
     /**
+     * Returns the value of the {@code "_CoordinateAxisType"} attribute, or {@code null} if none.
+     */
+    @Override
+    protected String getAxisType() {
+        if (variable instanceof CoordinateAxis) {
+            final AxisType type = ((CoordinateAxis) variable).getAxisType();
+            if (type != null) {
+                return type.name();
+            }
+        }
+        return getAttributeAsString(_Coordinate.AxisType);
+    }
+
+    /**
      * Returns a builder for the grid geometry of this variable, or {@code null} if this variable is not a data cube.
      * This method searches for a grid previously computed by {@link DecoderWrapper#getGrids()}, keeping in mind that
      * the UCAR library sometime builds {@link CoordinateSystem} instances with axes in different order than what we


Mime
View raw message