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/26 16:08:59 UTC
[sis] branch geoapi-4.0 updated: More complete parsing of
"grid_mapping" parameters specified by CF-conventions.
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 b638306 More complete parsing of "grid_mapping" parameters specified by CF-conventions.
b638306 is described below
commit b638306b84aaa1dea6dbbf942160697b668bcb94
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Apr 26 18:08:30 2019 +0200
More complete parsing of "grid_mapping" parameters specified by CF-conventions.
---
.../apache/sis/internal/earth/netcdf/GCOM_C.java | 3 +-
.../org/apache/sis/internal/netcdf/Convention.java | 28 ++--
.../apache/sis/internal/netcdf/GridMapping.java | 175 ++++++++++++++++++---
.../org/apache/sis/internal/netcdf/Resources.java | 6 +
.../sis/internal/netcdf/Resources.properties | 1 +
.../sis/internal/netcdf/Resources_fr.properties | 1 +
6 files changed, 186 insertions(+), 28 deletions(-)
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 0a88e7e..0993fc0 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
@@ -373,7 +373,7 @@ public final class GCOM_C extends Convention {
* }
*
* @param node the group of variables from which to read attributes.
- * @return the map projection definition, or {@code null} if none.
+ * @return the map projection definition as a modifiable map, or {@code null} if none.
*/
@Override
public Map<String,Object> projection(final Node node) {
@@ -396,6 +396,7 @@ public final class GCOM_C extends Convention {
final Map<String,Object> definition = new HashMap<>(4);
definition.put(BASE_CRS, CommonCRS.SPHERE.geographic());
definition.put("grid_mapping_name", method);
+ definition.put("conversion_name", name);
return definition;
}
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 b1aaeae..3a5939f 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
@@ -496,6 +496,8 @@ public class Convention {
/**
* Key associated to {@link GeographicCRS} value in the map returned by {@link #projection(Node)}.
+ * The geographic CRS associated to this key <strong>must</strong> have (latitude, longitude) axes
+ * in degrees.
*/
protected static final String BASE_CRS = "base_crs";
@@ -526,25 +528,28 @@ public class Convention {
* <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>
+ * <td>Attribute values 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>
+ * <td>Attribute values found on grid mapping variable.</td>
* </tr><tr>
* <td>{@code "towgs84"}</td>
* <td>{@link BursaWolfParameters}</td>
* <td>Datum shift information.</td>
- * <td>Attributes found on grid mapping variable.</td>
+ * <td>Built from {@code "towgs84"} attribute values.</td>
* </tr>
* </table>
*
- * Subclasses can override this method for example in order to override the {@value #BASE_CRS} attribute
+ * The {@value CF#GRID_MAPPING_NAME} entry is mandatory. All other entries are optional.
+ *
+ * <p>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.
+ * The returned map must be modifiable for allowing callers to modify its content.</p>
*
* @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.
+ * @return the map projection definition as a modifiable map, or {@code null} if none.
*
* @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a>
*/
@@ -555,7 +560,6 @@ public class Convention {
}
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;
@@ -587,9 +591,15 @@ public class Convention {
continue;
}
default: {
- final double n = node.getAttributeAsNumber(name);
- if (Double.isNaN(n)) continue;
- value = n;
+ /*
+ * Assume that all map projection parameters in netCDF files are numbers or array of numbers.
+ */
+ final Object[] values = node.getAttributeValues(name, true);
+ switch (values.length) {
+ case 0: continue; // Attribute not found or not numeric.
+ case 1: value = values[0]; break; // This is the usual case.
+ default: value = Vector.create(values, false).doubleValues(); break;
+ }
break;
}
}
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 7778b2a..e7d28bc 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
@@ -18,14 +18,18 @@ package org.apache.sis.internal.netcdf;
import java.util.Map;
import java.util.List;
+import java.util.HashMap;
+import java.util.Collections;
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.parameter.ParameterNotFoundException;
+import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.cs.CartesianCS;
import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -34,12 +38,18 @@ 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.DatumFactory;
+import org.opengis.referencing.datum.GeodeticDatum;
+import org.opengis.referencing.datum.PrimeMeridian;
+import org.opengis.referencing.datum.Ellipsoid;
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.CommonCRS;
import org.apache.sis.referencing.crs.AbstractCRS;
import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.datum.BursaWolfParameters;
+import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.TransformSeparator;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
@@ -49,11 +59,13 @@ import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.internal.system.Modules;
import org.apache.sis.internal.util.Constants;
+import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.io.wkt.WKTFormat;
import org.apache.sis.io.wkt.Warnings;
+import org.apache.sis.measure.Units;
import ucar.nc2.constants.CF;
@@ -63,12 +75,10 @@ import ucar.nc2.constants.CF;
* (e.g. GDAL or ESRI conventions). This class uses a different approach than {@link CRSBuilder},
* which creates Coordinate Reference Systems by inspecting coordinate system axes.
*
- * <p>Current implementation does not yet parse CF-convention attributes.
- * Only some GDAL and ESRI custom attributes are currently supported.</p>
- *
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
*
+ * @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a>
* @see <a href="https://www.unidata.ucar.edu/software/thredds/current/netcdf-java/reference/StandardCoordinateTransforms.html">UCAR projections</a>
*
* @since 1.0
@@ -147,22 +157,121 @@ final class GridMapping {
/**
* If the netCDF variable defines explicitly the map projection method and its parameters, returns those parameters.
- * Otherwise returns {@code null}.
+ * Otherwise returns {@code null}. The given {@code node} argument is typically a dummy variable referenced by value
+ * of the {@value CF#GRID_MAPPING} attribute on the real data variable (as required by CF-conventions), but may also
+ * be something else (the data variable itself, or a group, <i>etc.</i>). That node, together with the attributes to
+ * be parsed, depend on the {@link Convention} instance.
+ *
+ * @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a>
*/
private static GridMapping parseProjectionParameters(final Node node) {
- final Map<String, Object> definition = node.decoder.convention().projection(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));
+ /*
+ * Fetch now numerical values that are not map projection parameters.
+ * This step needs to be done before to try to set parameter values.
+ */
+ final Object greenwichLongitude = definition.remove("longitude_of_prime_meridian");
+ /*
+ * Prepare the block of projection parameters. The set of legal parameter depends on the map projection.
+ * We assume that all numerical values are map projection parameters; character sequences (assumed to be
+ * component names) are handled later. The CF-conventions use parameter names that are slightly different
+ * than OGC names, but Apache SIS implementations of map projections know how to handle them, including
+ * the redundant parameters like "inverse_flattening" and "earth_radius".
+ */
+ final CoordinateOperationFactory opFactory = node.decoder.getCoordinateOperationFactory();
+ final OperationMethod method = opFactory.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);
+ for (final Map.Entry<String,Object> entry : definition.entrySet()) {
+ final String name = entry.getKey();
+ final Object value = entry.getValue();
+ if (value instanceof Number || value instanceof double[]) try {
+ parameters.parameter(name).setValue(value);
+ } catch (IllegalArgumentException ex) {
+ warning(node, ex, Resources.Keys.CanNotSetProjectionParameter_5, node.decoder.getFilename(),
+ node.getName(), name, value, ex.getLocalizedMessage());
+ }
+ }
+ /*
+ * In principle, projection parameters do not include the semi-major and semi-minor axis lengths.
+ * But if those information are provided, then we use them for building the geodetic reference frame.
+ * Otherwise a default reference frame will be used.
+ */
+ final CRSFactory crsFactory = node.decoder.getCRSFactory();
+ GeographicCRS baseCRS = (GeographicCRS) definition.get(Convention.BASE_CRS);
+ if (baseCRS == null) {
+ final DatumFactory datumFactory = node.decoder.getDatumFactory();
+ final CommonCRS defaultDefinitions = CRSBuilder.DEFAULT;
+ boolean isSpecified = false;
+ /*
+ * Prime meridian built from "longitude_of_prime_meridian".
+ */
+ final PrimeMeridian meridian;
+ if (greenwichLongitude instanceof Number) {
+ final double longitude = ((Number) greenwichLongitude).doubleValue();
+ final Map<String,?> properties = properties(definition, "prime_meridian_name", null);
+ meridian = datumFactory.createPrimeMeridian(properties, longitude, Units.DEGREE);
+ isSpecified = true;
+ } else {
+ meridian = defaultDefinitions.primeMeridian();
+ }
+ /*
+ * Ellipsoid built from "semi_major_axis", "semi_minor_axis", etc.
+ */
+ Ellipsoid ellipsoid;
+ try {
+ final double semiMajor = parameters.parameter(Constants.SEMI_MAJOR).doubleValue();
+ final Map<String,?> properties = properties(definition, "reference_ellipsoid_name", null);
+ if (parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue()) {
+ final double ivf = parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue();
+ ellipsoid = datumFactory.createFlattenedSphere(properties, semiMajor, ivf, Units.METRE);
+ } else {
+ final double semiMinor = parameters.parameter(Constants.SEMI_MINOR).doubleValue();
+ ellipsoid = datumFactory.createEllipsoid(properties, semiMajor, semiMinor, Units.METRE);
+ }
+ isSpecified = true;
+ } catch (ParameterNotFoundException | IllegalStateException e) {
+ // Ignore - may be normal if the map projection is not an Apache SIS implementation.
+ ellipsoid = defaultDefinitions.ellipsoid();
+ }
+ /*
+ * Geodetic datum built from "towgs84" and above properties.
+ */
+ final Object bursaWolf = definition.remove("towgs84");
+ final GeodeticDatum datum;
+ if (isSpecified | bursaWolf != null) {
+ Map<String,Object> properties = properties(definition, "horizontal_datum_name", ellipsoid);
+ if (bursaWolf instanceof BursaWolfParameters) {
+ properties = new HashMap<>(properties);
+ properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, bursaWolf);
+ isSpecified = true;
+ }
+ datum = datumFactory.createGeodeticDatum(properties, ellipsoid, meridian);
+ } else {
+ datum = defaultDefinitions.datum();
+ }
+ /*
+ * Geographic CRS form all above properties.
+ */
+ if (isSpecified) {
+ final Map<String,?> properties = properties(definition, "geographic_coordinate_system_name", datum);
+ baseCRS = crsFactory.createGeographicCRS(properties, datum, defaultDefinitions.geographic().getCoordinateSystem());
+ } else {
+ baseCRS = defaultDefinitions.geographic();
+ }
+ }
+ /*
+ * Create defining conversion from the parameters and the projected CRS.
+ */
+ Map<String,?> properties = properties(definition, "conversion_name", node.getName());
+ final Conversion conversion = opFactory.createDefiningConversion(properties, method, parameters);
final CartesianCS cs = CommonCRS.WGS84.universal(0,0).getCoordinateSystem(); // TODO
- final ProjectedCRS crs = node.decoder.getCRSFactory().createProjectedCRS(name, baseCRS, conversion, cs);
-
+ properties = properties(definition, "projected_coordinate_system_name", conversion);
+ final ProjectedCRS crs = crsFactory.createProjectedCRS(properties, baseCRS, conversion, cs);
+ /*
+ * Build the "grid to CRS" if present. This is not present in CF-convention,
+ * but may be present in some non-CF conventions.
+ */
final MathTransform gridToCRS = node.decoder.convention().gridToCRS(node, crs);
return new GridMapping(crs, gridToCRS, false);
} catch (ClassCastException | IllegalArgumentException | FactoryException | TransformException e) {
@@ -172,6 +281,30 @@ final class GridMapping {
}
/**
+ * Returns the {@code properties} argument value to give to the factory methods of geodetic objects.
+ * The returned map contains at least an entry for {@value IdentifiedObject#NAME_KEY} with the name
+ * fetched from the value of the attribute named {@code nameAttribute}.
+ *
+ * @param definition map containing the attribute values.
+ * @param nameAttribute name of the attribute from which to get the name.
+ * @param fallback fallback as an {@link IdentifiedObject} (from which the name will be copied),
+ * or a character sequence, or {@code null} for "Unnamed" localized string.
+ */
+ private static Map<String,Object> properties(final Map<String,Object> definition, final String nameAttribute, final Object fallback) {
+ Object name = definition.remove(nameAttribute);
+ if (name == null) {
+ if (fallback instanceof IdentifiedObject) {
+ name = ((IdentifiedObject) fallback).getName();
+ } else if (fallback != null) {
+ name = fallback;
+ } else {
+ name = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed);
+ }
+ }
+ return Collections.singletonMap(IdentifiedObject.NAME_KEY, name);
+ }
+
+ /**
* 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
@@ -280,8 +413,14 @@ final class GridMapping {
* @param ex the exception that occurred while creating the CRS or grid geometry.
*/
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());
+ warning(node, ex, key, node.decoder.getFilename(), node.getName(), ex.getLocalizedMessage());
+ }
+
+ /**
+ * Logs a warning, presuming that {@link GridMapping} are invoked (indirectly) from {@link Variable#getGridGeometry()}.
+ */
+ private static void warning(final Node node, final Exception ex, final short key, final Object... arguments) {
+ NamedElement.warning(node.decoder.listeners, Variable.class, "getGridGeometry", ex, null, key, arguments);
}
/**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
index f24dab9..4baff51 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
@@ -92,6 +92,12 @@ public final class Resources extends IndexedResourceBundle {
public static final short CanNotRender_2 = 14;
/**
+ * Can not set map projection parameter “{1}:{2}” = {3} in the “{0}” netCDF file. The reason
+ * is: {4}
+ */
+ public static final short CanNotSetProjectionParameter_5 = 20;
+
+ /**
* Can not use axis “{0}” in a grid geometry.
*/
public static final short CanNotUseAxis_1 = 18;
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
index 7a05771..82f6080 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
@@ -25,6 +25,7 @@ CanNotCreateCRS_3 = Can not create the Coordinate Reference Syst
CanNotCreateGridGeometry_3 = Can not create the grid geometry \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2}
CanNotRelateVariableDimension_3 = Can not relate dimension \u201c{2}\u201d of variable \u201c{1}\u201d to a coordinate system dimension in netCDF file \u201c{0}\u201d.
CanNotRender_2 = Can not render an image for \u201c{0}\u201d. The reason is: {1}
+CanNotSetProjectionParameter_5 = Can not set map projection parameter \u201c{1}\u200b:{2}\u201d = {3} in the \u201c{0}\u201d netCDF file. The reason is: {4}
CanNotUseAxis_1 = Can not use axis \u201c{0}\u201d in a grid geometry.
CanNotUseUCAR = Can not use UCAR library for netCDF format. Fallback on Apache SIS implementation.
DimensionNotFound_3 = Dimension \u201c{2}\u201d declared by attribute \u201c{1}\u201d is not found in the \u201c{0}\u201d file.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
index b32eff7..79e59f4 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
@@ -30,6 +30,7 @@ CanNotCreateCRS_3 = Ne peut pas cr\u00e9er le syst\u00e8me de r\
CanNotCreateGridGeometry_3 = Ne peut pas cr\u00e9er la g\u00e9om\u00e9trie de grille \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u2008: {2}
CanNotRelateVariableDimension_3 = Ne peut pas relier la dimension \u00ab\u202f{2}\u202f\u00bb de la variable \u00ab\u202f{1}\u202f\u00bb \u00e0 une dimension d\u2019un syst\u00e8me de coordonn\u00e9es du fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
CanNotRender_2 = Ne peut pas produire une image pour \u00ab\u202f{0}\u202f\u00bb. La raison est\u2008: {1}
+CanNotSetProjectionParameter_5 = Ne peut pas d\u00e9finir le param\u00e8tre de projection \u00ab\u202f{1}\u200b:{2}\u202f\u00bb = {3} dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u2008: {4}
CanNotUseAxis_1 = Ne peut pas utiliser l\u2019axe \u00ab\u202f{0}\u202f\u00bb pour une g\u00e9om\u00e9trie de grille.
CanNotUseUCAR = Ne peut pas utiliser la biblioth\u00e8que de l\u2019UCAR pour le format netCDF. L\u2019impl\u00e9mentation de Apache SIS sera utilis\u00e9e \u00e0 la place.
DimensionNotFound_3 = La dimension \u00ab\u202f{2}\u202f\u00bb d\u00e9clar\u00e9e par l\u2019attribut \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans le fichier \u00ab\u202f{0}\u202f\u00bb.