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 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, etc. + */ + 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 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 gridMapping(final Variable data) { + final Set 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 projection(final Node node) { + String method = node.getAttributeAsString("Image_projection"); + if (method != null) { + if (method.matches("EQA\\b.*")) { + method = "Sinusoidal"; + final Map 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 { * clearly said "Unknown datum based upon the GRS 1980 ellipsoid" and for consistency with * {@link CommonCRS#SPHERE}, which also use GRS 1980. */ - 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 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. + * + *

The default implementation returns the value of {@link CF#GRID_MAPPING}, or an empty set + * if the given variable does not contain that attribute.

+ * + * @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 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: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Content of the returned map
KeyValue typeDescriptionDefault value
{@value CF#GRID_MAPPING_NAME}{@link String}Operation method nameValue of {@value CF#GRID_MAPPING_NAME} attribute.
{@value #BASE_CRS}{@link GeographicCRS}Base CRS of the map projectionUnknown datum based upon the GRS 1980 ellipsoid.
{@code "*_name"}{@link String}Name of a component (datum, base CRS, …)Attributes found on grid mapping variable.
(projection-dependent){@link Number} or {@code double[]}Map projection parameter valuesAttributes found on grid mapping variable.
{@value #GRID_TO_CRS}{@link MathTransform}Conversion from pixel indices to CRS.None (not from CF-convention).
+ * + * 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 projection(final Node node) { + final String method = node.getAttributeAsString(CF.GRID_MAPPING_NAME); + if (method == null) { + return null; + } + final Map 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 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 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 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 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 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 getGridDimensions(); /** - * Returns the names of all attributes associated to this variable. - * - * @return names of all attributes associated to this variable. - */ - public abstract Collection 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 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()];