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

[sis] 02/02: Move `FeatureInfos` to a package where it can be shared by the two implementations. This move allows to use that class also with the decoder based on UCAR library.

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

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

commit bc6ca0f1ef1f7e08ab358f8a235db7c105baa1f4
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Oct 12 12:32:30 2020 +0200

    Move `FeatureInfos` to a package where it can be shared by the two implementations.
    This move allows to use that class also with the decoder based on UCAR library.
---
 .../org/apache/sis/internal/netcdf/Decoder.java    |  44 +++-
 .../{impl/FeaturesInfo.java => FeatureSet.java}    | 262 +++++++++++----------
 .../org/apache/sis/internal/netcdf/Variable.java   |  28 +++
 .../sis/internal/netcdf/impl/ChannelDecoder.java   |  51 ++--
 .../sis/internal/netcdf/impl/VariableInfo.java     |  22 +-
 .../sis/internal/netcdf/ucar/DecoderWrapper.java   |  28 ++-
 .../sis/internal/netcdf/ucar/DimensionWrapper.java |   4 +-
 .../sis/internal/netcdf/ucar/FeaturesWrapper.java  |   4 +
 .../sis/internal/netcdf/ucar/VariableWrapper.java  |  38 ++-
 9 files changed, 316 insertions(+), 165 deletions(-)

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 e1c8c9b..a119e2a 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
@@ -46,6 +46,7 @@ import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
+import ucar.nc2.constants.CF;
 
 
 /**
@@ -295,8 +296,7 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
      * @param  e      the exception, or {@code null} if none.
      */
     final void illegalAttributeValue(final String name, final String value, final NumberFormatException e) {
-        listeners.warning(Resources.forLocale(listeners.getLocale()).getString(
-                Resources.Keys.IllegalAttributeValue_3, getFilename(), name, value), e);
+        listeners.warning(resources().getString(Resources.Keys.IllegalAttributeValue_3, getFilename(), name, value), e);
     }
 
     /**
@@ -371,7 +371,15 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
      * @throws IOException if an I/O operation was necessary but failed.
      * @throws DataStoreException if a logical error occurred.
      */
