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 2019/04/25 22:41:57 UTC
[sis] branch geoapi-4.0 updated: Partial work toward parsing netCDF
"grid_mapping" attributes.
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 <ma...@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()];