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 ceda4f9ca2 Try to make the warning a little bit more accurate in netCDF data store. When the CRS is declared in different ways, verify that they are consistent. Store enumeration values on 64 bits integers instead of 32 bits, because they are sometime bitmask with values that require 32 bits unsigned integers. ceda4f9ca2 is described below commit ceda4f9ca2d85460fe67474edc98b3278f4a6c24 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Feb 1 13:19:15 2025 +0100 Try to make the warning a little bit more accurate in netCDF data store. When the CRS is declared in different ways, verify that they are consistent. Store enumeration values on 64 bits integers instead of 32 bits, because they are sometime bitmask with values that require 32 bits unsigned integers. --- .../org/apache/sis/storage/netcdf/NetcdfStore.java | 5 +- .../org/apache/sis/storage/netcdf/base/Axis.java | 2 +- .../apache/sis/storage/netcdf/base/Convention.java | 8 - .../apache/sis/storage/netcdf/base/Decoder.java | 28 +- .../apache/sis/storage/netcdf/base/FeatureSet.java | 4 +- .../sis/storage/netcdf/base/GridMapping.java | 314 +++++++++++++-------- .../sis/storage/netcdf/base/NamedElement.java | 2 +- .../org/apache/sis/storage/netcdf/base/Node.java | 3 +- .../sis/storage/netcdf/base/RasterResource.java | 8 +- .../apache/sis/storage/netcdf/base/Variable.java | 36 +-- .../sis/storage/netcdf/classic/VariableInfo.java | 2 +- .../sis/storage/netcdf/internal/Resources.java | 20 +- .../storage/netcdf/internal/Resources.properties | 6 +- .../netcdf/internal/Resources_fr.properties | 6 +- .../sis/storage/netcdf/ucar/VariableWrapper.java | 11 +- .../apache/sis/storage/netcdf/base/GridTest.java | 15 +- 16 files changed, 293 insertions(+), 177 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java index 39f7c2229b..084777a151 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStore.java @@ -195,7 +195,7 @@ public class NetcdfStore extends DataStore implements Aggregate { @Override public synchronized Metadata getMetadata() throws DataStoreException { if (metadata == null) try { - final MetadataReader reader = new MetadataReader(decoder()); + final var reader = new MetadataReader(decoder()); metadata = reader.read(); } catch (IOException | ArithmeticException e) { throw new DataStoreException(e); @@ -216,7 +216,7 @@ public class NetcdfStore extends DataStore implements Aggregate { */ @Override public Optional<TreeTable> getNativeMetadata() throws DataStoreException { - final DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME, TableColumn.VALUE); + final var table = new DefaultTreeTable(TableColumn.NAME, TableColumn.VALUE); final TreeTable.Node root = table.getRoot(); root.setValue(TableColumn.NAME, Constants.NETCDF); decoder().addAttributesTo(root); @@ -235,6 +235,7 @@ public class NetcdfStore extends DataStore implements Aggregate { @SuppressWarnings("ReturnOfCollectionOrArrayField") public synchronized Collection<Resource> components() throws DataStoreException { if (components == null) try { + @SuppressWarnings("LocalVariableHidesMemberVariable") final Decoder decoder = decoder(); Resource[] resources = decoder.getDiscreteSampling(this); final List<Resource> grids = RasterResource.create(decoder, this); diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Axis.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Axis.java index dca2c1dd06..5057c537b6 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Axis.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Axis.java @@ -227,7 +227,7 @@ public final class Axis extends NamedElement { } } if (!isConsistent) { - coordinates.warning(Grid.class, "getAxes", // Caller of this constructor. + coordinates.warning(Grid.class, "getAxes", null, // Caller of this constructor. Resources.Keys.AmbiguousAxisDirection_4, coordinates.getFilename(), coordinates.getName(), dir, check); if (isSigned) { if (AxisDirections.isOpposite(dir)) { diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java index e6c8d59684..1ea0d3a379 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java @@ -507,14 +507,6 @@ public class Convention { value = bp; break; } - case "crs_wkt": { - /* - * CF-Convention said that even if a WKT definition is provided, other attributes shall be present - * and have precedence over the WKT definition. Consequently, purpose of WKT in netCDF files is not - * obvious (except for CompoundCRS). We ignore them for now. - */ - continue; - } default: { if (ln.endsWith(NAME_SUFFIX)) { value = node.getAttributeAsString(name); diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java index 14bf4b1c2a..e882d120e3 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java @@ -129,16 +129,24 @@ public abstract class Decoder extends ReferencingFactoryContainer { final Datum[] datumCache; /** - * The CRS and <i>grid to CRS</i> transform defined by attributes in a variable. For example, GDAL uses - * {@code "spatial_ref_sys"} and {@code "GeoTransform"} attributes associated to a variable having the name - * specified by the {@code "grid_mapping"} attribute. - * - * <p>Keys are either {@link Variable} instance for which we found a grid mapping, or {@link String} instances - * if we found some variables with {@code "grid_mapping"} attribute values.</p> + * Information for building <abbr>CRS</abbr>s and <i>grid to CRS</i> transforms for variables. + * The {@link GridMapping} class supports different conventions: the <abbr>CF</abbr> conventions + * are tried first, followed by <abbr>GDAL</abbr> conventions (pair of {@code "spatial_ref_sys"} + * and {@code "GeoTransform"} attributes), then <abbr>ESRI</abbr> conventions. + * The keys are variable names from two sources: + * + * <ol> + * <li>Name of an usually empty variable referenced by a {@code "grid_mapping"} attribute on the actual data. + * This is the standard approach, as it allows many variables to reference the same <abbr>CRS</abbr> + * definition by declaring the same value in their {@code "grid_mapping"} attribute.</li> + * <li>Name of the actual data variable when the attributes are found directly on that variable. + * This approach is non-standard, as it does not allow the sharing of the same <abbr>CRS</abbr> + * definition by many variables. But it is nevertheless observed in practice.</li> + * </ol> * * @see GridMapping#forVariable(Variable) */ - final Map<Object,GridMapping> gridMapping; + final Map<String,GridMapping> gridMapping; /** * Cache of localization grids created for a given pair of (<var>x</var>,<var>y</var>) axes. @@ -441,9 +449,9 @@ public abstract class Decoder extends ReferencingFactoryContainer { public final List<CoordinateReferenceSystem> getReferenceSystemInfo() throws IOException, DataStoreException { final var list = new ArrayList<CoordinateReferenceSystem>(); for (final Variable variable : getVariables()) { - final GridMapping m = GridMapping.forVariable(variable); - if (m != null) { - addIfNotPresent(list, m.crs); + final var gm = GridMapping.forVariable(variable); + if (gm != null) { + addIfNotPresent(list, gm.crs()); } } /* diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java index d9ecd205b4..dd5789ef2a 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java @@ -900,12 +900,12 @@ makeGeom: if (!isEmpty) { } else { value = p.read(extent, null); // Force the type to `Vector`. } - final Map<Integer,String> enumeration = p.getEnumeration(); + final Map<Long,String> enumeration = p.getEnumeration(); if (enumeration != null && value instanceof Vector) { final var data = (Vector) value; final var meanings = new String[data.size()]; for (int j=0; j<meanings.length; j++) { - String m = enumeration.get(data.intValue(j)); + String m = enumeration.get(data.longValue(j)); meanings[j] = (m != null) ? m : ""; } value = Arrays.asList(meanings); diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java index 4bdf40c8ea..70412a7534 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java @@ -18,6 +18,7 @@ package org.apache.sis.storage.netcdf.base; import java.util.Map; import java.util.List; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; @@ -29,6 +30,7 @@ import java.text.NumberFormat; import java.text.FieldPosition; import java.text.ParseException; import ucar.nc2.constants.CF; // String constants are copied by the compiler with no UCAR reference left. +import ucar.nc2.constants.ACDD; // idem import javax.measure.Unit; import javax.measure.quantity.Length; import org.opengis.util.FactoryException; @@ -52,6 +54,7 @@ import org.opengis.referencing.datum.PrimeMeridian; import org.opengis.referencing.datum.Ellipsoid; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.referencing.IdentifiedObjects; import org.apache.sis.referencing.crs.AbstractCRS; import org.apache.sis.referencing.cs.AxesConvention; import org.apache.sis.referencing.datum.BursaWolfParameters; @@ -71,9 +74,12 @@ import org.apache.sis.system.Modules; import org.apache.sis.util.CharSequences; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Numbers; +import org.apache.sis.util.Utilities; +import org.apache.sis.util.privy.Strings; import org.apache.sis.util.privy.Constants; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.util.resources.Errors; +import org.apache.sis.util.resources.IndexedResourceBundle; import org.apache.sis.io.wkt.WKTFormat; import org.apache.sis.io.wkt.Warnings; import org.apache.sis.measure.Units; @@ -83,7 +89,7 @@ import org.opengis.referencing.datum.DatumEnsemble; /** - * Temporary objects for creating a {@link GridGeometry} instance defined by attributes on a variable. + * Helper object for creating a {@link GridGeometry} instance defined by attributes on a variable. * Those attributes are defined by CF-conventions, but some other non-CF attributes are also in usage * (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. @@ -93,50 +99,58 @@ import org.opengis.referencing.datum.DatumEnsemble; * @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a> */ final class GridMapping { + /** + * The variable on which projection parameters are defined as attributes. + * This is typically an empty variable referenced by the value of the + * {@value CF#GRID_MAPPING} attribute on the actual data variable (CF-conventions), + * but may also be something else such as the data variable itself, or a group, <i>etc.</i>. + * That node, together with the attributes to be parsed, depends on the {@link Convention} instance. + */ + private final Node mapping; + /** * The Coordinate Reference System inferred from grid mapping attribute values, or {@code null} if none. * This CRS may have been constructed from Well Known Text or EPSG codes declared in {@code "spatial_ref"}, * {@code "ESRI_pe_string"} or {@code "EPSG_code"} attributes. * * <h4>Usage note</h4> - * This come from different information than the one used by {@link CRSBuilder}, - * which creates CRS by inspection of coordinate system axes. + * This is built from different information than the one used by {@link CRSBuilder}, + * which creates <abbr>CRS</abbr> by inspection of coordinate system axes. + * + * @see #crs() */ - final CoordinateReferenceSystem crs; + private CoordinateReferenceSystem crs; /** * The <i>grid to CRS</i> transform, or {@code null} if none. This information is usually not specified * except when using GDAL conventions. If {@code null}, then the transform should be inferred by {@link Grid}. */ - private final MathTransform gridToCRS; + private MathTransform gridToCRS; /** * Whether the {@link #crs} was defined by a WKT string. */ - private final boolean isWKT; + private boolean isWKT; /** - * Creates an instance for the given {@link #crs} and {@link #gridToCRS} values. + * Creates an initially empty instance. * - * @param crs CRS inferred from grid mapping attribute values, or {@code null} if none. - * @param gridToCRS transform from GDAL conventions, or {@code null} if none. - * @param isWKT wether the {@code crs} was defined by a WKT string. + * @param mapping the variable on which attributes are defined for projection parameters. */ - private GridMapping(final CoordinateReferenceSystem crs, final MathTransform gridToCRS, final boolean isWKT) { - this.crs = crs; - this.gridToCRS = gridToCRS; - this.isWKT = isWKT; + private GridMapping(final Node mapping) { + this.mapping = mapping; } /** * Fetches grid geometry information from attributes associated to the given variable. - * This method should be invoked only once per variable, but may return a shared {@code GridMapping} instance - * for all variables because there is typically only one set of grid mapping attributes for the whole file. + * This method should be invoked only one or two times per variable, but may return a + * shared {@code GridMapping} instance for all variables because there is typically + * only one set of grid mapping attributes for the whole file. * * @param variable the variable for which to create a grid geometry. */ static GridMapping forVariable(final Variable variable) { - final Map<Object,GridMapping> gridMapping = variable.decoder.gridMapping; + final Map<String,GridMapping> gridMapping = variable.decoder.gridMapping; for (final String name : variable.decoder.convention().nameOfMappingNode(variable)) { GridMapping gm = gridMapping.get(name); if (gm != null) { @@ -151,30 +165,21 @@ final class GridMapping { if (mapping != null) { gm = parse(mapping); } - gridMapping.put(name, gm); // Store even if null. + gridMapping.put(name, gm); // Store even if null. if (gm != null) { return gm; } } } /* - * Found no "grid_mapping" attribute. The block below is not CF-compliant, - * but we find some use of this non-standard approach in practice. + * Found no "grid_mapping" attribute. Search for the CRS attributes directly on the variable. + * This is not CF-compliant, but we find some uses of this non-standard approach in practice. */ - GridMapping gm = gridMapping.get(variable); - if (gm == null) { - final String name = variable.getName(); - gm = gridMapping.get(name); - if (gm == null && !gridMapping.containsKey(name)) { - gm = parse(variable); - gridMapping.put(name, gm); // Store even if null. - } - if (gm == null) { - gm = parseNonStandard(variable); - } - if (gm != null) { - gridMapping.put(variable, gm); - } + final String name = variable.getName(); + GridMapping gm = gridMapping.get(name); + if (gm == null && !gridMapping.containsKey(name)) { + gm = parse(variable); + gridMapping.put(name, gm); // Store even if null. } return gm; } @@ -185,26 +190,21 @@ final class GridMapping { * extensions (for example GDAL usage) are tried next. */ private static GridMapping parse(final Node mapping) { - GridMapping gm = parseProjectionParameters(mapping); - if (gm == null) { - gm = parseGeoTransform(mapping); - } - return gm; + final var gm = new GridMapping(mapping); + // Tries CF-convention first, and if it doesn't work, try GDAL convention. + return (gm.parseProjectionParameters() || gm.parseGeoTransform() || gm.parseESRI()) ? gm : null; } /** - * If the netCDF variable defines explicitly the map projection method and its parameters, returns those parameters. - * 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, depends on the {@link Convention} instance. + * Sets the <abbr>CRS</abbr> and "grid to <abbr>CRS</abbr>" from the <abbr>CF</abbr> conventions. + * If this method does not find the expected attributes, then it does nothing. * - * @param node the dummy variable on which attributes are defined for projection parameters. + * @return whether this method found grid geometry attributes. * * @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); + private boolean parseProjectionParameters() { + final Map<String,Object> definition = mapping.decoder.convention().projection(mapping); if (definition != null) try { /* * Fetch now numerical values that are not map projection parameters. @@ -219,7 +219,7 @@ final class GridMapping { * the redundant parameters like "inverse_flattening" and "earth_radius". */ final String mappingName = (String) definition.remove(CF.GRID_MAPPING_NAME); - final OperationMethod method = node.decoder.findOperationMethod(mappingName); + final OperationMethod method = mapping.decoder.findOperationMethod(mappingName); final ParameterValueGroup parameters = method.getParameters().createValue(); for (final Iterator<Map.Entry<String,Object>> it = definition.entrySet().iterator(); it.hasNext();) { final Map.Entry<String,Object> entry = it.next(); @@ -251,8 +251,8 @@ final class GridMapping { } } } catch (IllegalArgumentException ex) { // Includes NumberFormatException. - warning(node, ex, Resources.Keys.CanNotSetProjectionParameter_5, node.decoder.getFilename(), - node.getName(), name, value, ex.getLocalizedMessage()); + warning(mapping, ex, null, Resources.Keys.CanNotSetProjectionParameter_5, + mapping.decoder.getFilename(), mapping.getName(), name, value, ex.getLocalizedMessage()); } } /* @@ -260,41 +260,50 @@ final class GridMapping { * 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 GeographicCRS baseCRS = createBaseCRS(node.decoder, parameters, definition, greenwichLongitude); + final boolean geographic = (method instanceof PseudoPlateCarree); + final GeographicCRS baseCRS = createBaseCRS(mapping.decoder, parameters, definition, greenwichLongitude, geographic); final MathTransform baseToCRS; - final CoordinateReferenceSystem crs; - if (method instanceof PseudoPlateCarree) { + if (geographic) { // Only swap axis order from (latitude, longitude) to (longitude, latitude). baseToCRS = MathTransforms.linear(new Matrix3(0, 1, 0, 1, 0, 0, 0, 0, 1)); crs = baseCRS; } else { - final CoordinateOperationFactory opFactory = node.decoder.getCoordinateOperationFactory(); - Map<String,?> properties = properties(definition, Convention.CONVERSION_NAME, node.getName()); + final CoordinateOperationFactory opFactory = mapping.decoder.getCoordinateOperationFactory(); + Map<String,?> properties = properties(definition, Convention.CONVERSION_NAME, false, mapping.getName()); final Conversion conversion = opFactory.createDefiningConversion(properties, method, parameters); - final CartesianCS cs = node.decoder.getStandardProjectedCS(); - properties = properties(definition, Convention.PROJECTED_CRS_NAME, conversion); - final ProjectedCRS p = node.decoder.getCRSFactory().createProjectedCRS(properties, baseCRS, conversion, cs); + final CartesianCS cs = mapping.decoder.getStandardProjectedCS(); + properties = properties(definition, Convention.PROJECTED_CRS_NAME, true, conversion); + final ProjectedCRS p = mapping.decoder.getCRSFactory().createProjectedCRS(properties, baseCRS, conversion, cs); baseToCRS = p.getConversionFromBase().getMathTransform(); crs = p; } + /* + * The CF-Convention said that even if a WKT definition is provided, other attributes shall be present + * and have precedence over the WKT definition. Consequently, the purpose of WKT in netCDF files is not + * obvious (except for CompoundCRS). + */ + final var done = new ArrayList<String>(2); + setOrVerifyWKT(definition, "crs_wkt", done); + setOrVerifyWKT(definition, "spatial_ref", done); /* * Report all projection parameters that have not been used. If the map is not rendered * at expected location, it may be because we have ignored some important parameters. */ + definition.remove(CF.LONG_NAME); if (!definition.isEmpty()) { - warning(node, null, Resources.Keys.UnknownProjectionParameters_2, - node.decoder.getFilename(), String.join(", ", definition.keySet())); + warningInMapping(mapping, null, Resources.Keys.UnknownProjectionParameters_3, + String.join(", ", definition.keySet())); } /* * Build the "grid to CRS" if present. This is not defined by CF-convention, * but may be present in some non-CF conventions. */ - final MathTransform gridToCRS = node.decoder.convention().gridToCRS(node, baseToCRS); - return new GridMapping(crs, gridToCRS, false); + gridToCRS = mapping.decoder.convention().gridToCRS(mapping, baseToCRS); + return true; } catch (ClassCastException | IllegalArgumentException | FactoryException | TransformException e) { - canNotCreate(node, Resources.Keys.CanNotCreateCRS_3, e); + warningInMapping(mapping, e, Resources.Keys.CanNotCreateCRS_3, null); } - return null; + return false; } /** @@ -303,9 +312,11 @@ final class GridMapping { * * @param parameters parameters from which to get ellipsoid axis lengths. Will not be modified. * @param definition map from which to get element names. Elements used will be removed. + * @param main whether the returned <abbr>CRS</abbr> will be the main one. */ private static GeographicCRS createBaseCRS(final Decoder decoder, final ParameterValueGroup parameters, - final Map<String,Object> definition, final Object greenwichLongitude) throws FactoryException + final Map<String,Object> definition, final Object greenwichLongitude, final boolean main) + throws FactoryException { final DatumFactory datumFactory = decoder.getDatumFactory(); final CommonCRS defaultDefinitions = decoder.convention().defaultHorizontalCRS(false); @@ -316,8 +327,8 @@ final class GridMapping { final PrimeMeridian meridian; if (greenwichLongitude instanceof Number) { final double longitude = ((Number) greenwichLongitude).doubleValue(); - final Map<String,?> properties = properties(definition, - Convention.PRIME_MERIDIAN_NAME, (longitude == 0) ? "Greenwich" : null); + final String name = (longitude == 0) ? "Greenwich" : null; + Map<String,?> properties = properties(definition, Convention.PRIME_MERIDIAN_NAME, false, name); meridian = datumFactory.createPrimeMeridian(properties, longitude, Units.DEGREE); isSpecified = true; } else { @@ -354,7 +365,7 @@ final class GridMapping { .append(isSphere ? " R=" : " a="); return f.format(km, b, new FieldPosition(0)).append(" km").toString(); }; - final Map<String,?> properties = properties(definition, Convention.ELLIPSOID_NAME, fallback); + final Map<String,?> properties = properties(definition, Convention.ELLIPSOID_NAME, false, fallback); if (isIvfDefinitive) { ellipsoid = datumFactory.createFlattenedSphere(properties, semiMajor, secondDefiningParameter, axisUnit); } else { @@ -372,7 +383,7 @@ final class GridMapping { final GeodeticDatum datum; DatumEnsemble<GeodeticDatum> ensemble = null; if (isSpecified | bursaWolf != null) { - Map<String,Object> properties = properties(definition, Convention.GEODETIC_DATUM_NAME, ellipsoid); + Map<String,Object> properties = properties(definition, Convention.GEODETIC_DATUM_NAME, false, ellipsoid); if (bursaWolf instanceof BursaWolfParameters) { properties = new HashMap<>(properties); properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, bursaWolf); @@ -389,7 +400,7 @@ final class GridMapping { * Geographic CRS from all above properties. */ if (isSpecified) { - final Map<String,?> properties = properties(definition, Convention.GEOGRAPHIC_CRS_NAME, datum); + final Map<String,?> properties = properties(definition, Convention.GEOGRAPHIC_CRS_NAME, main, datum); return decoder.getCRSFactory().createGeographicCRS(properties, datum, ensemble, defaultDefinitions.geographic().getCoordinateSystem()); } else { @@ -404,10 +415,13 @@ final class GridMapping { * * @param definition map containing the attribute values. * @param nameAttribute name of the attribute from which to get the name. + * @param takeComment whether to consume the {@code comment} attribute. * @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) { + private static Map<String,Object> properties(final Map<String,Object> definition, final String nameAttribute, + final boolean takeComment, final Object fallback) + { Object name = definition.remove(nameAttribute); if (name == null) { if (fallback == null) { @@ -421,9 +435,50 @@ final class GridMapping { name = fallback.toString(); } } + if (takeComment) { + Object comment = definition.remove(ACDD.comment); + if (comment != null) { + return Map.of(IdentifiedObject.NAME_KEY, name, IdentifiedObject.REMARKS_KEY, comment.toString()); + } + } return Map.of(IdentifiedObject.NAME_KEY, name); } + /** + * Parses a <abbr>CRS</abbr> defined by an <abbr>WKT</abbr> string, if present. + * If {@link #crs} is null, it is set to the parsing result. Otherwise, the current {@link #crs} has precedence + * but the parsed <abbr>CRS</abbr> is compared and a warning is logged if an inconsistency is found. + * + * @param definition map containing the attribute values. + * @param attributeName name of the attribute to consume in the definition map. + * @param done <abbr>WKT</abbr> already parsed, for avoiding repetition. + */ + private void setOrVerifyWKT(final Map<String,Object> definition, final String attributeName, final List<String> done) { + Object value = definition.remove(attributeName); + if (value instanceof String) { + String wkt = ((String) value).strip(); + for (String previous : done) { + if (wkt.equalsIgnoreCase(previous)) { + return; + } + } + done.add(wkt); + CoordinateReferenceSystem check; + try { + check = createFromWKT((String) value); + } catch (Exception e) { + warning(mapping, e, mapping.errors(), Errors.Keys.CanNotParseCRS_1, attributeName); + return; + } + if (crs == null) { + crs = check; + } else if (!Utilities.equalsIgnoreMetadata(crs, check)) { + warning(mapping, null, null, Resources.Keys.InconsistentCRS_2, + mapping.decoder.getFilename(), mapping.getName()); + } + } + } + /** * Tries to parse a CRS and affine transform from GDAL GeoTransform coefficients. * Those coefficients are not in the usual order expected by matrix, affine @@ -435,36 +490,32 @@ final class GridMapping { * Y = c[3] + P*c[4] + L*c[5]; * } * - * @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. + * @return whether this method found grid geometry attributes. */ - private static GridMapping parseGeoTransform(final Node mapping) { + private boolean parseGeoTransform() { final String wkt = mapping.getAttributeAsString("spatial_ref"); final String gtr = mapping.getAttributeAsString("GeoTransform"); - if (wkt == null && gtr == null) { - return null; - } short message = Resources.Keys.CanNotCreateCRS_3; - CoordinateReferenceSystem crs = null; - MathTransform gridToCRS = null; + boolean done = false; try { if (wkt != null) { - crs = createFromWKT(mapping, wkt); + crs = createFromWKT(wkt); + isWKT = true; + done = true; } if (gtr != null) { message = Resources.Keys.CanNotCreateGridGeometry_3; final double[] c = parseDoubles(gtr); - if (c.length == 6) { - gridToCRS = new AffineTransform2D(c[1], c[4], c[2], c[5], c[0], c[3]); // X_DIMENSION, Y_DIMENSION - } else { - canNotCreate(mapping, message, new DataStoreContentException(mapping.errors() - .getString(Errors.Keys.UnexpectedArrayLength_2, 6, c.length))); + if (c.length != 6) { + throw new DataStoreContentException(mapping.errors().getString(Errors.Keys.UnexpectedArrayLength_2, 6, c.length)); } + gridToCRS = new AffineTransform2D(c[1], c[4], c[2], c[5], c[0], c[3]); // X_DIMENSION, Y_DIMENSION + done = true; } - } catch (ParseException | NumberFormatException e) { - canNotCreate(mapping, message, e); + } catch (Exception e) { + warningInMapping(mapping, e, message, null); } - return new GridMapping(crs, gridToCRS, wkt != null); + return done; } /** @@ -478,18 +529,17 @@ 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(Node)} found no grid geometry. + * This method is invoked as a fallback if {@link #parseGeoTransform()} 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 Node variable) { - String code = variable.getAttributeAsString("ESRI_pe_string"); - final boolean isWKT = (code != null); - if (!isWKT) { - code = variable.getAttributeAsString("EPSG_code"); + private boolean parseESRI() { + String code = mapping.getAttributeAsString("ESRI_pe_string"); + isWKT = (code != null); + if (code == null) { + code = mapping.getAttributeAsString("EPSG_code"); if (code == null) { - return null; + return false; } } /* @@ -499,55 +549,72 @@ final class GridMapping { * The CRS parsings below need to take those differences in account, except axis order which is tested in * the `adaptGridCRS(…)` method. */ - CoordinateReferenceSystem crs; try { if (isWKT) { - crs = createFromWKT(variable, code); + crs = createFromWKT(code); } else { crs = CRS.forCode(Constants.EPSG + ':' + code); } - } catch (FactoryException | ParseException | ClassCastException e) { - canNotCreate(variable, Resources.Keys.CanNotCreateCRS_3, e); - crs = null; + } catch (Exception e) { + warningInMapping(mapping, e, Resources.Keys.CanNotCreateCRS_3, null); + return false; } - return new GridMapping(crs, null, isWKT); + return true; } /** * 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 Node node, final String wkt) throws ParseException { - final var f = new WKTFormat(Decoder.DATA_LOCALE, TimeZone.getTimeZone(node.decoder.getTimeZone())); + private CoordinateReferenceSystem createFromWKT(final String wkt) throws ParseException { + final var f = new WKTFormat(Decoder.DATA_LOCALE, TimeZone.getTimeZone(mapping.decoder.getTimeZone())); f.setConvention(org.apache.sis.io.wkt.Convention.WKT1_COMMON_UNITS); - final var crs = (CoordinateReferenceSystem) f.parseObject(wkt); + final var parsed = (CoordinateReferenceSystem) f.parseObject(wkt); final Warnings warnings = f.getWarnings(); if (warnings != null) { - final LogRecord record = new LogRecord(Level.WARNING, warnings.toString()); + final var record = new LogRecord(Level.WARNING, warnings.toString()); record.setLoggerName(Modules.NETCDF); record.setSourceClassName(Variable.class.getCanonicalName()); record.setSourceMethodName("getGridGeometry"); - node.decoder.listeners.warning(record); + mapping.decoder.listeners.warning(record); } - return crs; + return parsed; } /** - * Logs a warning about a CRS or grid geometry that cannot be created. + * Logs a warning with a message that contains the netCDF file name and the mapping variable, in that order. * This method presumes that {@link GridMapping} are invoked (indirectly) from {@link Variable#getGridGeometry()}. * - * @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. + * @param mapping the variable on which the warning applies. + * @param ex the exception that occurred while creating the CRS or grid geometry, or {@code null} if none. + * @param key {@link Resources.Keys#CanNotCreateCRS_3} or {@link Resources.Keys#CanNotCreateGridGeometry_3}. + * @param more an additional argument for localization, or {@code null} for the exception message. */ - private static void canNotCreate(final Node node, final short key, final Exception ex) { - warning(node, ex, key, node.decoder.getFilename(), node.getName(), ex.getLocalizedMessage()); + private static void warningInMapping(final Node mapping, final Exception ex, final short key, String more) { + if (more == null) { + more = ex.getLocalizedMessage(); + } + warning(mapping, ex, null, key, mapping.decoder.getFilename(), mapping.getName(), more); } /** * Logs a warning, presuming that {@link GridMapping} are invoked (indirectly) from {@link Variable#getGridGeometry()}. + * + * @param mapping the variable on which the warning applies. + * @param exception the exception that occurred, or {@code null} if none. + * @param resources the resources bundle for {@code key} and {@code arguments}, or {@code null} for {@link Resources}. + * @param key one of the {@code resources} constants (by default, a {@link Resources.Keys} constant). + * @param arguments values to be formatted in the {@link java.text.MessageFormat} pattern. + */ + private static void warning(final Node mapping, Exception ex, IndexedResourceBundle resources, short key, Object... arguments) { + NamedElement.warning(mapping.decoder.listeners, Variable.class, "getGridGeometry", ex, resources, key, arguments); + } + + /** + * Returns the Coordinate Reference System inferred from grid mapping attribute values, or {@code null} if none. */ - 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); + final CoordinateReferenceSystem crs() { + return crs; } /** @@ -620,7 +687,7 @@ final class GridMapping { explicitCRS = new CRSMerger(variable.decoder) .replaceComponent(implicitCRS, firstAffectedCoordinate, explicitCRS); } catch (FactoryException e) { - canNotCreate(variable, Resources.Keys.CanNotCreateCRS_3, e); + warningInMapping(variable, e, Resources.Keys.CanNotCreateCRS_3, null); return null; } isSameGrid = implicitCRS.equals(explicitCRS); @@ -664,7 +731,7 @@ final class GridMapping { isSameGrid = false; } } catch (FactoryException e) { - canNotCreate(variable, Resources.Keys.CanNotCreateGridGeometry_3, e); + warningInMapping(variable, e, Resources.Keys.CanNotCreateGridGeometry_3, null); return null; } } @@ -678,4 +745,17 @@ final class GridMapping { return new GridGeometry(implicit.getExtent(), anchor, explicitG2C, explicitCRS); } } + + /** + * Returns a string representation for debugging purposes. + * + * @return a string representation for debugging purpose. + */ + @Override + public String toString() { + return Strings.toString(getClass(), + null, mapping.getName(), + "crs", IdentifiedObjects.getName(crs, null), + "isWKT", isWKT); + } } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/NamedElement.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/NamedElement.java index 07221908bc..f4a237050c 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/NamedElement.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/NamedElement.java @@ -98,7 +98,7 @@ public abstract class NamedElement { * @param method the caller method to report, preferable a public method. * @param exception the exception that occurred, or {@code null} if none. * @param resources the resources bundle for {@code key} and {@code arguments}, or {@code null} for {@link Resources}. - * @param key one or {@link Resources.Keys} constants. + * @param key one of the {@code resources} constants (by default, a {@link Resources.Keys} constant). * @param arguments values to be formatted in the {@link java.text.MessageFormat} pattern. */ static void warning(final StoreListeners listeners, final Class<?> caller, final String method, diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java index 4c56220944..d2f09c9126 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java @@ -266,10 +266,11 @@ public abstract class Node extends NamedElement { * * @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 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) { + protected final void warning(final Class<?> caller, final String method, final Exception exception, final short key, final Object... arguments) { warning(decoder.listeners, caller, method, null, null, key, arguments); } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java index 3a0a6f5b0f..df99dcd9f6 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java @@ -507,7 +507,7 @@ public final class RasterResource extends AbstractGridCoverageResource implement * unsigned integer with a signed one). */ if (range.isEmpty()) { - band.warning(RasterResource.class, "getSampleDimensions", Resources.Keys.IllegalValueRange_4, + band.warning(RasterResource.class, "getSampleDimensions", null, Resources.Keys.IllegalValueRange_4, band.getFilename(), band.getName(), range.getMinValue(), range.getMaxValue()); } else { String name = band.getDescription(); @@ -594,12 +594,12 @@ public final class RasterResource extends AbstractGridCoverageResource implement * @return {@code true} if flag attributes have been found, or {@code false} otherwise. */ private static boolean createEnumeration(final SampleDimension.Builder builder, final Variable band) { - final Map<Integer,String> enumeration = band.getEnumeration(); + final Map<Long,String> enumeration = band.getEnumeration(); if (enumeration == null) { return false; } - for (final Map.Entry<Integer,String> entry : enumeration.entrySet()) { - final Number value = entry.getKey(); + for (final Map.Entry<Long,String> entry : enumeration.entrySet()) { + final Long value = entry.getKey(); CharSequence name = entry.getValue(); if (name == null) { name = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed); diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java index 7288cdb144..cabdbad744 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java @@ -133,7 +133,7 @@ public abstract class Variable extends Node { * @see #setEnumeration(Map) * @see #getEnumeration() */ - private Map<Integer,String> enumeration; + private Map<Long,String> enumeration; /** * The grid associated to this variable, or {@code null} if none or not yet computed. @@ -197,7 +197,7 @@ public abstract class Variable extends Node { /** * Initializes the map of enumeration values. If the given map is non-null, then the enumerations are set - * to the specified map (by direct reference; the map is not cloned). Otherwise this method auto-detects + * to the specified map (by direct reference, the map is not cloned). Otherwise this method auto-detects * if this variable is an enumeration. * * <p>This method is invoked by subclass constructors for completing {@code Variable} creation. @@ -208,7 +208,7 @@ public abstract class Variable extends Node { * @see #getEnumeration() */ @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter") - protected final void setEnumeration(Map<Integer,String> enumeration) { + protected final void setEnumeration(Map<Long,String> enumeration) { if (enumeration == null) { String srcLabels, srcNumbers; // For more accurate message in case of warning. CharSequence[] labels = getAttributeAsStrings(srcLabels = AttributeNames.FLAG_NAMES, ' '); @@ -224,13 +224,15 @@ public abstract class Variable extends Node { if (numbers != null) { final int n = numbers.size(); if (n != count) { - warning(Variable.class, "setEnumeration", Resources.Keys.MismatchedAttributeLength_5, + warning(Variable.class, "setEnumeration", null, + Resources.Keys.MismatchedAttributeLength_5, getName(), srcNumbers, srcLabels, n, count); if (n < count) count = n; } } else { numbers = Vector.createSequence(0, 1, count); - warning(Variable.class, "setEnumeration", Resources.Keys.MissingVariableAttribute_3, + warning(Variable.class, "setEnumeration", null, + Resources.Keys.MissingVariableAttribute_3, getFilename(), getName(), AttributeNames.FLAG_VALUES); } /* @@ -245,7 +247,8 @@ public abstract class Variable extends Node { for (int i=0; i<count; i++) try { final CharSequence label = labels[i]; if (label != null) { - enumeration.merge(numbers.intValue(i), label.toString(), (o,n) -> { + long value = numbers.longValue(i); + enumeration.merge(value, label.toString(), (o,n) -> { return o.equals(n) ? o : o + " | " + n; }); } @@ -254,12 +257,12 @@ public abstract class Variable extends Node { error = e; invalids = new StringBuilder(); } else { + error.addSuppressed(e); final int length = invalids.length(); - final boolean tooManyErrors = (length > 100); // Arbitrary limit. + final boolean tooManyErrors = (length > 100); // Arbitrary limit in number of characters. if (tooManyErrors && invalids.charAt(length - 1) == '…') { continue; } - error.addSuppressed(e); invalids.append(", "); if (tooManyErrors) { invalids.append('…'); @@ -269,8 +272,8 @@ public abstract class Variable extends Node { invalids.append(numbers.stringValue(i)); } if (invalids != null) { - error(Variable.class, "setEnumeration", error, - Errors.Keys.CanNotConvertValue_2, invalids, numbers.getElementType()); + warning(Variable.class, "setEnumeration", error, + Resources.Keys.UnsupportedEnumerationValue_3, getName(), numbers.getElementType(), invalids); } } if (!enumeration.isEmpty()) { @@ -625,7 +628,7 @@ public abstract class Variable extends Node { final Dimension gridDimension = domain.remove(label); dimensions[i] = gridDimension; if (gridDimension == null) { - warning(Variable.class, "getGridGeometry", // Caller (indirectly) for this method. + warning(Variable.class, "getGridGeometry", null, // Caller (indirectly) for this method. Resources.Keys.CanNotRelateVariableDimension_3, getFilename(), getName(), label); return null; } @@ -685,8 +688,8 @@ public abstract class Variable extends Node { public final GridGeometry getGridGeometry() throws IOException, DataStoreException { if (!gridDetermined) { gridDetermined = true; // Set first so we don't try twice in case of failure. - final GridMapping gridMapping = GridMapping.forVariable(this); - final GridAdjustment adjustment = new GridAdjustment(); + final var gridMapping = GridMapping.forVariable(this); + final var adjustment = new GridAdjustment(); final Grid info = findGrid(adjustment); if (info != null) { /* @@ -745,7 +748,7 @@ public abstract class Variable extends Node { if (grid != null) { if (grid.isDefined(GridGeometry.EXTENT)) { GridExtent extent = grid.getExtent(); - final long[] sizes = new long[extent.getDimension()]; + final var sizes = new long[extent.getDimension()]; boolean needsResize = false; for (int i=sizes.length; --i >= 0;) { final int d = (sizes.length - 1) - i; // Convert "natural order" index into netCDF index. @@ -757,7 +760,8 @@ public abstract class Variable extends Node { if (needsResize) { final double[] dataToGridIndices = adjustment.dataToGridIndices(); if (dataToGridIndices == null || dataToGridIndices.length < sizes.length) { - warning(Variable.class, "getGridGeometry", Resources.Keys.ResamplingIntervalNotFound_2, getFilename(), getName()); + warning(Variable.class, "getGridGeometry", null, + Resources.Keys.ResamplingIntervalNotFound_2, getFilename(), getName()); return null; } extent = extent.resize(sizes); @@ -908,7 +912,7 @@ public abstract class Variable extends Node { * @see #setEnumeration(Map) */ @SuppressWarnings("ReturnOfCollectionOrArrayField") - final Map<Integer,String> getEnumeration() { + final Map<Long,String> getEnumeration() { return enumeration; } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java index 3aa85d6251..2d1f1986d1 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java @@ -236,7 +236,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { final long actual = Integer.toUnsignedLong(size); if (actual != expected) { if (expected != 0) { - warning(ChannelDecoder.class, "readVariables", // Caller of this constructor. + warning(ChannelDecoder.class, "readVariables", null, // Caller of this constructor. Resources.Keys.MismatchedVariableSize_3, getFilename(), name, actual - expected); } if (actual > offsetToNextRecord) { diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java index 3617ff7587..4d665bedea 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java @@ -65,8 +65,8 @@ public class Resources extends IndexedResourceBundle { public static final short CanNotComputeVariablePosition_2 = 6; /** - * Cannot create the Coordinate Reference System for “{1}” in the “{0}” netCDF file. The reason - * is: {2} + * Cannot create the Coordinate Reference System “{1}” in the “{0}” netCDF file. The reason is: + * {2} */ public static final short CanNotCreateCRS_3 = 11; @@ -142,6 +142,12 @@ public class Resources extends IndexedResourceBundle { */ public static final short IllegalValueRange_4 = 16; + /** + * The CRS declared by WKT is inconsistent with the attributes of “{1}” in the “{0}” netCDF + * file. + */ + public static final short InconsistentCRS_2 = 29; + /** * Attributes “{1}” and “{2}” on variable “{0}” have different lengths: {3} and {4} * respectively. @@ -182,9 +188,10 @@ public class Resources extends IndexedResourceBundle { public static final short UnexpectedDimensionForVariable_4 = 2; /** - * Unknown projection parameters in file “{0}”: {1} + * The attributes of “{1}” in the “{0}” netCDF file contains unknown projection parameters: + * {2}. */ - public static final short UnknownProjectionParameters_2 = 25; + public static final short UnknownProjectionParameters_3 = 25; /** * Variable “{1}” in file “{0}” has {2,number} dimensions but only {3,number} can be associated @@ -196,6 +203,11 @@ public class Resources extends IndexedResourceBundle { * NetCDF file “{0}” uses unsupported data type {2} for variable “{1}”. */ public static final short UnsupportedDataType_3 = 5; + + /** + * Value “{2}” of enumeration “{0}” cannot be converted to the ‘{1}’ type. + */ + public static final short UnsupportedEnumerationValue_3 = 28; } /** diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties index c53c4b3965..46408eec38 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties @@ -21,7 +21,7 @@ # AmbiguousAxisDirection_4 = NetCDF file \u201c{0}\u201d provides an ambiguous axis direction for variable \u201c{1}\u201d. It could be {2}\u00a0or {3}. CanNotComputeVariablePosition_2 = Cannot compute data location for \u201c{1}\u201d variable in the \u201c{0}\u201d netCDF file. -CanNotCreateCRS_3 = Cannot create the Coordinate Reference System for \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2} +CanNotCreateCRS_3 = Cannot create the Coordinate Reference System \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2} CanNotCreateGridGeometry_3 = Cannot create the grid geometry \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2} CanNotInjectComponent_1 = Cannot inject component \u201c{0}\u201d in the reference system. CanNotRelateVariableDimension_3 = Cannot relate dimension \u201c{2}\u201d of variable \u201c{1}\u201d to a coordinate system dimension in netCDF file \u201c{0}\u201d. @@ -35,6 +35,7 @@ DuplicatedAxis_2 = Duplicated axis \u201c{1}\u201d in a grid of DuplicatedAxisType_4 = Axes \u201c{2}\u201d and \u201c{3}\u201d have the same type \u201c{1}\u201d in netCDF file \u201c{0}\u201d. IllegalAttributeValue_3 = Illegal value \u201c{2}\u201d for attribute \u201c{1}\u201d in netCDF file \u201c{0}\u201d. IllegalValueRange_4 = Illegal value range {2,number} \u2026 {3,number} for variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d. +InconsistentCRS_2 = The CRS declared by WKT is inconsistent with the attributes of \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. GridLongitudeSpanTooWide_2 = The grid spans {0}\u00b0 of longitude, which may be too wide for the \u201c{1}\u201d domain. MismatchedAttributeLength_5 = Attributes \u201c{1}\u201d and \u201c{2}\u201d on variable \u201c{0}\u201d have different lengths: {3} and {4} respectively. MismatchedVariableSize_3 = The declared size of variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d is {2,number} bytes greater than expected. @@ -43,6 +44,7 @@ MissingVariableAttribute_3 = Missing attribute \u201c{2}\u201d on the \u2 ResamplingIntervalNotFound_2 = Variable \u201c{1}\u201d or netCDF file \u201c{0}\u201d has a different size than its coordinate system, but no resampling interval is specified. UnexpectedAxisCount_4 = Reference system of type \u2018{1}\u2019 cannot have {2}\u00a0axes. The axes found in the \u201c{0}\u201d netCDF file are: {3}. UnexpectedDimensionForVariable_4 = Variable \u201c{1}\u201d in file \u201c{0}\u201d has a dimension \u201c{3}\u201d while we expected \u201c{2}\u201d. -UnknownProjectionParameters_2 = Unknown projection parameters in file \u201c{0}\u201d: {1} +UnknownProjectionParameters_3 = The attributes of \u201c{1}\u201d in the \u201c{0}\u201d netCDF file contains unknown projection parameters: {2}. UnmappedDimensions_4 = Variable \u201c{1}\u201d in file \u201c{0}\u201d has {2,number} dimensions but only {3,number} can be associated to a coordinate reference system. UnsupportedDataType_3 = NetCDF file \u201c{0}\u201d uses unsupported data type {2} for variable \u201c{1}\u201d. +UnsupportedEnumerationValue_3 = Value \u201c{2}\u201d of enumeration \u201c{0}\u201d cannot be converted to the \u2018{1}\u2019 type. diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties index dab8cba36d..4d6ea7da30 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties @@ -26,7 +26,7 @@ # AmbiguousAxisDirection_4 = Le fichier netCDF \u00ab\u202f{0}\u202f\u00bb fournit une direction d\u2019axe ambigu\u00eb pour la variable \u00ab\u202f{1}\u202f\u00bb. Elle pourrait \u00eatre {2}\u00a0ou {3}. CanNotComputeVariablePosition_2 = Ne peut pas calculer la position des donn\u00e9es de la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. -CanNotCreateCRS_3 = Ne peut pas cr\u00e9er le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es pour \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {2} +CanNotCreateCRS_3 = Ne peut pas cr\u00e9er le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u00a0: {2} 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\u00a0: {2} CanNotInjectComponent_1 = Ne peut pas pas ins\u00e9rer la composante \u00ab\u202f{0}\u202f\u00bb dans le syst\u00e8me de r\u00e9f\u00e9rence. 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. @@ -40,6 +40,7 @@ DuplicatedAxis_2 = Axe \u00ab\u202f{1}\u202f\u00bb dupliqu\u00e DuplicatedAxisType_4 = Les axes \u00ab\u202f{2}\u202f\u00bb et \u00ab\u202f{3}\u202f\u00bb d\u00e9clarent le m\u00eame type \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. IllegalAttributeValue_3 = La valeur \u00ab\u202f{2}\u202f\u00bb est ill\u00e9gale pour l\u2019attribut \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. IllegalValueRange_4 = Plage de valeurs {2,number} \u2026 {3,number} ill\u00e9gale pour la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. +InconsistentCRS_2 = Le syst\u00e8me de r\u00e9f\u00e9rence d\u00e9clar\u00e9 par WKT est incoh\u00e9rent avec les attributs de \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. GridLongitudeSpanTooWide_2 = La grille s\u2019\u00e9tend sur {0}\u00b0 de longitude, ce qui peut \u00eatre trop pour le domaine de \u00ab\u202f{1}\u202f\u00bb. MismatchedAttributeLength_5 = Les attributs \u201c{1}\u201d et \u201c{2}\u201d de la variable \u201c{0}\u201d ont des longueurs diff\u00e9rentes\u00a0: {3} et {4} respectivement. MismatchedVariableSize_3 = La longueur d\u00e9clar\u00e9e de la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb d\u00e9passe de {2,number} octets la valeur attendue. @@ -48,6 +49,7 @@ MissingVariableAttribute_3 = Il manque l\u2019attribut \u201c{2}\u201d su ResamplingIntervalNotFound_2 = La variable \u00ab\u202f{1}\u202f\u00bb du fichier netCDF \u00ab\u202f{0}\u202f\u00bb a une taille diff\u00e9rente de celle de son syst\u00e8me de coordonn\u00e9es, mais l\u2019intervalle d\u2019\u00e9chantillonnage n\u2019a pas \u00e9t\u00e9 sp\u00e9cifi\u00e9. UnexpectedAxisCount_4 = Les syst\u00e8mes de r\u00e9f\u00e9rence de type \u2018{1}\u2019 ne peuvent pas avoir {2}\u00a0axes. Les axes trouv\u00e9s dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb sont\u00a0: {3}. UnexpectedDimensionForVariable_4 = La variable \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb a une dimension \u00ab\u202f{3}\u202f\u00bb alors qu\u2019on attendait \u00ab\u202f{2}\u202f\u00bb. -UnknownProjectionParameters_2 = Param\u00e8tres de projection inconnus dans le fichier \u00ab\u202f{0}\u202f\u00bb\u00a0: {1} +UnknownProjectionParameters_3 = Les attributs de \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb contiennent des param\u00e8tres de projection inconnus\u00a0: {2}. UnmappedDimensions_4 = La variable \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb a {2,number} dimensions mais seulement {3,number} peuvent \u00eatre associ\u00e9es \u00e0 un syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es. UnsupportedDataType_3 = Le fichier netCDF \u00ab\u202f{0}\u202f\u00bb utilise un type de donn\u00e9es non-support\u00e9 {2} pour la variable \u00ab\u202f{1}\u202f\u00bb. +UnsupportedEnumerationValue_3 = La valeur \u00ab\u202f{2}\u202f\u00bb de l\u2019\u00e9numeration \u00ab\u202f{0}\u202f\u00bb ne peut pas \u00eatre convertie vers le type \u2018{1}\u2019. diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/VariableWrapper.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/VariableWrapper.java index ed90e30614..24a5cb3753 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/VariableWrapper.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/ucar/VariableWrapper.java @@ -54,6 +54,7 @@ import org.apache.sis.util.privy.Strings; import org.apache.sis.measure.MeasurementRange; import org.apache.sis.measure.NumberRange; import org.apache.sis.measure.Units; +import org.apache.sis.pending.jdk.JDK19; /** @@ -94,9 +95,15 @@ final class VariableWrapper extends org.apache.sis.storage.netcdf.base.Variable * If the UCAR library recognizes this variable as an enumeration, we will use UCAR services. * Only if UCAR did not recognized the enumeration, fallback on Apache SIS implementation. */ - Map<Integer,String> enumeration = null; + Map<Long,String> enumeration = null; if (variable.getDataType().isEnum()) { - enumeration = variable.getEnumTypedef().getMap(); + Map<Integer,String> m = variable.getEnumTypedef().getMap(); + if (m != null) { + enumeration = JDK19.newHashMap(m.size()); + for (Map.Entry<Integer,String> entry : m.entrySet()) { + enumeration.put(Integer.toUnsignedLong(entry.getKey()), entry.getValue()); + } + } } setEnumeration(enumeration); // Use SIS fallback if `enumeration` is null. } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/GridTest.java b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/GridTest.java index 6d7a28d010..7a1cdb4991 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/GridTest.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/base/GridTest.java @@ -69,6 +69,13 @@ public class GridTest extends TestCase { return geometries; } + /** + * Returns the coordinate system axes for the <abbr>CRS</abbr> decoded from the given file. + */ + private Axis[] axes(final TestData data) throws IOException, DataStoreException { + return getSingleton(filter(selectDataset(data).getGridCandidates())).getAxes(decoder()); + } + /** * Tests {@link Grid#getSourceDimensions()} and {@code Grid.getTargetDimensions()}. * @@ -95,7 +102,7 @@ public class GridTest extends TestCase { */ @Test public void testAxes2D() throws IOException, DataStoreException { - final Axis[] axes = getSingleton(filter(selectDataset(TestData.NETCDF_2D_GEOGRAPHIC).getGridCandidates())).getAxes(decoder()); + final Axis[] axes = axes(TestData.NETCDF_2D_GEOGRAPHIC); assertEquals(2, axes.length); final Axis x = axes[0]; final Axis y = axes[1]; @@ -118,7 +125,7 @@ public class GridTest extends TestCase { */ @Test public void testAxes4D() throws IOException, DataStoreException { - final Axis[] axes = getSingleton(filter(selectDataset(TestData.NETCDF_4D_PROJECTED).getGridCandidates())).getAxes(decoder()); + final Axis[] axes = axes(TestData.NETCDF_4D_PROJECTED); assertEquals(includeRuntimeDimension ? 5 : 4, axes.length); final Axis x = axes[0]; final Axis y = axes[1]; @@ -159,8 +166,8 @@ public class GridTest extends TestCase { final Node data = selectDataset(TestData.NETCDF_4D_PROJECTED).findNode("CIP"); final GridMapping mapping = GridMapping.forVariable((Variable) data); assertNotNull(mapping); - assertInstanceOf(ProjectedCRS.class, mapping.crs); - final ParameterValueGroup pg = ((ProjectedCRS) mapping.crs).getConversionFromBase().getParameterValues(); + assertInstanceOf(ProjectedCRS.class, mapping.crs()); + final ParameterValueGroup pg = ((ProjectedCRS) mapping.crs()).getConversionFromBase().getParameterValues(); assertEquals( 25, pg.parameter("Latitude of false origin") .doubleValue()); assertEquals(-95, pg.parameter("Longitude of false origin") .doubleValue()); assertEquals( 25, pg.parameter("Latitude of 1st standard parallel").doubleValue());