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: Partial work toward parsing netCDF "grid_mapping" attributes.
Date Thu, 25 Apr 2019 22:41:57 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 0945e8a  Partial work toward parsing netCDF "grid_mapping" attributes.
0945e8a is described below

commit 0945e8a068e3b2b067a08d4c7d9b8007dc3be43b
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri Apr 26 00:41:25 2019 +0200

    Partial work toward parsing netCDF "grid_mapping" attributes.
---
 .../org/apache/sis/coverage/grid/GridGeometry.java |  17 +-
 .../apache/sis/internal/earth/netcdf/GCOM_C.java   | 102 ++++++++---
 .../org/apache/sis/internal/netcdf/CRSBuilder.java |   2 +-
 .../org/apache/sis/internal/netcdf/Convention.java | 112 ++++++++++++
 .../org/apache/sis/internal/netcdf/Decoder.java    |   8 +
 .../apache/sis/internal/netcdf/GridMapping.java    |  79 ++++++---
 .../apache/sis/internal/netcdf/NamedElement.java   |   2 +-
 .../java/org/apache/sis/internal/netcdf/Node.java  | 190 +++++++++++++++++++++
 .../org/apache/sis/internal/netcdf/Variable.java   | 153 +----------------
 .../sis/internal/netcdf/impl/ChannelDecoder.java   |  14 +-
 .../sis/internal/netcdf/ucar/DecoderWrapper.java   |  17 ++
 .../sis/internal/netcdf/ucar/GroupWrapper.java     |  78 +++++++++
 .../sis/internal/netcdf/ucar/VariableWrapper.java  |  16 +-
 13 files changed, 582 insertions(+), 208 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index d61e583..06d05c6 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -1150,7 +1150,7 @@ public class GridGeometry implements Serializable {
 
         /**
          * The section under the {@linkplain #root} where to write elements.
-         * This is updated when {@link #section(int, short, Object, boolean)} is invoked.
+         * This is updated when {@link #section(int, short, boolean)} is invoked.
          */
         private TreeTable.Node section;
 
@@ -1198,7 +1198,7 @@ public class GridGeometry implements Serializable {
              * ├─ Dimension 0: [370 … 389]  (20 cells)
              * └─ Dimension 1: [ 41 … 340] (300 cells)
              */
-            if (section(EXTENT, Vocabulary.Keys.GridExtent, extent, false)) {
+            if (section(EXTENT, Vocabulary.Keys.GridExtent, false)) {
                 extent.appendTo(buffer, vocabulary);
                 writeNodes();
             }
@@ -1207,7 +1207,7 @@ public class GridGeometry implements Serializable {
              * ├─ Geodetic latitude:  -69.75 … 80.25  Δφ = 0.5°
              * └─ Geodetic longitude:   4.75 … 14.75  Δλ = 0.5°
              */
-            if (section(ENVELOPE, Vocabulary.Keys.Envelope, envelope, false)) {
+            if (section(ENVELOPE, Vocabulary.Keys.Envelope, false)) {
                 final boolean appendResolution = (bitmask & RESOLUTION) != 0 && resolution != null;
                 final TableAppender table = new TableAppender(buffer, "");
                 final int dimension = envelope.getDimension();
@@ -1235,7 +1235,7 @@ public class GridGeometry implements Serializable {
                 }
                 GridExtent.flush(table);
                 writeNodes();
-            } else if (section(RESOLUTION, Vocabulary.Keys.Resolution, resolution, false)) {
+            } else if (section(RESOLUTION, Vocabulary.Keys.Resolution, false)) {
                 /*
                  * Example: Resolution
                  * └─ 0.5° × 0.5°
@@ -1251,7 +1251,7 @@ public class GridGeometry implements Serializable {
              * Example: Coordinate reference system
              * └─ EPSG:4326 — WGS 84 (φ,λ)
              */
-            if (section(CRS, Vocabulary.Keys.CoordinateRefSys, crs, false)) {
+            if (section(CRS, Vocabulary.Keys.CoordinateRefSys, false)) {
                 final Identifier id = IdentifiedObjects.getIdentifier(crs, null);
                 if (id != null) {
                     buffer.append(IdentifiedObjects.toString(id)).append(" — ");
@@ -1264,7 +1264,7 @@ public class GridGeometry implements Serializable {
              * └─ 2D → 2D non linear in 2
              */
             final Matrix matrix = MathTransforms.getMatrix(gridToCRS);
-            if (section(GRID_TO_CRS, Vocabulary.Keys.Conversion, gridToCRS, matrix != null)) {
+            if (section(GRID_TO_CRS, Vocabulary.Keys.Conversion, matrix != null)) {
                 if (matrix != null) {
                     writeNode(Matrices.toString(matrix));
                 } else {
@@ -1291,10 +1291,9 @@ public class GridGeometry implements Serializable {
          * @param  property    one of {@link #EXTENT}, {@link #ENVELOPE}, {@link #CRS}, {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
          * @param  title       the {@link Vocabulary} key for the title to show for this section, if formatted.
          * @param  cellCenter  whether to add a "origin in cell center" text in the title. This is relevant only for conversion.
-         * @param  value       the value to be formatted in that section.
          * @return {@code true} if the caller shall format the value.
          */
-        private boolean section(final int property, final short title, final Object value, final boolean cellCenter) {
+        private boolean section(final int property, final short title, final boolean cellCenter) {
             if ((bitmask & property) != 0) {
                 CharSequence text = vocabulary.getString(title);
                 if (cellCenter) {
@@ -1305,7 +1304,7 @@ public class GridGeometry implements Serializable {
                 }
                 section = root.newChild();
                 section.setValue(TableColumn.VALUE_AS_TEXT, text);
-                if (value != null) {
+                if (isDefined(property)) {
                     return true;
                 }
                 writeNode(vocabulary.getString(Vocabulary.Keys.Unspecified));
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
index a17d736..a541688 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
@@ -20,6 +20,7 @@ import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.regex.Pattern;
 import org.apache.sis.storage.netcdf.AttributeNames;
 import org.apache.sis.internal.netcdf.Convention;
@@ -27,8 +28,10 @@ import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.netcdf.VariableRole;
 import org.apache.sis.internal.netcdf.Linearizer;
+import org.apache.sis.internal.netcdf.Node;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
 import org.apache.sis.measure.NumberRange;
+import org.apache.sis.referencing.CommonCRS;
 
 
 /**
@@ -40,30 +43,30 @@ import org.apache.sis.measure.NumberRange;
  *     group: Geometry_data {
  *         variables:
  *             float Latitude(161, 126)
- *             string Unit = "degree"
- *             string Dim0 = "Line grids"
- *             string Dim1 = "Pixel grids"
- *             int Resampling_interval = 10
- *         float Longitude(161, 126)
- *             string Unit = "degree"
- *             string Dim0 = "Line grids"
- *             string Dim1 = "Pixel grids"
- *             int Resampling_interval = 10
+ *                 string Unit = "degree"
+ *                 string Dim0 = "Line grids"
+ *                 string Dim1 = "Pixel grids"
+ *                 int Resampling_interval = 10
+ *             float Longitude(161, 126)
+ *                 string Unit = "degree"
+ *                 string Dim0 = "Line grids"
+ *                 string Dim1 = "Pixel grids"
+ *                 int Resampling_interval = 10
  *     }
  *     group: Image_data {
  *         variables:
  *             ushort SST(1599, 1250)                   // Note: different size than (latitude, longitude) variables.
- *             string dim0 = "Line grids"
- *             string dim1 = "Piexl grids"              // Note: typo in "Pixel"
- *             ushort Error_DN           = 65535
- *             ushort Land_DN            = 65534
- *             ushort Cloud_error_DN     = 65533
- *             ushort Retrieval_error_DN = 65532
- *             ushort Maximum_valid_DN   = 65531
- *             ushort Minimum_valid_DN   = 0
- *             float  Slope              = 0.0012
- *             float  Offset             = -10
- *             string Unit               = "degree"
+ *                 string dim0 = "Line grids"
+ *                 string dim1 = "Piexl grids"              // Note: typo in "Pixel"
+ *                 ushort Error_DN           = 65535
+ *                 ushort Land_DN            = 65534
+ *                 ushort Cloud_error_DN     = 65533
+ *                 ushort Retrieval_error_DN = 65532
+ *                 ushort Maximum_valid_DN   = 65531
+ *                 ushort Minimum_valid_DN   = 0
+ *                 float  Slope              = 0.0012
+ *                 float  Offset             = -10
+ *                 string Unit               = "degree"
  *     }
  *     group: Global_attributes {
  *         string :Algorithm_developer = "Japan Aerospace Exploration Agency (JAXA)"
@@ -134,6 +137,11 @@ public final class GCOM_C extends Convention {
     }
 
     /**
+     * Name of the group defining map projection parameters, localization grid, <i>etc</i>.
+     */
+    private static final String GEOMETRY_DATA = "Geometry_data";
+
+    /**
      * Names of attributes for sample values having "no-data" meaning.
      * All those names have {@value #SUFFIX} suffix.
      */
@@ -242,7 +250,7 @@ public final class GCOM_C extends Convention {
              * In a future version we should probably keep them but store them in their own resource aggregate.
              */
             final String group = variable.getGroupName();
-            if ("Geometry_data".equalsIgnoreCase(group)) {
+            if (GEOMETRY_DATA.equalsIgnoreCase(group)) {
                 role = VariableRole.OTHER;
             }
         }
@@ -321,4 +329,56 @@ public final class GCOM_C extends Convention {
     public Set<Linearizer> linearizers(final Decoder decoder) {
         return Collections.singleton(Linearizer.GROUND_TRACK);
     }
+
+    /**
+     * Returns the name of nodes (variables or groups) that may define the map projection parameters.
+     * For GCOM files, this is {@value #GEOMETRY_DATA}.
+     *
+     * @param  data  the variable for which to get the grid mapping node.
+     * @return name of nodes that may contain the grid mapping, or an empty set if none.
+     */
+    @Override
+    public Set<String> gridMapping(final Variable data) {
+        final Set<String> names = new LinkedHashSet<>(4);
+        names.add(GEOMETRY_DATA);
+        names.addAll(super.gridMapping(data));              // Fallback if geometry data does not exist.
+        return names;
+    }
+
+    /**
+     * Returns the map projection definition for the given data variable.
+     * This method expects the following attribute names in the {@value #GEOMETRY_DATA} group:
+     *
+     * {@preformat text
+     *     group: Geometry_data {
+     *         // group attributes:
+     *         string Image_projection      = "EQA (sinusoidal equal area) projection from 0-deg longitude"
+     *         float  Upper_left_longitude  = 115.17541
+     *         float  Upper_left_latitude   =  80.0
+     *         float  Upper_right_longitude = 172.7631
+     *         float  Upper_right_latitude  =  80.0
+     *         float  Lower_left_longitude  =  58.47609
+     *         float  Lower_left_latitude   =  70.0
+     *         float  Lower_right_longitude =  87.714134
+     *         float  Lower_right_latitude  =  70.0
+     *     }
+     * }
+     *
+     * @param  node  the group of variables from which to read attributes.
+     * @return the map projection definition, or {@code null} if none.
+     */
+    @Override
+    public Map<String,Object> projection(final Node node) {
+        String method = node.getAttributeAsString("Image_projection");
+        if (method != null) {
+            if (method.matches("EQA\\b.*")) {
+                method = "Sinusoidal";
+                final Map<String,Object> definition = new HashMap<>(4);
+                definition.put(BASE_CRS, CommonCRS.WGS84.geographic());
+                definition.put("grid_mapping_name", method);
+                return definition;
+            }
+        }
+        return super.projection(node);
+    }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
index 3a62463..a214e3f 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
@@ -86,7 +86,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
      * clearly said "Unknown datum based upon the GRS 1980 ellipsoid" and for consistency with
      * {@link CommonCRS#SPHERE}, which also use GRS 1980.</div>
      */
-    private static final CommonCRS DEFAULT = CommonCRS.GRS1980;
+    static final CommonCRS DEFAULT = CommonCRS.GRS1980;
 
     /**
      * The type of datum as a GeoAPI sub-interface of {@link Datum}.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
index da1e979..55ffd26 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
@@ -18,16 +18,22 @@ package org.apache.sis.internal.netcdf;
 
 import java.util.Map;
 import java.util.Set;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Collections;
 import java.util.LinkedHashMap;
+import java.util.Locale;
 import java.awt.image.DataBuffer;
+import org.opengis.referencing.crs.GeographicCRS;
+import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.internal.referencing.LazySet;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
 import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.util.Numbers;
+import org.apache.sis.util.resources.Errors;
 import ucar.nc2.constants.CDM;
+import ucar.nc2.constants.CF;
 
 
 /**
@@ -464,4 +470,110 @@ public class Convention {
     public Set<Linearizer> linearizers(final Decoder decoder) {
         return Collections.emptySet();
     }
+
+    /**
+     * Returns the name of nodes (variables or groups) that may define the map projection parameters.
+     * The variables or groups will be inspected in the order they are declared in the returned set.
+     * For each string in the set, {@link Decoder#findNode(String)} is invoked.
+     *
+     * <p>The default implementation returns the value of {@link CF#GRID_MAPPING}, or an empty set
+     * if the given variable does not contain that attribute.</p>
+     *
+     * @param  data  the variable for which to get the grid mapping node.
+     * @return name of nodes that may contain the grid mapping, or an empty set if none.
+     */
+    public Set<String> gridMapping(final Variable data) {
+        final String mapping = data.getAttributeAsString(CF.GRID_MAPPING);
+        return (mapping != null) ? Collections.singleton(mapping) : Collections.emptySet();
+    }
+
+    /**
+     * Key associated to {@link GeographicCRS} value in the map returned by {@link #projection(Node)}.
+     */
+    protected static final String BASE_CRS = "base_crs";
+
+    /**
+     * Key associated to {@link MathTransform} value in the map returned by {@link #projection(Node)}.
+     */
+    protected static final String GRID_TO_CRS = "grid_to_crs";
+
+    /**
+     * Returns the map projection defined by the given node.  The given {@code node} is typically the variable named by a
+     * {@value CF#GRID_MAPPING} attribute (this is the preferred, CF-compliant approach), or if no grid mapping attribute
+     * is found {@code node} may be directly the data variable (non CF-compliant, but found in practice).
+     * If non-null, the returned map contains the following information:
+     *
+     * <table class="sis">
+     *   <caption>Content of the returned map</caption>
+     *   <tr>
+     *     <th>Key</th>
+     *     <th>Value type</th>
+     *     <th>Description</th>
+     *     <th>Default value</th>
+     *   </tr><tr>
+     *     <td>{@value CF#GRID_MAPPING_NAME}</td>
+     *     <td>{@link String}</td>
+     *     <td>Operation method name</td>
+     *     <td>Value of {@value CF#GRID_MAPPING_NAME} attribute.</td>
+     *   </tr><tr>
+     *     <td>{@value #BASE_CRS}</td>
+     *     <td>{@link GeographicCRS}</td>
+     *     <td>Base CRS of the map projection</td>
+     *     <td>Unknown datum based upon the GRS 1980 ellipsoid.</td>
+     *   </tr><tr>
+     *     <td>{@code "*_name"}</td>
+     *     <td>{@link String}</td>
+     *     <td>Name of a component (datum, base CRS, …)</td>
+     *     <td>Attributes found on grid mapping variable.</td>
+     *   </tr><tr>
+     *     <td>(projection-dependent)</td>
+     *     <td>{@link Number} or {@code double[]}</td>
+     *     <td>Map projection parameter values</td>
+     *     <td>Attributes found on grid mapping variable.</td>
+     *   </tr><tr>
+     *     <td>{@value #GRID_TO_CRS}</td>
+     *     <td>{@link MathTransform}</td>
+     *     <td>Conversion from pixel indices to CRS.</td>
+     *     <td>None (not from CF-convention).</td>
+     *   </tr>
+     * </table>
+     *
+     * Subclasses can override this method for example in order to override the {@value #BASE_CRS} attribute
+     * if they know that a particular product is based on "World Geodetic System 1984" or other datum.
+     *
+     * @param  node  the {@value CF#GRID_MAPPING} variable (preferred) or the data variable (as a fallback) from which to read attributes.
+     * @return the map projection definition, or {@code null} if none.
+     */
+    public Map<String,Object> projection(final Node node) {
+        final String method = node.getAttributeAsString(CF.GRID_MAPPING_NAME);
+        if (method == null) {
+            return null;
+        }
+        final Map<String,Object> definition = new HashMap<>();
+        definition.put(CF.GRID_MAPPING_NAME, method);
+        definition.put(BASE_CRS, CRSBuilder.DEFAULT.geographic());
+        for (final String name : node.getAttributeNames()) {
+            final String ln = name.toLowerCase(Locale.US);
+            final Object value;
+            if (ln.endsWith("_name")) {
+                value = node.getAttributeAsString(name);
+                if (value == null) continue;
+                break;
+            } else switch (ln) {
+                case CF.GRID_MAPPING_NAME: continue;        // Already stored.
+                case "towgs84":            continue;        // TODO: process in a future version.
+                case "crs_wkt":            continue;        // TODO: process in a future version.
+                default: {
+                    final double n = node.getAttributeAsNumber(name);
+                    if (Double.isNaN(n)) continue;
+                    value = n;
+                    break;
+                }
+            }
+            if (definition.putIfAbsent(name, value) != null) {
+                node.error(Convention.class, "projection", null, Errors.Keys.DuplicatedIdentifier_1, name);
+            }
+        }
+        return definition;
+    }
 }
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 75c575f..6e069df 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
@@ -379,4 +379,12 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
             list.add(crs);
         }
     }
+
+    /**
+     * 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.
+     * @return the variable or group of the given name, or {@code null} if none.
+     */
+    protected abstract Node findNode(String name);
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java
index ea7a691..eb9628e 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java
@@ -21,12 +21,21 @@ import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.text.ParseException;
+import java.util.Collections;
 import org.opengis.util.FactoryException;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.cs.CartesianCS;
 import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.crs.ProjectedCRS;
+import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.CoordinateOperationFactory;
+import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.datum.PixelInCell;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.crs.AbstractCRS;
 import org.apache.sis.referencing.cs.AxesConvention;
@@ -101,19 +110,20 @@ final class GridMapping {
      */
     static GridMapping forVariable(final Variable variable) {
         final Map<Object,GridMapping> gridMapping = variable.decoder.gridMapping;
-        final String name = variable.getAttributeAsString(CF.GRID_MAPPING);
-        if (name != null) {
+        for (final String name : variable.decoder.convention().gridMapping(variable)) {
             GridMapping gm = gridMapping.get(name);
             if (gm != null) {
                 return gm;
             }
-            for (final Variable mapping : variable.decoder.getVariables()) {
-                if (name.equals(mapping.getName())) {
+            final Node mapping = variable.decoder.findNode(name);
+            if (mapping != null) {
+                gm = parseProjectionParameters(mapping);
+                if (gm == null) {
                     gm = parseGeoTransform(mapping);
-                    if (gm != null) {
-                        gridMapping.put(name, gm);
-                        return gm;
-                    }
+                }
+                if (gm != null) {
+                    gridMapping.put(name, gm);
+                    return gm;
                 }
             }
         }
@@ -123,7 +133,10 @@ final class GridMapping {
          */
         GridMapping gm = gridMapping.get(variable);
         if (gm == null) {
-            gm = parseNonStandard(variable);
+            gm = parseProjectionParameters(variable);
+            if (gm == null) {
+                gm = parseNonStandard(variable);
+            }
             if (gm != null) {
                 gridMapping.put(variable, gm);
             }
@@ -132,6 +145,30 @@ final class GridMapping {
     }
 
     /**
+     * If the netCDF variable defines explicitly the map projection method and its parameters, returns those parameters.
+     * Otherwise returns {@code null}.
+     */
+    private static GridMapping parseProjectionParameters(final Node node) {
+        final Map<String, Object> definition = node.decoder.convention().projection(node);
+        if (definition != null) try {
+            final CoordinateOperationFactory factory = node.decoder.getCoordinateOperationFactory();
+            final OperationMethod method = factory.getOperationMethod((String) definition.get(CF.GRID_MAPPING_NAME));
+            final ParameterValueGroup parameters = method.getParameters().createValue();
+            // TODO: set parameter values.
+            final Map<String,?> name = Collections.singletonMap(Conversion.NAME_KEY, "NetCDF projection");      // TODO: find a better name.
+            final Conversion conversion = factory.createDefiningConversion(name, method, parameters);
+
+            final GeographicCRS baseCRS = (GeographicCRS) definition.get(Convention.BASE_CRS);
+            final CartesianCS cs = CommonCRS.WGS84.universal(0,0).getCoordinateSystem();                     // TODO
+            final ProjectedCRS crs = node.decoder.getCRSFactory().createProjectedCRS(name, baseCRS, conversion, cs);
+            return new GridMapping(crs, null, false);
+        } catch (ClassCastException | IllegalArgumentException | FactoryException e) {
+            canNotCreate(node, Resources.Keys.CanNotCreateCRS_3, e);
+        }
+        return null;
+    }
+
+    /**
      * Tries to parse a CRS and affine transform from GDAL GeoTransform coefficients.
      * Those coefficients are not in the usual order expected by matrix, affine
      * transforms or TFW files. The relationship from pixel/line (P,L) coordinates
@@ -145,7 +182,7 @@ final class GridMapping {
      * @param  mapping  the variable that contains attributes giving CRS definition.
      * @return the mapping, or {@code null} if this method did not found grid geometry attributes.
      */
-    private static GridMapping parseGeoTransform(final Variable mapping) {
+    private static GridMapping parseGeoTransform(final Node mapping) {
         final String wkt = mapping.getAttributeAsString("spatial_ref");
         final String gtr = mapping.getAttributeAsString("GeoTransform");
         if (wkt == null && gtr == null) {
@@ -177,12 +214,12 @@ final class GridMapping {
 
     /**
      * Tries to parse the Coordinate Reference System using ESRI conventions or other non-CF conventions.
-     * This method is invoked as a fallback if {@link #parseGeoTransform(Variable)} found no grid geometry.
+     * This method is invoked as a fallback if {@link #parseGeoTransform(Node)} found no grid geometry.
      *
      * @param  variable  the variable potentially with attributes to parse.
      * @return whether this method found grid geometry attributes.
      */
-    private static GridMapping parseNonStandard(final Variable variable) {
+    private static GridMapping parseNonStandard(final Node variable) {
         boolean isEPSG = false;
         String code = variable.getAttributeAsString("ESRI_pe_string");
         if (code == null) {
@@ -217,8 +254,8 @@ final class GridMapping {
      * Creates a coordinate reference system by parsing a Well Known Text (WKT) string. The WKT is presumed
      * to use the GDAL flavor of WKT 1, and warnings are redirected to decoder listeners.
      */
-    private static CoordinateReferenceSystem createFromWKT(final Variable variable, final String wkt) throws ParseException {
-        final WKTFormat f = new WKTFormat(variable.getLocale(), variable.decoder.getTimeZone());
+    private static CoordinateReferenceSystem createFromWKT(final Node node, final String wkt) throws ParseException {
+        final WKTFormat f = new WKTFormat(node.getLocale(), node.decoder.getTimeZone());
         f.setConvention(org.apache.sis.io.wkt.Convention.WKT1_COMMON_UNITS);
         final CoordinateReferenceSystem crs = (CoordinateReferenceSystem) f.parseObject(wkt);
         final Warnings warnings = f.getWarnings();
@@ -227,7 +264,7 @@ final class GridMapping {
             record.setLoggerName(Modules.NETCDF);
             record.setSourceClassName(Variable.class.getCanonicalName());
             record.setSourceMethodName("getGridGeometry");
-            variable.decoder.listeners.warning(record);
+            node.decoder.listeners.warning(record);
         }
         return crs;
     }
@@ -239,9 +276,9 @@ final class GridMapping {
      * @param  key  one of {@link Resources.Keys#CanNotCreateCRS_3} or {@link Resources.Keys#CanNotCreateGridGeometry_3}.
      * @param  ex   the exception that occurred while creating the CRS or grid geometry.
      */
-    private static void canNotCreate(final Variable variable, final short key, final Exception ex) {
-        NamedElement.warning(variable.decoder.listeners, Variable.class, "getGridGeometry", ex, null,
-                key, variable.decoder.getFilename(), variable.getName(), ex.getLocalizedMessage());
+    private static void canNotCreate(final Node node, final short key, final Exception ex) {
+        NamedElement.warning(node.decoder.listeners, Variable.class, "getGridGeometry", ex, null,
+                key, node.decoder.getFilename(), node.getName(), ex.getLocalizedMessage());
     }
 
     /**
@@ -275,7 +312,6 @@ final class GridMapping {
             final CoordinateReferenceSystem templateCRS = template.getCoordinateReferenceSystem();
             if (givenCRS == null) {
                 givenCRS = templateCRS;
-                isSameGrid = false;
             } else {
                 /*
                  * The CRS built by Grid may have a different axis order than the CRS specified by grid mapping attributes.
@@ -312,7 +348,7 @@ final class GridMapping {
                     if (c != null) components[count++] = c;
                 }
                 switch (count) {
-                    case 0: break;                                          // Should never happen.
+                    case 0: /* Keep givenCRS as-is */ break;                // Should never happen.
                     case 1: givenCRS = components[0]; break;
                     default: {
                         components = ArraysExt.resize(components, count);
@@ -328,7 +364,7 @@ final class GridMapping {
                 }
                 isSameGrid = templateCRS.equals(givenCRS);
                 if (isSameGrid) {
-                    givenCRS = templateCRS;                                 // Keep using existing instance if appropriate.
+                    givenCRS = templateCRS;                                 // Keep existing instance if appropriate.
                 }
             }
         }
@@ -343,7 +379,6 @@ final class GridMapping {
             final MathTransform templateG2C = template.getGridToCRS(anchor);
             if (givenG2C == null) {
                 givenG2C = templateG2C;
-                isSameGrid = false;
             } else try {
                 int count = 0;
                 MathTransform[] components = new MathTransform[3];
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java
index dce6207..b2a1ca3 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java
@@ -28,7 +28,7 @@ import org.apache.sis.util.resources.IndexedResourceBundle;
 
 
 /**
- * Base class of netCDF dimension, variable or attribute.
+ * Base class of netCDF dimension, variable or axis or grid.
  * All those objects share in common a {@link #getName()} method.
  *
  * @author  Martin Desruisseaux (Geomatys)
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Node.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Node.java
new file mode 100644
index 0000000..0135eef
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Node.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.netcdf;
+
+import java.util.Locale;
+import java.util.Collection;
+import org.apache.sis.math.DecimalFunctions;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * Base class of variables or groups.
+ * The common characteristic of those objects is to have attributes.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public abstract class Node extends NamedElement {
+    /**
+     * The netCDF file where this node is stored.
+     */
+    protected final Decoder decoder;
+
+    /**
+     * Creates a new node.
+     *
+     * @param decoder  the netCDF file where this node is stored.
+     */
+    protected Node(final Decoder decoder) {
+        this.decoder = decoder;
+    }
+
+    /**
+     * Returns the names of all attributes associated to this variable.
+     *
+     * @return names of all attributes associated to this variable.
+     */
+    public abstract Collection<String> getAttributeNames();
+
+    /**
+     * Returns the type of the attribute of the given name,
+     * or {@code null} if the given attribute is not found.
+     *
+     * @param  attributeName  the name of the attribute for which to get the type.
+     * @return type of the given attribute, or {@code null} if the attribute does not exist.
+     *
+     * @see Variable#getDataType()
+     */
+    public abstract Class<?> getAttributeType(String attributeName);
+
+    /**
+     * Returns the sequence of values for the given attribute, or an empty array if none.
+     * The elements will be of class {@link String} if {@code numeric} is {@code false},
+     * or {@link Number} if {@code numeric} is {@code true}. Some elements may be null
+     * if they are not of the expected type.
+     *
+     * @param  attributeName  the name of the attribute for which to get the values.
+     * @param  numeric        {@code true} if the values are expected to be numeric, or {@code false} for strings.
+     * @return the sequence of {@link String} or {@link Number} values for the named attribute.
+     *         May contain null elements.
+     */
+    public abstract Object[] getAttributeValues(String attributeName, boolean numeric);
+
+    /**
+     * Returns the singleton value for the given attribute, or {@code null} if none or ambiguous.
+     *
+     * @param  attributeName  the name of the attribute for which to get the value.
+     * @param  numeric        {@code true} if the value is expected to be numeric, or {@code false} for string.
+     * @return the {@link String} or {@link Number} value for the named attribute.
+     */
+    private Object getAttributeValue(final String attributeName, final boolean numeric) {
+        Object singleton = null;
+        for (final Object value : getAttributeValues(attributeName, numeric)) {
+            if (value != null) {
+                if (singleton != null && !singleton.equals(value)) {              // Paranoiac check.
+                    return null;
+                }
+                singleton = value;
+            }
+        }
+        return singleton;
+    }
+
+    /**
+     * Returns the value of the given attribute as a non-blank string with leading/trailing spaces removed.
+     * This is a convenience method for {@link #getAttributeValues(String, boolean)} when a singleton value
+     * is expected and blank strings ignored.
+     *
+     * @param  attributeName  the name of the attribute for which to get the value.
+     * @return the singleton attribute value, or {@code null} if none, empty, blank or ambiguous.
+     */
+    public String getAttributeAsString(final String attributeName) {
+        final Object value = getAttributeValue(attributeName, false);
+        if (value != null) {
+            final String text = value.toString().trim();
+            if (!text.isEmpty()) return text;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the value of the given attribute as a number, or {@link Double#NaN}.
+     * If the number is stored with single-precision, it is assumed casted from a
+     * representation in base 10.
+     *
+     * @param  attributeName  the name of the attribute for which to get the value.
+     * @return the singleton attribute value, or {@code NaN} if none or ambiguous.
+     */
+    public final double getAttributeAsNumber(final String attributeName) {
+        final Object value = getAttributeValue(attributeName, true);
+        if (value instanceof Number) {
+            double dp = ((Number) value).doubleValue();
+            final float sp = (float) dp;
+            if (sp == dp) {                              // May happen even if the number was stored as a double.
+                dp = DecimalFunctions.floatToDouble(sp);
+            }
+            return dp;
+        }
+        return Double.NaN;
+    }
+
+    /**
+     * Returns the locale to use for warnings and error messages.
+     *
+     * @return the locale for warnings and error messages.
+     */
+    protected final Locale getLocale() {
+        return decoder.listeners.getLocale();
+    }
+
+    /**
+     * Returns the resources to use for warnings or error messages.
+     *
+     * @return the resources for the locales specified to the decoder.
+     */
+    protected final Resources resources() {
+        return Resources.forLocale(getLocale());
+    }
+
+    /**
+     * Returns the resources to use for error messages.
+     *
+     * @return the resources for error messages using the locales specified to the decoder.
+     */
+    final Errors errors() {
+        return Errors.getResources(getLocale());
+    }
+
+    /**
+     * Reports a warning to the listeners specified at construction time.
+     * This method is for Apache SIS internal purpose only since resources may change at any time.
+     *
+     * @param  caller     the caller class to report, preferably a public class.
+     * @param  method     the caller method to report, preferable a public method.
+     * @param  key        one or {@link Resources.Keys} constants.
+     * @param  arguments  values to be formatted in the {@link java.text.MessageFormat} pattern.
+     */
+    protected final void warning(final Class<?> caller, final String method, final short key, final Object... arguments) {
+        warning(decoder.listeners, caller, method, null, null, key, arguments);
+    }
+
+    /**
+     * Reports a warning to the listeners specified at construction time.
+     *
+     * @param  caller     the caller class to report, preferably a public class.
+     * @param  method     the caller method to report, preferable a public method.
+     * @param  exception  the exception that occurred, or {@code null} if none.
+     * @param  key        one or {@link Errors.Keys} constants.
+     * @param  arguments  values to be formatted in the {@link java.text.MessageFormat} pattern.
+     */
+    final void error(final Class<?> caller, final String method, final Exception exception, final short key, final Object... arguments) {
+        warning(decoder.listeners, caller, method, exception, errors(), key, arguments);
+    }
+}
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 e2d74b8..fcf6b98 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
@@ -20,7 +20,6 @@ import java.util.Set;
 import java.util.Map;
 import java.util.HashSet;
 import java.util.HashMap;
-import java.util.Collection;
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Locale;
@@ -36,7 +35,6 @@ import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.math.Vector;
 import org.apache.sis.math.MathFunctions;
-import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.ArraysExt;
@@ -57,7 +55,7 @@ import ucar.nc2.constants.CF;
  * @since   0.3
  * @module
  */
-public abstract class Variable extends NamedElement {
+public abstract class Variable extends Node {
     /**
      * Pool of vectors created by the {@link #read()} method. This pool is used for sharing netCDF coordinate axes,
      * since the same vectors tend to be repeated in many netCDF files produced by the same data producer. Because
@@ -68,11 +66,6 @@ public abstract class Variable extends NamedElement {
     protected static final WeakHashSet<Vector> SHARED_VECTORS = new WeakHashSet<>(Vector.class);
 
     /**
-     * The netCDF file where this variable is stored.
-     */
-    protected final Decoder decoder;
-
-    /**
      * The pattern to use for parsing temporal units of the form "days since 1970-01-01 00:00:00".
      *
      * @see #parseUnit(String)
@@ -145,7 +138,7 @@ public abstract class Variable extends NamedElement {
      * @param decoder  the netCDF file where this variable is stored.
      */
     protected Variable(final Decoder decoder) {
-        this.decoder = decoder;
+        super(decoder);
     }
 
     /**
@@ -792,95 +785,6 @@ public abstract class Variable extends NamedElement {
     public abstract List<Dimension> getGridDimensions();
 
     /**
-     * Returns the names of all attributes associated to this variable.
-     *
-     * @return names of all attributes associated to this variable.
-     */
-    public abstract Collection<String> getAttributeNames();
-
-    /**
-     * Returns the type of the attribute of the given name,
-     * or {@code null} if the given attribute is not found.
-     *
-     * @param  attributeName  the name of the attribute for which to get the type.
-     * @return type of the given attribute, or {@code null} if the attribute does not exist.
-     *
-     * @see #getDataType()
-     */
-    public abstract Class<?> getAttributeType(String attributeName);
-
-    /**
-     * Returns the sequence of values for the given attribute, or an empty array if none.
-     * The elements will be of class {@link String} if {@code numeric} is {@code false},
-     * or {@link Number} if {@code numeric} is {@code true}. Some elements may be null
-     * if they are not of the expected type.
-     *
-     * @param  attributeName  the name of the attribute for which to get the values.
-     * @param  numeric        {@code true} if the values are expected to be numeric, or {@code false} for strings.
-     * @return the sequence of {@link String} or {@link Number} values for the named attribute.
-     *         May contain null elements.
-     */
-    public abstract Object[] getAttributeValues(String attributeName, boolean numeric);
-
-    /**
-     * Returns the singleton value for the given attribute, or {@code null} if none or ambiguous.
-     *
-     * @param  attributeName  the name of the attribute for which to get the value.
-     * @param  numeric        {@code true} if the value is expected to be numeric, or {@code false} for string.
-     * @return the {@link String} or {@link Number} value for the named attribute.
-     */
-    private Object getAttributeValue(final String attributeName, final boolean numeric) {
-        Object singleton = null;
-        for (final Object value : getAttributeValues(attributeName, numeric)) {
-            if (value != null) {
-                if (singleton != null && !singleton.equals(value)) {              // Paranoiac check.
-                    return null;
-                }
-                singleton = value;
-            }
-        }
-        return singleton;
-    }
-
-    /**
-     * Returns the value of the given attribute as a non-blank string with leading/trailing spaces removed.
-     * This is a convenience method for {@link #getAttributeValues(String, boolean)} when a singleton value
-     * is expected and blank strings ignored.
-     *
-     * @param  attributeName  the name of the attribute for which to get the value.
-     * @return the singleton attribute value, or {@code null} if none, empty, blank or ambiguous.
-     */
-    public String getAttributeAsString(final String attributeName) {
-        final Object value = getAttributeValue(attributeName, false);
-        if (value != null) {
-            final String text = value.toString().trim();
-            if (!text.isEmpty()) return text;
-        }
-        return null;
-    }
-
-    /**
-     * Returns the value of the given attribute as a number, or {@link Double#NaN}.
-     * If the number is stored with single-precision, it is assumed casted from a
-     * representation in base 10.
-     *
-     * @param  attributeName  the name of the attribute for which to get the value.
-     * @return the singleton attribute value, or {@code NaN} if none or ambiguous.
-     */
-    public final double getAttributeAsNumber(final String attributeName) {
-        final Object value = getAttributeValue(attributeName, true);
-        if (value instanceof Number) {
-            double dp = ((Number) value).doubleValue();
-            final float sp = (float) dp;
-            if (sp == dp) {                              // May happen even if the number was stored as a double.
-                dp = DecimalFunctions.floatToDouble(sp);
-            }
-            return dp;
-        }
-        return Double.NaN;
-    }
-
-    /**
      * Returns the range of valid values, or {@code null} if unknown. This is a shortcut for
      * {@link Convention#validRange(Variable)} with a fallback on {@link #getRangeFallback()}.
      *
@@ -1137,59 +1041,6 @@ public abstract class Variable extends NamedElement {
     }
 
     /**
-     * Returns the locale to use for warnings and error messages.
-     *
-     * @return the locale for warnings and error messages.
-     */
-    protected final Locale getLocale() {
-        return decoder.listeners.getLocale();
-    }
-
-    /**
-     * Returns the resources to use for warnings or error messages.
-     *
-     * @return the resources for the locales specified to the decoder.
-     */
-    protected final Resources resources() {
-        return Resources.forLocale(getLocale());
-    }
-
-    /**
-     * Returns the resources to use for error messages.
-     *
-     * @return the resources for error messages using the locales specified to the decoder.
-     */
-    final Errors errors() {
-        return Errors.getResources(getLocale());
-    }
-
-    /**
-     * Reports a warning to the listeners specified at construction time.
-     * This method is for Apache SIS internal purpose only since resources may change at any time.
-     *
-     * @param  caller     the caller class to report, preferably a public class.
-     * @param  method     the caller method to report, preferable a public method.
-     * @param  key        one or {@link Resources.Keys} constants.
-     * @param  arguments  values to be formatted in the {@link java.text.MessageFormat} pattern.
-     */
-    protected final void warning(final Class<?> caller, final String method, final short key, final Object... arguments) {
-        warning(decoder.listeners, caller, method, null, null, key, arguments);
-    }
-
-    /**
-     * Reports a warning to the listeners specified at construction time.
-     *
-     * @param  caller     the caller class to report, preferably a public class.
-     * @param  method     the caller method to report, preferable a public method.
-     * @param  exception  the exception that occurred, or {@code null} if none.
-     * @param  key        one or {@link Errors.Keys} constants.
-     * @param  arguments  values to be formatted in the {@link java.text.MessageFormat} pattern.
-     */
-    final void error(final Class<?> caller, final String method, final Exception exception, final short key, final Object... arguments) {
-        warning(decoder.listeners, caller, method, exception, errors(), key, arguments);
-    }
-
-    /**
      * Appends the name of the variable data type as the name of the primitive type
      * followed by the span of each dimension (in unit of grid cells) between brackets.
      * Dimensions are listed in "natural" order (reverse of netCDF order).
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 e9fa786..70113e6 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
@@ -44,6 +44,7 @@ import javax.measure.format.ParserException;
 import org.opengis.parameter.InvalidParameterCardinalityException;
 import org.apache.sis.internal.netcdf.DataType;
 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.NamedElement;
@@ -714,7 +715,7 @@ 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 attribute value, or {@code null} if none.
+     * @return the variable of the given name, or {@code null} if none.
      */
     final VariableInfo findVariable(final String name) {
         VariableInfo v = variableMap.get(name);
@@ -729,6 +730,17 @@ public final class ChannelDecoder extends Decoder {
     }
 
     /**
+     * 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}.
+     * @return the variable of the given name, or {@code null} if none.
+     */
+    @Override
+    protected Node findNode(final String name) {
+        return findVariable(name);
+    }
+
+    /**
      * Returns the netCDF attribute of the given name, or {@code null} if none. This method is invoked
      * for every global attributes to be read by this class (but not {@linkplain VariableInfo variable}
      * attributes), thus providing a single point where we can filter the attributes to be read.
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 cb011c1..a5d9fe2 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
@@ -44,6 +44,7 @@ import org.apache.sis.util.logging.WarningListeners;
 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.Node;
 import org.apache.sis.internal.netcdf.Grid;
 import org.apache.sis.internal.netcdf.DiscreteSampling;
 import org.apache.sis.setup.GeometryLibrary;
@@ -484,6 +485,22 @@ public final class DecoderWrapper extends Decoder implements CancelTask {
     }
 
     /**
+     * Returns the variable or group of the given name.
+     *
+     * @param  name  name of the variable or group to search.
+     * @return the variable or group of the given name, or {@code null} if none.
+     */
+    @Override
+    protected Node findNode(final String name) {
+        final VariableIF v = file.findVariable(name);
+        if (v != null) {
+            return getWrapperFor(v);
+        }
+        final Group group = file.findGroup(name);
+        return (group != null) ? new GroupWrapper(this, group) : null;
+    }
+
+    /**
      * Invoked by the UCAR netCDF library for checking if the reading process has been canceled.
      * This method returns the {@link #canceled} flag.
      *
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GroupWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GroupWrapper.java
new file mode 100644
index 0000000..2a7dc45
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GroupWrapper.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.netcdf.ucar;
+
+import java.util.Collection;
+import ucar.nc2.Group;
+import org.apache.sis.internal.netcdf.Node;
+import org.apache.sis.internal.netcdf.Decoder;
+
+
+/**
+ * Wrapper for a netCDF group.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class GroupWrapper extends Node {
+    /**
+     * The netCDF group.
+     */
+    private final Group group;
+
+    /**
+     * Creates a new node wrapping the given netCDF group.
+     */
+    GroupWrapper(final Decoder decoder, final Group node) {
+        super(decoder);
+        group = node;
+    }
+
+    /**
+     * Returns the name of this group.
+     */
+    @Override
+    public String getName() {
+        return group.getShortName();
+    }
+
+    /**
+     * Returns the names of all attributes associated to this node.
+     */
+    @Override
+    public Collection<String> getAttributeNames() {
+        return VariableWrapper.toNames(group.getAttributes());
+    }
+
+    /**
+     * Returns the type of the attribute of the given name, or {@code null}.
+     */
+    @Override
+    public Class<?> getAttributeType(final String attributeName) {
+        return VariableWrapper.getAttributeType(group.findAttributeIgnoreCase(attributeName));
+    }
+
+    /**
+     * Returns the sequence of values for the given attribute, or an empty array if none.
+     */
+    @Override
+    public Object[] getAttributeValues(final String attributeName, final boolean numeric) {
+        return VariableWrapper.getAttributeValues(group.findAttributeIgnoreCase(attributeName), numeric);
+    }
+}
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 39f25c1..497710d 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
@@ -303,7 +303,13 @@ final class VariableWrapper extends Variable {
      */
     @Override
     public Class<?> getAttributeType(final String attributeName) {
-        final Attribute attribute = raw.findAttributeIgnoreCase(attributeName);
+        return getAttributeType(raw.findAttributeIgnoreCase(attributeName));
+    }
+
+    /**
+     * Implementation of {@link #getAttributeType(String)} shared with {@link GroupWrapper}.
+     */
+    static Class<?> getAttributeType(final Attribute attribute) {
         if (attribute != null) {
             switch (attribute.getDataType()) {
                 case BYTE:   return Byte.class;
@@ -326,7 +332,13 @@ final class VariableWrapper extends Variable {
      */
     @Override
     public Object[] getAttributeValues(final String attributeName, final boolean numeric) {
-        final Attribute attribute = raw.findAttributeIgnoreCase(attributeName);
+        return getAttributeValues(raw.findAttributeIgnoreCase(attributeName), numeric);
+    }
+
+    /**
+     * Implementation of {@link #getAttributeValues(String, boolean)} shared with {@link GroupWrapper}.
+     */
+    static Object[] getAttributeValues(final Attribute attribute, final boolean numeric) {
         if (attribute != null) {
             boolean hasValues = false;
             final Object[] values = new Object[attribute.getLength()];


Mime
View raw message