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: Make the `FeatureSet` implementation more robust and more generic. This commit prepares `FeatureSet` to support "ordinary" trajectory (without time vector) in addition to moving features.
Date Wed, 14 Oct 2020 18:41:37 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 924d4a8  Make the `FeatureSet` implementation more robust and more generic. This commit prepares `FeatureSet` to support "ordinary" trajectory (without time vector) in addition to moving features.
924d4a8 is described below

commit 924d4a8e7f3725bd1c09cc2279256a267d0f5ace
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Oct 14 20:39:16 2020 +0200

    Make the `FeatureSet` implementation more robust and more generic.
    This commit prepares `FeatureSet` to support "ordinary" trajectory
    (without time vector) in addition to moving features.
---
 .../org/apache/sis/internal/netcdf/DataType.java   |  60 +++-
 .../org/apache/sis/internal/netcdf/Decoder.java    |   7 +-
 .../org/apache/sis/internal/netcdf/FeatureSet.java | 372 +++++++++++++--------
 .../org/apache/sis/internal/netcdf/Resources.java  |  11 +
 .../sis/internal/netcdf/Resources.properties       |   2 +
 .../sis/internal/netcdf/Resources_fr.properties    |   2 +
 .../org/apache/sis/internal/netcdf/Variable.java   |  93 +++++-
 .../sis/internal/netcdf/impl/VariableInfo.java     |  44 +--
 .../sis/internal/netcdf/ucar/DecoderWrapper.java   |  28 +-
 .../sis/internal/netcdf/ucar/VariableWrapper.java  |  22 +-
 .../apache/sis/internal/netcdf/DataTypeTest.java   |  23 +-
 11 files changed, 441 insertions(+), 223 deletions(-)

diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java
index 0bc81b4..3a8fb12 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java
@@ -17,6 +17,7 @@
 package org.apache.sis.internal.netcdf;
 
 import java.awt.image.DataBuffer;
+import org.apache.sis.math.Vector;
 import org.apache.sis.util.Numbers;
 
 
@@ -37,7 +38,7 @@ import org.apache.sis.util.Numbers;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.8
  * @module
  */