-    public abstract DiscreteSampling[] getDiscreteSampling() throws IOException, DataStoreException;
+    public DiscreteSampling[] getDiscreteSampling() throws IOException, DataStoreException {
+        if ("trajectory".equalsIgnoreCase(stringValue(CF.FEATURE_TYPE))) try {
+            return FeatureSet.create(this);
+        } catch (IllegalArgumentException | ArithmeticException e) {
+            // Illegal argument is not a problem with content, but rather with configuration.
+            throw new DataStoreException(e.getLocalizedMessage(), e);
+        }
+        return new FeatureSet[0];
+    }
 
     /**
      * Returns all grid geometries (related to coordinate systems) found in the netCDF file.
@@ -432,6 +440,25 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
     }
 
     /**
+     * Returns the dimension of the given name (eventually ignoring case), or {@code null} if none.
+     * This method searches in all dimensions found in the netCDF file, regardless of variables.
+     *
+     * @param  dimName  the name of the dimension to search.
+     * @return dimension of the given name, or {@code null} if none.
+     */
+    protected abstract Dimension findDimension(String dimName);
+
+    /**
+     * Returns the netCDF variable of the given name, or {@code null} if none.
+     *
+     * @param  name  the name of the variable to search, or {@code null}.
+     * @return the variable of the given name, or {@code null} if none.
+     *
+     * @see #getVariables()
+     */
+    protected abstract Variable findVariable(String name);
+
+    /**
      * Returns the variable or group of the given name. Groups exist in netCDF 4 but not in netCDF 3.
      *
      * @param  name  name of the variable or group to search.
@@ -450,10 +477,19 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
      */
     final void performance(final Class<?> caller, final String method, final short resourceKey, long time) {
         time = System.nanoTime() - time;
-        final LogRecord record = Resources.forLocale(listeners.getLocale()).getLogRecord(
+        final LogRecord record = resources().getLogRecord(
                 PerformanceLevel.forDuration(time, TimeUnit.NANOSECONDS), resourceKey,
                 getFilename(), time / (double) StandardDateFormat.NANOS_PER_SECOND);
         record.setLoggerName(Modules.NETCDF);
         Logging.log(caller, method, record);
     }
+
+    /**
+     * Returns the netCDF-specific resource bundle for the locale given by {@link StoreListeners#getLocale()}.
+     *
+     * @return the localized error resource bundle.
+     */
+    final Resources resources() {
+        return Resources.forLocale(listeners.getLocale());
+    }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
similarity index 60%
rename from storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
rename to storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
index 86cf5e8..91b7655 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.netcdf.impl;
+package org.apache.sis.internal.netcdf;
 
 import java.util.Map;
 import java.util.List;
@@ -31,9 +31,6 @@ import java.util.OptionalLong;
 import java.io.IOException;
 import org.apache.sis.math.Vector;
 import org.apache.sis.coverage.grid.GridExtent;
-import org.apache.sis.internal.netcdf.DataType;
-import org.apache.sis.internal.netcdf.DiscreteSampling;
-import org.apache.sis.internal.netcdf.Resources;
 import org.apache.sis.internal.feature.MovingFeature;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.feature.DefaultFeatureType;
@@ -51,13 +48,14 @@ import org.opengis.feature.AttributeType;
 /**
  * Implementations of the discrete sampling features decoder. This implementation shall be able to decode at least the
  * netCDF files encoded as specified in the OGC 16-114 (OGC Moving Features Encoding Extension: netCDF) specification.
+ * This implementation is used as a fallback when the subclass does not provide a more specialized class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.8
  * @module
  */
-final class FeaturesInfo extends DiscreteSampling {
+final class FeatureSet extends DiscreteSampling {
     /**
      * The number of instances for each feature.
      */
@@ -67,26 +65,26 @@ final class FeaturesInfo extends DiscreteSampling {
      * The moving feature identifiers ("mfIdRef").
      * The amount of identifiers shall be the same than the length of the {@link #counts} vector.
      */
-    private final VariableInfo identifiers;
+    private final Variable identifiers;
 
     /**
      * The variable that contains time.
      */
-    private final VariableInfo time;
+    private final Variable time;
 
     /**
      * 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}.
      */
-    private final VariableInfo[] coordinates;
+    private final Variable[] coordinates;
 
     /**
      * Any custom properties.
      */
-    private final VariableInfo[] properties;
+    private final Variable[] properties;
 
     /**
-     * The type of all features to be read by this {@code FeaturesInfo}.
+     * The type of all features to be read by this {@code FeatureSet}.
      */
     private final FeatureType type;
 
@@ -102,15 +100,15 @@ final class FeaturesInfo extends DiscreteSampling {
      * @throws IllegalArgumentException if the given library is non-null but not available.
      */
     @SuppressWarnings("rawtypes")                               // Because of generic array creation.
-    private FeaturesInfo(final ChannelDecoder decoder,
-            final Vector counts, final VariableInfo identifiers, final VariableInfo time,
-            final Collection<VariableInfo> coordinates, final Collection<VariableInfo> properties)
+    private FeatureSet(final Decoder decoder,
+            final Vector counts, final Variable identifiers, final Variable time,
+            final Collection<Variable> coordinates, final Collection<Variable> properties)
     {
         super(decoder.geomlib, decoder.listeners);
         this.counts      = counts;
         this.identifiers = identifiers;
-        this.coordinates = coordinates.toArray(new VariableInfo[coordinates.size()]);
-        this.properties  = properties .toArray(new VariableInfo[properties .size()]);
+        this.coordinates = coordinates.toArray(new Variable[coordinates.size()]);
+        this.properties  = properties .toArray(new Variable[properties .size()]);
         this.time        = time;
         /*
          * Creates a description of the features to be read.
@@ -119,14 +117,14 @@ final class FeaturesInfo extends DiscreteSampling {
         final PropertyType[] pt = new PropertyType[this.properties.length + 2];
         AttributeType[] characteristics = null;
         for (int i=0; i<pt.length; i++) {
-            final VariableInfo variable;
+            final Variable variable;
             final Class<?> valueClass;
             int minOccurs = 1;
             int maxOccurs = 1;
             switch (i) {
                 case 0: {
-                    variable        = identifiers;
-                    valueClass      = Integer.class;
+                    variable   = identifiers;
+                    valueClass = Integer.class;
                     break;
                 }
                 case 1: {
@@ -137,10 +135,10 @@ final class FeaturesInfo extends DiscreteSampling {
                 }
                 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;
+                    variable   = this.properties[i-2];
+                    valueClass = (variable.meaning(0) != null) ? String.class : Number.class;
+                    minOccurs  = 0;
+                    maxOccurs  = Integer.MAX_VALUE;
                     break;
                 }
             }
@@ -157,8 +155,8 @@ final class FeaturesInfo extends DiscreteSampling {
      * Returns {@code true} if the given attribute value is one of the {@code cf_role} attribute values
      * supported by this implementation.
      */
-    private static boolean isSupportedRole(final String role) {
-        return CF.TRAJECTORY_ID.equalsIgnoreCase(role);
+    private static boolean hasSupportedRole(final Variable variable) {
+        return CF.TRAJECTORY_ID.equalsIgnoreCase(variable.getAttributeAsString(CF.CF_ROLE));
     }
 
     /**
@@ -167,9 +165,10 @@ final class FeaturesInfo 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.
      */
-    static FeaturesInfo[] create(final ChannelDecoder decoder) throws IOException, DataStoreException {
-        final List<FeaturesInfo> features = new ArrayList<>(3);     // Will usually contain at most one element.
-search: for (final VariableInfo counts : decoder.variables) {
+    @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()) {
             /*
              * 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
@@ -186,108 +185,131 @@ search: for (final VariableInfo counts : decoder.variables) {
              *         int counts(identifiers);
              *             counts:sample_dimension = "points";
              */
-            if (counts.dimensions.length == 1 && counts.getDataType().isInteger) {
-                final String sampleDimName = counts.getAttributeAsString(CF.SAMPLE_DIMENSION);
-                if (sampleDimName != null) {
-                    final DimensionInfo featureDimension = counts.dimensions[0];
-                    final DimensionInfo sampleDimension = decoder.findDimension(sampleDimName);
-                    if (sampleDimension == null) {
-                        decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DimensionNotFound_3,
-                                decoder.getFilename(), counts.getName(), sampleDimName));
-                        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.
-                     */
-                    VariableInfo identifiers = decoder.findVariable(featureDimension.name);
-                    if (identifiers == null || !isSupportedRole(identifiers.getAttributeAsString(CF.CF_ROLE))) {
-                        VariableInfo replacement = null;
-                        for (final VariableInfo alt : decoder.variables) {
-                            if (alt.dimensions.length != 0 && alt.dimensions[0] == featureDimension
-                                    && isSupportedRole(alt.getAttributeAsString(CF.CF_ROLE)))
-                            {
-                                if (replacement != null) {
-                                    replacement = null;
-                                    break;                  // Ambiguity found: consider that we found no replacement.
-                                }
-                                replacement = alt;
-                            }
-                        }
-                        if (replacement != null) {
-                            identifiers = replacement;
-                        }
-                        if (identifiers == null) {
-                            decoder.listeners.warning(decoder.resources().getString(Resources.Keys.VariableNotFound_2,
-                                    decoder.getFilename(), featureDimension.name));
-                            continue;
+            if (counts.getNumDimensions() != 1 || !counts.getDataType().isInteger) {
+                continue;
+            }
+            final String sampleDimName = counts.getAttributeAsString(CF.SAMPLE_DIMENSION);
+            if (sampleDimName == null) {
+                continue;
+            }
+            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));
+                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.
+             */
+            final String name = featureDimension.getName();
+            Variable identifiers = decoder.findVariable(name);
+            if (identifiers == null || !hasSupportedRole(identifiers)) {
+                for (final Variable alt : decoder.getVariables()) {
+                    if (startsWith(alt, featureDimension, false) && hasSupportedRole(alt)) {
+                        if (identifiers != null) {
+                            identifiers = null;
+                            break;                  // Ambiguity found: consider that we found no replacement.
                         }
+                        identifiers = alt;
                     }
-                    /*
-                     * At this point we found a variable that should be the feature identifiers.
-                     * Verify that the variable dimensions are valid.
-                     */
-                    for (int i=0; i<identifiers.dimensions.length; i++) {
-                        final boolean isValid;
-                        switch (i) {
-                            case 0:  isValid = (identifiers.dimensions[0] == featureDimension); break;
-                            case 1:  isValid = (identifiers.getDataType() == DataType.CHAR); break;
-                            default: isValid = false; break;                    // Too many dimensions
-                        }
-                        if (!isValid) {
-                            decoder.listeners.warning(decoder.resources().getString(
-                                    Resources.Keys.UnexpectedDimensionForVariable_4,
-                                    decoder.getFilename(), identifiers.getName(),
-                                    featureDimension.getName(), identifiers.dimensions[i].name));
-                            continue search;
-                        }
+                }
+                if (identifiers == null) {
+                    decoder.listeners.warning(decoder.resources().getString(
+                            Resources.Keys.VariableNotFound_2, decoder.getFilename(), name));
+                    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;
                     }
-                    /*
-                     * 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,VariableInfo> coordinates = new LinkedHashMap<>();
-                    final List<VariableInfo> properties  = new ArrayList<>();
-                    for (final VariableInfo data : decoder.variables) {
-                        if (data.dimensions.length == 1 && data.dimensions[0] == sampleDimension) {
-                            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.
-                            }
-                        }
+                    // Fall through for checking the first dimension.
+                }
+                case 1: {
+                    if (!featureDimension.equals(dimensions.get(0))) {
+                        unexpectedDimension = 0;
                     }
-                    final VariableInfo time = coordinates.remove("T");
-                    if (time != null) {
-                        features.add(new FeaturesInfo(decoder, counts.read(), identifiers, time, coordinates.values(), properties));
+                    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:
+             *
+             *     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 (startsWith(data, sampleDimension, true)) {
+                    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.
                     }
                 }
             }
+            final Variable time = coordinates.remove("T");
+            if (time != null) {
+                features.add(new FeatureSet(decoder, counts.read(), identifiers, time, coordinates.values(), properties));
+            }
         }
-        return features.toArray(new FeaturesInfo[features.size()]);
+        return features.toArray(new FeatureSet[features.size()]);
+    }
+
+    /**
+     * Returns {@code true} if the given variable starts with the given 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}.
+     */
+    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));
     }
 
     /**
-     * Returns the type of all features to be read by this {@code FeaturesInfo}.
+     * Returns the type of all features to be read by this {@code FeatureSet}.
      */
     @Override
     public FeatureType getType() {
@@ -359,7 +381,7 @@ search: for (final VariableInfo counts : decoder.variables) {
                     coords[i] = coordinates[i].read(extent, step);
                 }
                 for (int i=0; i<properties.length; i++) {
-                    final VariableInfo p = properties[i];
+                    final Variable p = properties[i];
                     final Vector data = p.read(extent, step);
                     if (p.isEnumeration()) {
                         final String[] meanings = new String[data.size()];
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 9eb29b6..eec64c2 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
@@ -323,6 +323,15 @@ public abstract class Variable extends Node {
     }
 
     /**
+     * Returns {@code true} if this variable is an enumeration.
+     *
+     * @return whether this variable is an enumeration.
+     *
+     * @see #meaning(int)
+     */
+    protected abstract boolean isEnumeration();
+
+    /**
      * 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.
      *
@@ -621,6 +630,14 @@ public abstract class Variable extends Node {
     }
 
     /**
+     * Returns the number of grid dimensions. This is the size of the {@link #getGridDimensions()}
+     * list but may be cheaper than a call to {@code getGridDimensions().size()}.
+     *
+     * @return number of grid dimensions.
+     */
+    public abstract int getNumDimensions();
+
+    /**
      * Returns the dimensions of this variable in the order they are declared in the netCDF file.
      * The dimensions are those of the grid, not the dimensions of the coordinate system.
      * In ISO 19123 terminology, {@link Dimension#length()} on each dimension give the upper corner
@@ -642,6 +659,7 @@ public abstract class Variable extends Node {
      *
      * @return all dimensions of this variable, in netCDF order (reverse of "natural" order).
      *
+     * @see #getNumDimensions()
      * @see Grid#getDimensions()
      */
     public abstract List<Dimension> getGridDimensions();
@@ -930,6 +948,16 @@ public abstract class Variable extends Node {
     }
 
     /**
+     * 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.
+     */
+    protected abstract String meaning(final int ordinal);
+
+    /**
      * Returns a string representation of this variable for debugging purpose.
      *
      * @return a string representation of this variable.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
index c278a8d..4fb9046 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
@@ -47,9 +47,8 @@ import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.Node;
 import org.apache.sis.internal.netcdf.Grid;
 import org.apache.sis.internal.netcdf.Variable;
+import org.apache.sis.internal.netcdf.Dimension;
 import org.apache.sis.internal.netcdf.NamedElement;
-import org.apache.sis.internal.netcdf.DiscreteSampling;
-import org.apache.sis.internal.netcdf.Resources;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.CollectionsExt;
@@ -65,7 +64,6 @@ import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.measure.Units;
 import org.apache.sis.math.Vector;
-import ucar.nc2.constants.CF;
 
 
 /**
@@ -346,15 +344,6 @@ public final class ChannelDecoder extends Decoder {
     }
 
     /**
-     * Returns the netCDF-specific resource bundle for the locale given by {@link StoreListeners#getLocale()}.
-     *
-     * @return the localized error resource bundle.
-     */
-    final Resources resources() {
-        return Resources.forLocale(listeners.getLocale());
-    }
-
-    /**
      * Returns an exception for a malformed header. This is used only after we have determined
      * that the file should be a netCDF one, but we found some inconsistency or unknown tags.
      */
@@ -714,7 +703,8 @@ public final class ChannelDecoder extends Decoder {
      * @param  dimName  the name of the dimension to search.
      * @return dimension of the given name, or {@code null} if none.
      */
-    final DimensionInfo findDimension(final String dimName) {
+    @Override
+    protected Dimension findDimension(final String dimName) {
         DimensionInfo dim = dimensionMap.get(dimName);          // Give precedence to exact match before to ignore case.
         if (dim == null) {
             final String lower = dimName.toLowerCase(ChannelDecoder.NAME_LOCALE);
@@ -731,7 +721,7 @@ public final class ChannelDecoder extends Decoder {
      * @param  name  the name of the variable to search, or {@code null}.
      * @return the variable of the given name, or {@code null} if none.
      */
-    final VariableInfo findVariable(final String name) {
+    private VariableInfo findVariableInfo(final String name) {
         VariableInfo v = variableMap.get(name);
         if (v == null && name != null) {
             final String lower = name.toLowerCase(NAME_LOCALE);
@@ -744,6 +734,17 @@ public final class ChannelDecoder extends Decoder {
     }
 
     /**
+     * Returns the netCDF variable of the given name, or {@code null} if none.
+     *
+     * @param  name  the name of the variable to search, or {@code null}.
+     * @return the variable of the given name, or {@code null} if none.
+     */
+    @Override
+    protected Variable findVariable(final String name) {
+        return findVariableInfo(name);
+    }
+
+    /**
      * Returns the variable of the given name. Note that groups do not exist in netCDF 3.
      *
      * @param  name  the name of the variable to search, or {@code null}.
@@ -751,7 +752,7 @@ public final class ChannelDecoder extends Decoder {
      */
     @Override
     protected Node findNode(final String name) {
-        return findVariable(name);
+        return findVariableInfo(name);
     }
 
     /**
@@ -894,24 +895,6 @@ public final class ChannelDecoder extends Decoder {
     }
 
     /**
-     * If this decoder can handle the file content as features, returns handlers for them.
-     *
-     * @return {@inheritDoc}
-     * @throws IOException if an I/O operation was necessary but failed.
-     * @throws DataStoreException if a logical error occurred.
-     */
-    @Override
-    public DiscreteSampling[] getDiscreteSampling() throws IOException, DataStoreException {
-        if ("trajectory".equalsIgnoreCase(stringValue(CF.FEATURE_TYPE))) try {
-            return FeaturesInfo.create(this);
-        } catch (IllegalArgumentException | ArithmeticException e) {
-            // Illegal argument is not a problem with content, but rather with configuration.
-            throw new DataStoreException(e.getLocalizedMessage(), e);
-        }
-        return new FeaturesInfo[0];
-    }
-
-    /**
      * Adds to the given set all variables of the given names. This operation is performed when the set of axes is
      * specified by a {@code "coordinates"} attribute associated to a data variable, or by customized conventions
      * specified by {@link org.apache.sis.internal.netcdf.Convention#namesOfAxisVariables(Variable)}.
@@ -926,7 +909,7 @@ public final class ChannelDecoder extends Decoder {
             return false;
         }
         for (final CharSequence name : names) {
-            final VariableInfo axis = findVariable(name.toString());
+            final VariableInfo axis = findVariableInfo(name.toString());
             if (axis == null) {
                 dimensions.clear();
                 axes.clear();
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 9067c2d..989e047 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
@@ -172,7 +172,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
      * @see #isEnumeration()
      * @see #meaning(int)
      *
-     * @todo Need to be consistent with {@code VariableWrapper}. We could move this field to {@link FeaturesInfo},
+     * @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.
      */
@@ -426,8 +426,11 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
 
     /**
      * Returns {@code true} if this variable is an enumeration.
+     *
+     * @see #meaning(int)
      */
-    final boolean isEnumeration() {
+    @Override
+    protected boolean isEnumeration() {
         return meanings != null;
     }
 
@@ -486,10 +489,22 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     }
 
     /**
+     * Returns the number of grid dimensions. This is the size of the {@link #getGridDimensions()} list.
+     *
+     * @return number of grid dimensions.
+     */
+    @Override
+    public int getNumDimensions() {
+        return dimensions.length;
+    }
+
+    /**
      * Returns the dimensions of this variable in the order they are declared in the netCDF file.
      * The dimensions are those of the grid, not the dimensions (or axes) of the coordinate system.
      * In ISO 19123 terminology, the {@linkplain Dimension#length() dimension lengths} give the upper
      * corner of the grid envelope plus one. The lower corner is always (0, 0, …, 0).
+     *
+     * @see #getNumDimensions()
      */
     @Override
     public List<Dimension> getGridDimensions() {
@@ -748,7 +763,8 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
      * @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.
      */
-    final String meaning(final int ordinal) {
+    @Override
+    protected String meaning(final int ordinal) {
         return (ordinal >= 0 && ordinal < meanings.length) ? meanings[ordinal] : null;
     }
 
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 d3cd549..ffb55e9 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
@@ -45,6 +45,7 @@ import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.internal.netcdf.Convention;
 import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.Variable;
+import org.apache.sis.internal.netcdf.Dimension;
 import org.apache.sis.internal.netcdf.Node;
 import org.apache.sis.internal.netcdf.Grid;
 import org.apache.sis.internal.netcdf.DiscreteSampling;
@@ -57,7 +58,7 @@ import org.apache.sis.storage.event.StoreListeners;
  * Provides netCDF decoding services based on the netCDF library.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.3
  * @module
  */
@@ -486,6 +487,31 @@ public final class DecoderWrapper extends Decoder implements CancelTask {
     }
 
     /**
+     * Returns the dimension of the given name (eventually ignoring case), or {@code null} if none.
+     * This method searches in all dimensions found in the netCDF file, regardless of variables.
+     *
+     * @param  dimName  the name of the dimension to search.
+     * @return dimension of the given name, or {@code null} if none.
+     */
+    @Override
+    protected Dimension findDimension(final String dimName) {
+        final ucar.nc2.Dimension dimension = file.findDimension(dimName);
+        return (dimension != null) ? new DimensionWrapper(dimension) : null;
+    }
+
+    /**
+     * Returns the netCDF variable of the given name, or {@code null} if none.
+     *
+     * @param  name  the name of the variable to search, or {@code null}.
+     * @return the variable of the given name, or {@code null} if none.
+     */
+    @Override
+    protected Variable findVariable(final String name) {
+        final VariableIF v = file.findVariable(name);
+        return (v != null) ? getWrapperFor(v) : null;
+    }
+
+    /**
      * Returns the variable or group of the given name.
      *
      * @param  name  name of the variable or group to search.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DimensionWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DimensionWrapper.java
index 4353814..06377f3 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DimensionWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DimensionWrapper.java
@@ -28,7 +28,7 @@ import ucar.nc2.Dimension;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
@@ -59,7 +59,7 @@ final class DimensionWrapper extends org.apache.sis.internal.netcdf.Dimension {
     /**
      * Wraps the given netCDF dimension object.
      */
-    private DimensionWrapper(final Dimension netcdf) {
+    DimensionWrapper(final Dimension netcdf) {
         this.netcdf = netcdf;
     }
 
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java
index 8d8b3cb..236060f 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java
@@ -29,11 +29,15 @@ import org.opengis.feature.FeatureType;
 
 /**
  * A wrapper around the UCAR {@code ucar.nc2.ft} package.
+ * Created by {@link DecoderWrapper#getDiscreteSampling()}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   0.8
  * @module
+ *
+ * @todo we do not yet have an example of file that {@link ucar.nc2.ft.FeatureDatasetFactoryManager} can handle
+ *       (maybe we don't use that class correctly).
  */
 final class FeaturesWrapper extends DiscreteSampling {
     /**
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 982f6ef..bde1f71 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
@@ -56,7 +56,7 @@ import org.apache.sis.measure.Units;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.3
  * @module
  */
@@ -212,6 +212,16 @@ final class VariableWrapper extends Variable {
     }
 
     /**
+     * Returns {@code true} if this variable is an enumeration.
+     *
+     * @see #meaning(int)
+     */
+    @Override
+    protected boolean isEnumeration() {
+        return variable.getDataType().isEnum() && (variable instanceof ucar.nc2.Variable);
+    }
+
+    /**
      * Returns whether this variable can grow. A variable is unlimited if at least one of its dimension is unlimited.
      */
     @Override
@@ -278,10 +288,23 @@ final class VariableWrapper extends Variable {
     }
 
     /**
+     * Returns the number of grid dimensions. This is the size of the {@link #getGridDimensions()} list
+     * but cheaper than a call to {@code getGridDimensions().size()}.
+     *
+     * @return number of grid dimensions.
+     */
+    @Override
+    public int getNumDimensions() {
+        return variable.getRank();
+    }
+
+    /**
      * Returns the dimensions of this variable in the order they are declared in the netCDF file.
      * The dimensions are those of the grid, not the dimensions (or axes) of the coordinate system.
      * In ISO 19123 terminology, the dimension lengths give the upper corner of the grid envelope plus one.
      * The lower corner is always (0, 0, …, 0).
+     *
+     * @see #getNumDimensions()
      */
     @Override
     public List<org.apache.sis.internal.netcdf.Dimension> getGridDimensions() {
@@ -506,6 +529,19 @@ final class VariableWrapper extends Variable {
     }
 
     /**
+     * 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 ((ucar.nc2.Variable) variable).lookupEnumString(ordinal);
+    }
+
+    /**
      * Returns {@code true} if this Apache SIS variable is a wrapper for the given UCAR variable.
      */
     final boolean isWrapperFor(final VariableIF v) {