@@ -45,77 +46,77 @@ public enum DataType {
     /**
      * The enumeration for unknown data type. This is not a valid netCDF type.
      */
-    UNKNOWN(Numbers.OTHER, false, false, (byte) 0, null),
+    UNKNOWN(Numbers.OTHER, Object.class, false, false, (byte) 0, null),
 
     /**
      * 8 bits signed integer (netCDF type 1).
      * Can be made unsigned by assigning the “_Unsigned” attribute to a netCDF variable.
      */
-    BYTE(Numbers.BYTE, true, false, (byte) 7, org.apache.sis.image.DataType.BYTE),
+    BYTE(Numbers.BYTE, Byte.class, true, false, (byte) 7, org.apache.sis.image.DataType.BYTE),
 
     /**
      * Character type as unsigned 8 bits (netCDF type 2).
      * Encoding can be specified by assigning the “_Encoding” attribute to a netCDF variable.
      */
-    CHAR(Numbers.BYTE, false, true, (byte) 2, null),                // NOT Numbers.CHARACTER
+    CHAR(Numbers.BYTE, Character.class, false, true, (byte) 2, null),   // NOT Numbers.CHARACTER
 
     /**
      * 16 bits signed integer (netCDF type 3).
      */
-    SHORT(Numbers.SHORT, true, false, (byte) 8, org.apache.sis.image.DataType.SHORT),
+    SHORT(Numbers.SHORT, Short.class, true, false, (byte) 8, org.apache.sis.image.DataType.SHORT),
 
     /**
      * 32 bits signed integer (netCDF type 4).
      * This is also called "long", but that name is deprecated.
      */
-    INT(Numbers.INTEGER, true, false, (byte) 9, org.apache.sis.image.DataType.INT),
+    INT(Numbers.INTEGER, Integer.class, true, false, (byte) 9, org.apache.sis.image.DataType.INT),
 
     /**
      * 32 bits floating point number (netCDF type 5)
      * This is also called "real".
      */
-    FLOAT(Numbers.FLOAT, false, false, (byte) 5, org.apache.sis.image.DataType.FLOAT),
+    FLOAT(Numbers.FLOAT, Float.class, false, false, (byte) 5, org.apache.sis.image.DataType.FLOAT),
 
     /**
      * 64 bits floating point number (netCDF type 6).
      */
-    DOUBLE(Numbers.DOUBLE, false, false, (byte) 6, org.apache.sis.image.DataType.DOUBLE),
+    DOUBLE(Numbers.DOUBLE, Double.class, false, false, (byte) 6, org.apache.sis.image.DataType.DOUBLE),
 
     /**
      * 8 bits unsigned integer (netCDF type 7).
      * Not available in netCDF classic format.
      */
-    UBYTE(Numbers.BYTE, true, true, (byte) 1, org.apache.sis.image.DataType.BYTE),
+    UBYTE(Numbers.BYTE, Short.class, true, true, (byte) 1, org.apache.sis.image.DataType.BYTE),
 
     /**
      * 16 bits unsigned integer (netCDF type 8).
      * Not available in netCDF classic format.
      */
-    USHORT(Numbers.SHORT, true, true, (byte) 3, org.apache.sis.image.DataType.USHORT),
+    USHORT(Numbers.SHORT, Integer.class, true, true, (byte) 3, org.apache.sis.image.DataType.USHORT),
 
     /**
      * 32 bits unsigned integer (netCDF type 9).
      * Not available in netCDF classic format.
      */
-    UINT(Numbers.INTEGER, true, true, (byte) 4, org.apache.sis.image.DataType.INT),
+    UINT(Numbers.INTEGER, Long.class, true, true, (byte) 4, org.apache.sis.image.DataType.INT),
 
     /**
      * 64 bits signed integer (netCDF type 10).
      * Not available in netCDF classic format.
      */
-    INT64(Numbers.LONG, true, false, (byte) 11, null),
+    INT64(Numbers.LONG, Long.class, true, false, (byte) 11, null),
 
     /**
      * 64 bits unsigned integer (netCDF type 11).
      * Not available in netCDF classic format.
      */
-    UINT64(Numbers.LONG, true, true, (byte) 10, null),
+    UINT64(Numbers.LONG, Number.class, true, true, (byte) 10, null),
 
     /**
      * Character string (netCDF type 12).
      * Not available in netCDF classic format.
      */
-    STRING(Numbers.OTHER, false, false, (byte) 12, null);
+    STRING(Numbers.OTHER, String.class, false, false, (byte) 12, null);
 
     /**
      * Mapping from the netCDF data type to the enumeration used by our {@link Numbers} class.
@@ -149,12 +150,20 @@ public enum DataType {
     public final org.apache.sis.image.DataType rasterDataType;
 
     /**
-     * Creates a new enumeration.
+     * The smallest Java wrapper class that can hold the values. Values are always signed. If {@link #isUnsigned}
+     * is {@code true}, then a wider type is used for holding the large unsigned values. For example the 16 bits
+     * signed integer type is used for holding 8 bits unsigned integers.
      */
-    private DataType(final byte number, final boolean isInteger, final boolean isUnsigned,
+    private final Class<?> classe;
+
+    /**
+     * Creates a new enumeration value.
+     */
+    private DataType(final byte number, final Class<?> classe, final boolean isInteger, final boolean isUnsigned,
             final byte opposite, final org.apache.sis.image.DataType rasterDataType)
     {
         this.number         = number;
+        this.classe         = classe;
         this.isInteger      = isInteger;
         this.isUnsigned     = isUnsigned;
         this.opposite       = opposite;
@@ -162,6 +171,25 @@ public enum DataType {
     }
 
     /**
+     * Returns the Java class to use for storing the values.
+     *
+     * @param  vector  {@code true} for a vector object, or {@code false} for a scalar object.
+     */
+    final Class<?> getClass(final boolean vector) {
+        if (vector) {
+            if (classe == Character.class) {
+                return String.class;
+            } else if (Number.class.isAssignableFrom(classe)) {
+                return Vector.class;
+            } else {
+                return Object.class;
+            }
+        } else {
+            return classe;
+        }
+    }
+
+    /**
      * Returns the number of bytes for this data type, or 0 if unknown.
      *
      * @return number of bytes for this data type, or 0 if unknown.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
index a119e2a..dd5bf0f 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
@@ -364,15 +364,16 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
 
     /**
      * If the file contains features encoded as discrete sampling (for example profiles or trajectories),
-     * returns objects for handling them.
-     * This method may return a direct reference to an internal array - do not modify.
+     * returns objects for handling them. This method does not need to cache the returned array, because
+     * it will be invoked only once by {@link org.apache.sis.storage.netcdf.NetcdfStore#components()}.
      *
      * @return a handler for the features, or an empty array if none.
      * @throws IOException if an I/O operation was necessary but failed.
      * @throws DataStoreException if a logical error occurred.
      */
     public DiscreteSampling[] getDiscreteSampling() throws IOException, DataStoreException {
-        if ("trajectory".equalsIgnoreCase(stringValue(CF.FEATURE_TYPE))) try {
+        final String type = stringValue(CF.FEATURE_TYPE);
+        if (type == null || type.equalsIgnoreCase(FeatureSet.TRAJECTORY)) try {
             return FeatureSet.create(this);
         } catch (IllegalArgumentException | ArithmeticException e) {
             // Illegal argument is not a problem with content, but rather with configuration.
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 91b7655..d47a942 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
@@ -21,7 +21,6 @@ import java.util.List;
 import java.util.Collection;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Spliterator;
 import java.util.stream.Stream;
@@ -29,20 +28,21 @@ import java.util.stream.StreamSupport;
 import java.util.function.Consumer;
 import java.util.OptionalLong;
 import java.io.IOException;
+import java.util.Collections;
+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.MovingFeature;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.feature.DefaultFeatureType;
-import org.apache.sis.feature.DefaultAttributeType;
+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 ucar.nc2.constants.CF;
 
 // Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyType;
-import org.opengis.feature.AttributeType;
 
 
 /**
@@ -57,29 +57,46 @@ import org.opengis.feature.AttributeType;
  */
 final class FeatureSet extends DiscreteSampling {
     /**
-     * The number of instances for each feature.
+     * Kind of features supported by this class. Also used as property name.
+     */
+    static final String TRAJECTORY = "trajectory";
+
+    /**
+     * The number of instances for each feature, or {@code null} if none. If non-null, then the number of features
+     * is the length of this vector and each {@link Feature} instance has multi-valued properties with a number of
+     * elements given by this count.
+     *
+     * If null, then the number of features is determined by the length of other variables.
+     *
+     * @see #getFeatureCount()
      */
     private final Vector counts;
 
     /**
-     * The moving feature identifiers ("mfIdRef").
-     * The amount of identifiers shall be the same than the length of the {@link #counts} vector.
+     * The singleton properties (for which there is only one value per feature instance), or an empty array if none.
+     * If non-empty, it typically contains the moving feature identifiers ("mfIdRef"). If {@link #counts} is non-null,
+     * then the length of {@code singletons} variables shall be the same than the length of the {@link #counts} vector.
+     * If {@link #counts} is null, then the first {@code singletons} variable determines the number of features.
      */
-    private final Variable identifiers;
+    private final Variable[] singletons;
 
     /**
-     * The variable that contains time.
+     * Whether the {@link #coordinates} array contains a temporal variable.
+     * If {@code true}, then the time variable shall be last.
      */
-    private final Variable time;
+    private final boolean hasTime;
 
     /**
-     * The variable that contains <var>x</var> and <var>y</var> coordinate values (typically longitudes and latitudes).
-     * All variables in this array shall have the same length, and that length shall be the same than {@link #time}.
+     * The variables for <var>x</var>, <var>y</var> and potentially <var>z</var> or <var>t</var> coordinate values.
+     * The <var>x</var> and <var>y</var> coordinates are typically longitudes and latitudes, but not necessarily.
+     * If temporal coordinates exist, the time variable must be last and {@link #hasTime} shall be set to {@code true}.
+     * All variables in this array shall have the same length.
      */
     private final Variable[] coordinates;
 
     /**
-     * Any custom properties.
+     * Any time-varying properties other than coordinate values, or an empty array if none.
+     * All variables in this array shall have the same length than {@link #coordinates} variables.
      */
     private final Variable[] properties;
 
@@ -92,63 +109,67 @@ final class FeatureSet extends DiscreteSampling {
      * Creates a new discrete sampling parser for features identified by the given variable.
      *
      * @param  decoder      the source of the features to create.
-     * @param  counts       the count of instances per feature.
-     * @param  identifiers  the feature identifiers.
-     * @param  time         the variable that contains time.
-     * @param  coordinates  the variable that contains <var>x</var> and <var>y</var> coordinate values.
-     * @param  properties   the variables that contain custom properties.
+     * @param  counts       the count of instances per feature, or {@code null} if none.
+     * @param  singletons   the feature identifiers, possibly with other singleton properties.
+     * @param  hasTime      whether the {@code coordinates} array contains a temporal variable.
+     * @param  coordinates  <var>x</var>, <var>y</var> and potentially <var>z</var> or <var>t</var> coordinate values.
+     * @param  properties   the variables that contain custom time-varying properties.
      * @throws IllegalArgumentException if the given library is non-null but not available.
      */
-    @SuppressWarnings("rawtypes")                               // Because of generic array creation.
-    private FeatureSet(final Decoder decoder,
-            final Vector counts, final Variable identifiers, final Variable time,
-            final Collection<Variable> coordinates, final Collection<Variable> properties)
+    private FeatureSet(final Decoder decoder, final Vector counts, final Collection<Variable> singletons,
+            final boolean hasTime, final Collection<Variable> coordinates, final Collection<Variable> properties)
     {
         super(decoder.geomlib, decoder.listeners);
         this.counts      = counts;
-        this.identifiers = identifiers;
+        this.singletons  = singletons .toArray(new Variable[singletons .size()]);
         this.coordinates = coordinates.toArray(new Variable[coordinates.size()]);
         this.properties  = properties .toArray(new Variable[properties .size()]);
-        this.time        = time;
+        this.hasTime     = hasTime;
         /*
-         * Creates a description of the features to be read.
+         * Creates a description of the features to be read with following properties:
+         *
+         *    - Identifier and other properties having a single value per feature instance.
+         *    - Trajectory as a geometry object, potentially with a time characteristic.
+         *    - Time-varying properties (i.e. properties having a value per instant).
          */
-        final Map<String,Object> info = new HashMap<>(4);
-        final PropertyType[] pt = new PropertyType[this.properties.length + 2];
-        AttributeType[] characteristics = null;
-        for (int i=0; i<pt.length; i++) {
-            final Variable variable;
-            final Class<?> valueClass;
-            int minOccurs = 1;
-            int maxOccurs = 1;
-            switch (i) {
-                case 0: {
-                    variable   = identifiers;
-                    valueClass = Integer.class;
-                    break;
-                }
-                case 1: {
-                    variable        = null;
-                    valueClass      = factory.polylineClass;
-                    characteristics = new AttributeType[] {MovingFeature.TIME};
-                    break;
-                }
-                default: {
-                    // TODO: use more accurate Number subtype for value class.
-                    variable   = this.properties[i-2];
-                    valueClass = (variable.meaning(0) != null) ? String.class : Number.class;
-                    minOccurs  = 0;
-                    maxOccurs  = Integer.MAX_VALUE;
-                    break;
-                }
-            }
-            info.put(DefaultAttributeType.NAME_KEY, (variable != null) ? variable.getName() : "trajectory");
-            // TODO: add description.
-            pt[i] = new DefaultAttributeType<>(info, valueClass, minOccurs, maxOccurs, null, characteristics);
+        final FeatureTypeBuilder builder = new FeatureTypeBuilder(decoder.nameFactory, decoder.geomlib, decoder.listeners.getLocale());
+        for (final Variable v : this.singletons) {
+            final Class<?> type = v.getDataType().getClass(v.getNumDimensions() > 1);
+            describe(v, builder.addAttribute(Long.class), false);   // TODO: use type.
+        }
+        final AttributeTypeBuilder<?> geometry = builder.addAttribute(counts != null ? GeometryType.LINEAR : GeometryType.POINT);
+        geometry.setName(TRAJECTORY).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        if (hasTime) {
+            geometry.addCharacteristic(MovingFeature.TIME);
+        }
+        for (final Variable v : this.properties) {
+            /*
+             * Use `Number` type instead than a more specialized subclass because values
+             * will be stored in `Vector` objects and that class implements `List<Number>`.
+             */
+            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.
+    }
+
+    /**
+     * Sets the attribute name, and potentially its definition, from the given variable.
+     *
+     * @param  variable   the variable from which to get metadata.
+     * @param  attribute  the attribute to configure with variable metadata.
+     * @param  hasTime    whether to add a "time" characteristic on the attribute.
+     */
+    private static void describe(final Variable variable, final AttributeTypeBuilder<?> attribute, final boolean hasTime) {
+        final String name = variable.getName();
+        attribute.setName(name);
+        final String desc = variable.getDescription();
+        if (desc != null && !desc.equals(name)) {
+            attribute.setDefinition(desc);
+        }
+        if (hasTime) {
+            attribute.addCharacteristic(MovingFeature.TIME);
         }
-        String name = "Features";       // TODO: find a better name.
-        info.put(DefaultAttributeType.NAME_KEY, decoder.nameFactory.createLocalName(decoder.namespace, name));
-        type = new DefaultFeatureType(info, false, null, pt);
     }
 
     /**
@@ -165,7 +186,6 @@ final class FeatureSet extends DiscreteSampling {
      * @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.
      */
-    @SuppressWarnings("fallthrough")
     static FeatureSet[] create(final Decoder decoder) throws IOException, DataStoreException {
         final List<FeatureSet> features = new ArrayList<>(3);     // Will usually contain at most one element.
 search: for (final Variable counts : decoder.getVariables()) {
@@ -206,11 +226,15 @@ search: for (final Variable counts : decoder.getVariables()) {
              * 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.
              */
-            final String name = featureDimension.getName();
-            Variable identifiers = decoder.findVariable(name);
-            if (identifiers == null || !hasSupportedRole(identifiers)) {
+            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 (startsWith(alt, featureDimension, false) && hasSupportedRole(alt)) {
+                    if (hasSupportedRole(alt) && isScalarOrString(alt, featureDimension, null)) {
                         if (identifiers != null) {
                             identifiers = null;
                             break;                  // Ambiguity found: consider that we found no replacement.
@@ -219,45 +243,12 @@ search: for (final Variable counts : decoder.getVariables()) {
                     }
                 }
                 if (identifiers == null) {
-                    decoder.listeners.warning(decoder.resources().getString(
-                            Resources.Keys.VariableNotFound_2, decoder.getFilename(), name));
+                    decoder.listeners.warning(decoder.resources().getString(Resources.Keys.VariableNotFound_2,
+                                              decoder.getFilename(), featureDimension.getName()));
                     continue;
                 }
             }
             /*
-             * At this point we found a variable that should be the feature identifiers.
-             * Verify that the variable dimensions are valid.
-             */
-            final List<Dimension> dimensions = identifiers.getGridDimensions();
-            int unexpectedDimension = -1;
-            switch (dimensions.size()) {
-                default: {                              // Too many dimensions
-                    unexpectedDimension = 2;
-                    break;
-                }
-                case 2: {
-                    if (identifiers.getDataType() != DataType.CHAR) {
-                        unexpectedDimension = 1;
-                        break;
-                    }
-                    // Fall through for checking the first dimension.
-                }
-                case 1: {
-                    if (!featureDimension.equals(dimensions.get(0))) {
-                        unexpectedDimension = 0;
-                    }
-                    break;
-                }
-                case 0: continue;                       // Should not happen.
-            }
-            if (unexpectedDimension >= 0) {
-                decoder.listeners.warning(decoder.resources().getString(
-                        Resources.Keys.UnexpectedDimensionForVariable_4,
-                        decoder.getFilename(), identifiers.getName(),
-                        featureDimension.getName(), dimensions.get(unexpectedDimension).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:
@@ -279,7 +270,7 @@ search: for (final Variable counts : decoder.getVariables()) {
             final Map<String,Variable> coordinates = new LinkedHashMap<>();
             final List<Variable> properties  = new ArrayList<>();
             for (final Variable data : decoder.getVariables()) {
-                if (startsWith(data, sampleDimension, true)) {
+                if (isScalarOrString(data, sampleDimension, null)) {
                     final String axisType = data.getAttributeAsString(CF.AXIS);
                     if (axisType == null) {
                         properties.add(data);
@@ -290,7 +281,9 @@ search: for (final Variable counts : decoder.getVariables()) {
             }
             final Variable time = coordinates.remove("T");
             if (time != null) {
-                features.add(new FeatureSet(decoder, counts.read(), identifiers, time, coordinates.values(), properties));
+                coordinates.put("T", time);     // Make sure that time is last.
+                features.add(new FeatureSet(decoder, counts.read(), Collections.singleton(identifiers),
+                             true, coordinates.values(), properties));
             }
         }
         return features.toArray(new FeatureSet[features.size()]);
@@ -298,14 +291,47 @@ search: for (final Variable counts : decoder.getVariables()) {
 
     /**
      * Returns {@code true} if the given variable starts with the given dimension.
+     * If the variable is an array of character, then it can have 2 dimensions.
+     * Otherwise it shall have exactly one dimension.
      *
-     * @param  data       the data for which to check the dimensions.
-     * @param  first      the dimension that we expect as the first dimension.
-     * @param  singleton  whether the variable shall have no more dimension than {@code first}.
+     * @param  data              the data for which to check the dimensions.
+     * @param  featureDimension  the dimension that we expect as the first dimension.
+     * @param  decoder           decoder where to report warnings, or {@code null} for silent mode.
      */
-    private static boolean startsWith(final Variable data, final Dimension first, final boolean singleton) {
-        final int n = data.getNumDimensions();
-        return (singleton ? n == 1 : n >= 1) && first.equals(data.getGridDimensions().get(0));
+    @SuppressWarnings("fallthrough")
+    private static boolean isScalarOrString(final Variable data, final Dimension featureDimension, final Decoder decoder) {
+        final List<Dimension> dimensions = data.getGridDimensions();
+        final int unexpectedDimension;
+        switch (dimensions.size()) {
+            default: {                              // Too many dimensions
+                unexpectedDimension = 2;
+                break;
+            }
+            case 2: {
+                if (data.getDataType() != DataType.CHAR) {
+                    unexpectedDimension = 1;
+                    break;
+                }
+                // Fall through for checking the first dimension.
+            }
+            case 1: {
+                if (featureDimension.equals(dimensions.get(0))) {
+                    return true;
+                }
+                unexpectedDimension = 0;
+                break;
+            }
+            case 0: {                               // Should not happen.
+                return false;
+            }
+        }
+        if (decoder != null) {
+            decoder.listeners.warning(decoder.resources().getString(
+                    Resources.Keys.UnexpectedDimensionForVariable_4,
+                    decoder.getFilename(), data.getName(),
+                    featureDimension.getName(), dimensions.get(unexpectedDimension).getName()));
+        }
+        return false;
     }
 
     /**
@@ -323,7 +349,22 @@ search: for (final Variable counts : decoder.getVariables()) {
      */
     @Override
     protected OptionalLong getFeatureCount() {
-        return OptionalLong.of(counts.size());
+        if (counts != null) {
+            return OptionalLong.of(counts.size());
+        }
+        for (int i=0; ; i++) {
+            final Variable[] data;
+            switch (i) {
+                case 0: data = singletons;  break;
+                case 1: data = coordinates; break;
+                case 2: data = properties;  break;
+                default: return OptionalLong.empty();
+            }
+            if (data.length != 0) {
+                final long length = data[0].getGridDimensions().get(0).length();
+                if (length >= 0) return OptionalLong.of(length);
+            }
+        }
     }
 
     /**
@@ -332,8 +373,12 @@ search: for (final Variable counts : decoder.getVariables()) {
      * @param  parallel  ignored, since current version does not support parallelism.
      */
     @Override
-    public Stream<Feature> features(boolean parallel) {
-        return StreamSupport.stream(new Iter(), false);
+    public Stream<Feature> features(boolean parallel) throws DataStoreException {
+        try {
+            return StreamSupport.stream(new Iter(), false);
+        } catch (IOException e) {
+            throw new DataStoreException(canNotReadFile(), e);
+        }
     }
 
     /**
@@ -341,6 +386,11 @@ search: for (final Variable counts : decoder.getVariables()) {
      */
     private final class Iter implements Spliterator<Feature> {
         /**
+         * Expected number of features.
+         */
+        private final int count;
+
+        /**
          * Index of the next feature to read.
          */
         private int index;
@@ -352,9 +402,23 @@ search: for (final Variable counts : decoder.getVariables()) {
         private int position;
 
         /**
+         * The singleton properties, or an empty array if none. This is called "identifiers" because
+         * this is usually an array of length 1 with a vector containing feature identifiers.
+         *
+         * @see FeatureSet#singletons
+         */
+        private final Vector[] identifiers;
+
+        /**
          * Creates a new iterator.
          */
-        Iter() {
+        Iter() throws IOException, DataStoreException {
+            count = (int) Math.min(getFeatureCount().orElse(0), Integer.MAX_VALUE);
+            identifiers = new Vector[singletons.length];
+            for (int i=0; i < identifiers.length; i++) {
+                // Efficiency should be okay because those vectors are cached.
+                identifiers[i] = singletons[i].read();
+            }
         }
 
         /**
@@ -367,52 +431,68 @@ search: for (final Variable counts : decoder.getVariables()) {
          */
         @Override
         public boolean tryAdvance(final Consumer<? super Feature> action) {
-            final int length = counts.intValue(index);
-            final GridExtent extent = new GridExtent(null, new long[] {position},
-                            new long[] {Math.addExact(position, length)}, false);
+            final Vector[] coordinateValues  = new Vector[coordinates.length];
+            final Object[] singleProperties  = new Number[singletons .length];
+            final Object[] varyingProperties = new Object[properties .length];
+            for (int i=0; i < singleProperties.length; i++) {
+                singleProperties[i] = identifiers[i].get(index);
+            }
             final int[] step = {1};
-            final Vector   id, t;
-            final Vector[] coords = new Vector[coordinates.length];
-            final Object[] props  = new Object[properties.length];
-            try {
-                id = identifiers.read();                    // Efficiency should be okay because of cached value.
-                t = time.read(extent, step);
-                for (int i=0; i<coordinates.length; i++) {
-                    coords[i] = coordinates[i].read(extent, step);
-                }
-                for (int i=0; i<properties.length; i++) {
-                    final Variable p = properties[i];
-                    final Vector data = p.read(extent, step);
-                    if (p.isEnumeration()) {
-                        final String[] meanings = new String[data.size()];
-                        for (int j=0; j<meanings.length; j++) {
-                            String m = p.meaning(data.intValue(j));
-                            meanings[j] = (m != null) ? m : "";
+            boolean isEmpty = true;
+            int length;
+            do {
+                length = (counts != null) ? counts.intValue(index) : 1;
+                if (length != 0) {
+                    isEmpty = false;
+                    final GridExtent extent = new GridExtent(null, new long[] {position},
+                                    new long[] {Math.addExact(position, length)}, false);
+                    try {
+                        for (int i=0; i<coordinateValues.length; i++) {
+                            coordinateValues[i] = coordinates[i].read(extent, step);
                         }
-                        props[i] = Arrays.asList(meanings);
-                    } else {
-                        props[i] = data;
+                        for (int i=0; i<properties.length; i++) {
+                            final Variable p = properties[i];
+                            final Vector data = p.read(extent, step);
+                            if (p.isEnumeration()) {
+                                final String[] meanings = new String[data.size()];
+                                for (int j=0; j<meanings.length; j++) {
+                                    String m = p.meaning(data.intValue(j));
+                                    meanings[j] = (m != null) ? m : "";
+                                }
+                                varyingProperties[i] = Arrays.asList(meanings);
+                            } else {
+                                varyingProperties[i] = data;
+                            }
+                        }
+                    } catch (IOException | DataStoreException e) {
+                        throw new BackingStoreException(canNotReadFile(), e);
                     }
                 }
-            } catch (IOException | DataStoreException e) {
-                throw new BackingStoreException(canNotReadFile(), e);
-            }
+                if (++index >= count) {
+                    return false;
+                }
+            } while (isEmpty);
+            /*
+             * At this point we found that there is some data we can put in a feature instance.
+             */
             final Feature feature = type.newInstance();
-            feature.setPropertyValue(identifiers.getName(), id.intValue(index));
+            for (int i=0; i < singleProperties.length; i++) {
+                feature.setPropertyValue(singletons[i].getName(), singleProperties[i]);
+            }
             for (int i=0; i<properties.length; i++) {
-                feature.setPropertyValue(properties[i].getName(), props[i]);
+                feature.setPropertyValue(properties[i].getName(), varyingProperties[i]);
                 // TODO: set time characteristic.
             }
             // TODO: temporary hack - to be replaced by support in Vector.
-            final int dimension = coordinates.length;
+            final int dimension = coordinates.length - (hasTime ? 1 : 0);
             final double[] tmp = new double[length * dimension];
             for (int i=0; i<tmp.length; i++) {
-                tmp[i] = coords[i % dimension].doubleValue(i / dimension);
+                tmp[i] = coordinateValues[i % dimension].doubleValue(i / dimension);
             }
-            feature.setPropertyValue("trajectory", factory.createPolyline(false, dimension, Vector.create(tmp)));
+            feature.setPropertyValue(TRAJECTORY, factory.createPolyline(false, dimension, Vector.create(tmp)));
             action.accept(feature);
             position = Math.addExact(position, length);
-            return ++index < counts.size();
+            return true;
         }
 
         /**
@@ -428,7 +508,7 @@ search: for (final Variable counts : decoder.getVariables()) {
          */
         @Override
         public long estimateSize() {
-            return counts.size() - index;
+            return count - index;
         }
 
         /**
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 5712d71..f6acc63 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
@@ -131,6 +131,12 @@ public final class Resources extends IndexedResourceBundle {
         public static final short IllegalValueRange_4 = 16;
 
         /**
+         * Attributes “{1}” and “{2}” on variable “{0}” have different lengths: {3} and {4}
+         * respectively.
+         */
+        public static final short MismatchedAttributeLength_5 = 24;
+
+        /**
          * The declared size of variable “{1}” in netCDF file “{0}” is {2,number} bytes greater than
          * expected.
          */
@@ -142,6 +148,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short MismatchedVariableType_3 = 13;
 
         /**
+         * Missing attribute “{2}” on the “{1}” variable of netCDF file “{0}”.
+         */
+        public static final short MissingVariableAttribute_3 = 23;
+
+        /**
          * Variable “{1}” or netCDF file “{0}” has a different size than its coordinate system, but no
          * resampling interval is specified.
          */
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 b84929e..62b864e 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
@@ -33,8 +33,10 @@ DimensionNotFound_3               = Dimension \u201c{2}\u201d declared by attrib
 DuplicatedAxis_2                  = Duplicated axis \u201c{1}\u201d in a grid of 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.
 MismatchedVariableSize_3          = The declared size of variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d is {2,number} bytes greater than expected.
 MismatchedVariableType_3          = Variables \u201c{1}\u201d and \u201c{2}\u201d in netCDF file \u201c{0}\u201d does not have the same type.
+MissingVariableAttribute_3        = Missing attribute \u201c{2}\u201d on the \u201c{1}\u201d variable of netCDF file \u201c{0}\u201d.
 ResamplingIntervalNotFound_2      = Variable \u201c{1}\u201d or netCDF file \u201c{0}\u201d has a different size than its coordinate system, but no resampling interval is specified.
 UnexpectedAxisCount_4             = Reference system of type \u2018{1}\u2019 can not have {2}\u00a0axes. The axes found in the \u201c{0}\u201d netCDF file are: {3}.
 UnexpectedDimensionForVariable_4  = Variable \u201c{1}\u201d in file \u201c{0}\u201d has a dimension \u201c{3}\u201d while we expected \u201c{2}\u201d.
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 1106a07..e9dd81c 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
@@ -38,8 +38,10 @@ DimensionNotFound_3               = La dimension \u00ab\u202f{2}\u202f\u00bb d\u
 DuplicatedAxis_2                  = Axe \u00ab\u202f{1}\u202f\u00bb dupliqu\u00e9 dans une grille du 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.
 MismatchedVariableSize_3          = La longueur d\u00e9clar\u00e9e de la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb d\u00e9passe de {2,number} octets la valeur attendue.
 MismatchedVariableType_3          = Les variables \u00ab\u202f{1}\u202f\u00bb et \u00ab\u202f{2}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb ne sont pas du m\u00eame type.
+MissingVariableAttribute_3        = Il manque l\u2019attribut \u201c{2}\u201d sur la variable \u201c{1}\u201d du fichier netCDF \u201c{0}\u201d.
 ResamplingIntervalNotFound_2      = La variable \u00ab\u202f{1}\u202f\u00bb du fichier netCDF \u00ab\u202f{0}\u202f\u00bb a une taille diff\u00e9rente de celle de son syst\u00e8me de coordonn\u00e9es, mais l\u2019intervalle d\u2019\u00e9chantillonnage n\u2019a pas \u00e9t\u00e9 sp\u00e9cifi\u00e9.
 UnexpectedAxisCount_4             = Les syst\u00e8mes de r\u00e9f\u00e9rence de type \u2018{1}\u2019 ne peuvent pas avoir {2}\u00a0axes. Les axes trouv\u00e9s dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb sont\u00a0: {3}.
 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.
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 eec64c2..6285bd9 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
@@ -38,9 +38,12 @@ import org.apache.sis.math.MathFunctions;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.internal.util.CollectionsExt;
+import org.apache.sis.storage.netcdf.AttributeNames;
 import org.apache.sis.util.resources.Errors;
 import ucar.nc2.constants.CDM;                      // We use only String constants.
 import ucar.nc2.constants.CF;
@@ -100,13 +103,23 @@ public abstract class Variable extends Node {
     /**
      * All no-data values declared for this variable, or an empty map if none.
      * This is computed by {@link #getNodataValues()} and cached for efficiency and stability.
-     * The meaning of entries in this map is described in {@code getNodataValues()} method javadoc.
+     * The meaning of entries in this map is described in {@link #getNodataValues()} method javadoc.
      *
      * @see #getNodataValues()
      */
     private Map<Number,Object> nodataValues;
 
     /**
+     * The {@code flag_meanings} values (used for enumeration values),
+     * or {@code null} if this variable is not an enumeration.
+     *
+     * @see #isEnumeration()
+     * @see #meaning(int)
+     * @see #setFlagMeanings(Object, Object)
+     */
+    private Map<Integer,String> meanings;
+
+    /**
      * The grid associated to this variable, or {@code null} if none or not yet computed.
      * The grid needs to be computed if {@link #gridDetermined} is {@code false}.
      *
@@ -142,6 +155,76 @@ public abstract class Variable extends Node {
     }
 
     /**
+     * If {@code flags} is non-null, declares this variable as an enumeration.
+     * This method stores the information needed for {@link #meaning(int)} default implementation.
+     *
+     * @param  flags   the flag meanings as a space-separated string, or {@code null} if none.
+     * @param  values  the flag values as a vector of integer values, or {@code null} if none.
+     *
+     * @see #isEnumeration()
+     * @see #meaning(int)
+     */
+    @SuppressWarnings("null")
+    protected final void setFlagMeanings(final Object flags, final Object values) {
+        if (flags == null) {
+            return;
+        }
+        final String[] labels = (String[]) CharSequences.split(flags.toString(), ' ');
+        int count = labels.length;
+        meanings = new HashMap<>(Containers.hashMapCapacity(count));
+        final Vector numbers;
+        if (values instanceof Vector) {
+            numbers = (Vector) values;
+            final int n = numbers.size();
+            if (n != count) {
+                warning(Variable.class, "setFlagMeanings", Resources.Keys.MismatchedAttributeLength_5,
+                        getName(), AttributeNames.FLAG_VALUES, AttributeNames.FLAG_MEANINGS, n, count);
+                if (n < count) count = n;
+            }
+        } else {
+            numbers = Vector.createSequence(0, 1, count);
+            warning(Variable.class, "setFlagMeanings", Resources.Keys.MissingVariableAttribute_3,
+                    getFilename(), getName(), AttributeNames.FLAG_VALUES);
+        }
+        /*
+         * Copy (numbers, labels) entries in an HashMap with keys converted to 32-bits signed integer.
+         * If a key can not be converted, we will log a warning after all errors have been collected
+         * in order to produce only one log message. We put a limit on the amount of reported errors
+         * for avoiding to flood the logger.
+         */
+        Exception     error    = null;
+        StringBuilder invalids = null;
+        for (int i=0; i<count; i++) try {
+            meanings.merge(numbers.intValue(i), labels[i], (o,n) -> o + " | " + n);
+        } catch (NumberFormatException | ArithmeticException e) {
+            if (error == null) {
+                error = e;
+                invalids = new StringBuilder();
+            } else {
+                final int length = invalids.length();
+                final boolean tooManyErrors = (length > 100);                   // Arbitrary limit.
+                if (tooManyErrors && invalids.charAt(length - 1) == '…') {
+                    continue;
+                }
+                error.addSuppressed(e);
+                invalids.append(", ");
+                if (tooManyErrors) {
+                    invalids.append('…');
+                    continue;
+                }
+            }
+            invalids.append(numbers.stringValue(i));
+        }
+        if (invalids != null) {
+            error(Variable.class, "setFlagMeanings", error,
+                  Errors.Keys.CanNotConvertValue_2, invalids, numbers.getElementType());
+        }
+        if (meanings.isEmpty()) {
+            meanings = null;
+        }
+    }
+
+    /**
      * Returns the name of the netCDF file containing this variable, or {@code null} if unknown.
      * This is used for information purpose or error message formatting only.
      *
@@ -329,7 +412,9 @@ public abstract class Variable extends Node {
      *
      * @see #meaning(int)
      */
-    protected abstract boolean isEnumeration();
+    protected boolean isEnumeration() {
+        return meanings != null;
+    }
 
     /**
      * Returns whether this variable can grow. A variable is unlimited if at least one of its dimension is unlimited.
@@ -955,7 +1040,9 @@ public abstract class Variable extends Node {
      * @param  ordinal  the ordinal of the enumeration for which to get the value.
      * @return the value associated to the given ordinal, or {@code null} if none.
      */
-    protected abstract String meaning(final int ordinal);
+    protected String meaning(final int ordinal) {
+        return meanings.get(ordinal);
+    }
 
     /**
      * Returns a string representation of this variable for debugging purpose.
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 989e047..4f3f8c8 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
@@ -166,19 +166,6 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     private transient Vector values;
 
     /**
-     * The {@code flag_meanings} values (used for enumeration values), or {@code null} if this variable is not
-     * an enumeration.
-     *
-     * @see #isEnumeration()
-     * @see #meaning(int)
-     *
-     * @todo Need to be consistent with {@code VariableWrapper}. We could move this field to {@code FeatureSet},
-     *       or provides the same functionality in {@code VariableWrapper}. Whatever solution is chosen,
-     *       {@code RasterResource.createEnumeration(…)} needs to use the mechanism common to both implementations.
-     */
-    private final String[] meanings;
-
-    /**
      * Creates a new variable.
      *
      * @param  decoder     the netCDF file where this variable is stored.
@@ -267,13 +254,9 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
          * enumeration since those attributes may be verbose and "pollute" the variable definition.
          */
         if (!attributes.isEmpty()) {    // For avoiding UnsupportedOperationException if unmodifiable map.
-            final Object flags = attributes.remove(AttributeNames.FLAG_MEANINGS);
-            if (flags != null) {
-                meanings = (String[]) CharSequences.split(flags.toString(), ' ');
-                return;
-            }
+            setFlagMeanings(attributes.remove(AttributeNames.FLAG_MEANINGS),
+                            attributes.remove(AttributeNames.FLAG_VALUES));
         }
-        meanings = null;
     }
 
     /**
@@ -425,16 +408,6 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     }
 
     /**
-     * Returns {@code true} if this variable is an enumeration.
-     *
-     * @see #meaning(int)
-     */
-    @Override
-    protected boolean isEnumeration() {
-        return meanings != null;
-    }
-
-    /**
      * Returns whether this variable can grow. A variable is unlimited if at least one of its dimension is unlimited.
      * In netCDF 3 classic format, only the first dimension can be unlimited.
      */
@@ -756,19 +729,6 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     }
 
     /**
-     * Returns the meaning of the given ordinal value, or {@code null} if none.
-     * Callers must have verified that {@link #isEnumeration()} returned {@code true}
-     * before to invoke this method
-     *
-     * @param  ordinal  the ordinal of the enumeration for which to get the value.
-     * @return the value associated to the given ordinal, or {@code null} if none.
-     */
-    @Override
-    protected String meaning(final int ordinal) {
-        return (ordinal >= 0 && ordinal < meanings.length) ? meanings[ordinal] : null;
-    }
-
-    /**
      * Returns the error message for an unknown data type.
      */
     private String unknownType() {
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java
index ffb55e9..36a6bda 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java
@@ -91,6 +91,9 @@ public final class DecoderWrapper extends Decoder implements CancelTask {
 
     /**
      * The discrete sampling features, or {@code null} if none.
+     * This reference is kept for making possible to close it in {@link #close()}.
+     *
+     * @see #getDiscreteSampling()
      */
     private transient FeatureDataset features;
 
@@ -426,19 +429,24 @@ public final class DecoderWrapper extends Decoder implements CancelTask {
             features = FeatureDatasetFactoryManager.wrap(null, (NetcdfDataset) file, this,
                     new Formatter(new LogAdapter(listeners), listeners.getLocale()));
         }
-        List<FeatureCollection> fc = null;
         if (features instanceof FeatureDatasetPoint) {
-            fc = ((FeatureDatasetPoint) features).getPointFeatureCollectionList();
-        }
-        final FeaturesWrapper[] wrappers = new FeaturesWrapper[(fc != null) ? fc.size() : 0];
-        try {
-            for (int i=0; i<wrappers.length; i++) {
-                wrappers[i] = new FeaturesWrapper(fc.get(i), geomlib, listeners);
+            final List<FeatureCollection> fc = ((FeatureDatasetPoint) features).getPointFeatureCollectionList();
+            if (fc != null && !fc.isEmpty()) {
+                final FeaturesWrapper[] wrappers = new FeaturesWrapper[fc.size()];
+                try {
+                    for (int i=0; i<wrappers.length; i++) {
+                        wrappers[i] = new FeaturesWrapper(fc.get(i), geomlib, listeners);
+                    }
+                } catch (IllegalArgumentException e) {
+                    throw new DataStoreException(e.getLocalizedMessage(), e);
+                }
+                return wrappers;
             }
-        } catch (IllegalArgumentException e) {
-            throw new DataStoreException(e.getLocalizedMessage(), e);
         }
-        return wrappers;
+        /*
+         * If the UCAR library did not recognized the features in this file, ask to SIS.
+         */
+        return super.getDiscreteSampling();
     }
 
     /**
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 bde1f71..e447373 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
@@ -46,6 +46,7 @@ import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.storage.DataStoreException;
+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;
@@ -83,6 +84,11 @@ final class VariableWrapper extends Variable {
     private transient Vector values;
 
     /**
+     * {@code true} if this variable is an enumeration.
+     */
+    private final boolean isEnumeration;
+
+    /**
      * Creates a new variable wrapping the given netCDF interface.
      */
     VariableWrapper(final Decoder decoder, VariableIF v) {
@@ -95,6 +101,17 @@ final class VariableWrapper extends Variable {
             }
         }
         raw = v;
+        /*
+         * If the UCAR library recognizes this variable as an enumeration, we will use UCAR services.
+         * Only if UCAR did not recognized the enumeration, fallback on Apache SIS implementation.
+         */
+        if (variable.getDataType().isEnum() && (variable instanceof ucar.nc2.Variable)) {
+            isEnumeration = true;
+        } else {
+            setFlagMeanings(getAttributeAsString(AttributeNames.FLAG_MEANINGS),
+                            getAttributeAsVector(AttributeNames.FLAG_VALUES));
+            isEnumeration = super.isEnumeration();
+        }
     }
 
     /**
@@ -218,7 +235,7 @@ final class VariableWrapper extends Variable {
      */
     @Override
     protected boolean isEnumeration() {
-        return variable.getDataType().isEnum() && (variable instanceof ucar.nc2.Variable);
+        return isEnumeration;
     }
 
     /**
@@ -538,7 +555,8 @@ final class VariableWrapper extends Variable {
      */
     @Override
     protected String meaning(final int ordinal) {
-        return ((ucar.nc2.Variable) variable).lookupEnumString(ordinal);
+        return super.isEnumeration() ? super.meaning(ordinal)
+                : ((ucar.nc2.Variable) variable).lookupEnumString(ordinal);
     }
 
     /**
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/DataTypeTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/DataTypeTest.java
index 6231ce5..6ce401d 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/DataTypeTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/DataTypeTest.java
@@ -18,6 +18,7 @@ package org.apache.sis.internal.netcdf;
 
 import org.junit.Test;
 import org.apache.sis.test.TestCase;
+import org.apache.sis.util.Numbers;
 
 import static org.junit.Assert.*;
 
@@ -26,7 +27,7 @@ import static org.junit.Assert.*;
  * Tests {@link DataType} enumeration values.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.1
  * @since   0.8
  * @module
  */
@@ -82,4 +83,24 @@ public final strictfp class DataTypeTest extends TestCase {
         assertSame("unsigned", unsigned, unsigned.unsigned(true));
         assertSame("unsigned", unsigned,   signed.unsigned(true));
     }
+
+    /**
+     * Verifies the {@link DataType#classe} values.
+     */
+    @Test
+    public void testClasses() {
+        for (final DataType type : DataType.values()) {
+            final String name = type.name();
+            final int code = Numbers.getEnumConstant(type.getClass(false));
+            if (type.isInteger) {
+                if (!type.isUnsigned) {
+                    assertEquals(name, type.number, code);
+                } else if (type != DataType.UINT64) {
+                    assertTrue(name, code > type.number);
+                }
+            } else {
+                assertEquals(name, (type == DataType.CHAR) ? Numbers.CHARACTER : type.number, code);
+            }
+        }
+    }
 }


Mime
View raw message