| /* |
| * 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.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.ProjectedCRS; |
| import org.opengis.referencing.crs.GeographicCRS; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.TransformException; |
| import org.apache.sis.referencing.operation.transform.TransferFunction; |
| import org.apache.sis.referencing.datum.BursaWolfParameters; |
| import org.apache.sis.referencing.CommonCRS; |
| import org.apache.sis.internal.referencing.LazySet; |
| import org.apache.sis.measure.MeasurementRange; |
| import org.apache.sis.measure.NumberRange; |
| import org.apache.sis.math.Vector; |
| import org.apache.sis.util.Numbers; |
| import org.apache.sis.util.resources.Errors; |
| import ucar.nc2.constants.CDM; |
| import ucar.nc2.constants.CF; |
| |
| |
| /** |
| * Extends the CF-Conventions with some conventions particular to a data producer. |
| * By default, Apache SIS netCDF reader applies the <a href="http://cfconventions.org">CF conventions</a>. |
| * But some data producers does not provides all necessary information for allowing Apache SIS to read the |
| * netCDF file. Some information may be missing because considered implicit by the data producer. |
| * This class provides a mechanism for supplying the implicit values. |
| * Conventions can be registered in a file having this exact path: |
| * |
| * <blockquote><pre>META-INF/services/org.apache.sis.internal.netcdf.Convention</pre></blockquote> |
| * |
| * Instances of this class must be immutable and thread-safe. |
| * This class does not encapsulate all conventions needed for understanding a netCDF file, |
| * but only conventions that are more likely to need to be overridden for some data producers. |
| * |
| * <p><b>This is an experimental class for internal usage only (for now).</b> |
| * The API of this class is likely to change in any future Apache SIS version. |
| * This class may become public (in a modified form) in the future if we gain |
| * enough experience about extending netCDF conventions.</p> |
| * |
| * @author Johann Sorel (Geomatys) |
| * @author Martin Desruisseaux (Geomatys) |
| * @author Alexis Manin (Geomatys) |
| * @version 1.1 |
| * |
| * @see <a href="https://issues.apache.org/jira/browse/SIS-315">SIS-315</a> |
| * |
| * @since 1.0 |
| * @module |
| */ |
| public class Convention { |
| /** |
| * All conventions found on the classpath. |
| */ |
| private static final LazySet<Convention> AVAILABLES = new LazySet<>(Convention.class); |
| |
| /** |
| * The convention to use when no specific conventions were found. |
| */ |
| public static final Convention DEFAULT = new Convention(); |
| |
| /** |
| * Names of groups where to search for metadata, in precedence order. |
| * The {@code null} value stands for global attributes. |
| * |
| * <p>REMINDER: if modified, update {@link org.apache.sis.storage.netcdf.MetadataReader} class javadoc too.</p> |
| */ |
| private static final String[] SEARCH_PATH = {"NCISOMetadata", "CFMetadata", null, "THREDDSMetadata"}; |
| |
| /** |
| * Names of attributes where to fetch minimum and maximum sample values, in preference order. |
| * |
| * @see #validRange(Variable, Set) |
| */ |
| private static final String[] RANGE_ATTRIBUTES = { |
| "valid_range", // Expected "reasonable" range for variable. |
| "actual_range", // Actual data range for variable. |
| "valid_min", // Fallback if "valid_range" is not specified. |
| "valid_max" |
| }; |
| |
| /** |
| * Names of attributes where to fetch missing or pad values. Order matter since it determines the bits to be set in the |
| * map returned by {@link #nodataValues(Variable)}. The main bit is bit #0, which identifies the background value. |
| */ |
| private static final String[] NODATA_ATTRIBUTES = { |
| CDM.FILL_VALUE, |
| CDM.MISSING_VALUE |
| }; |
| |
| /** |
| * For subclass constructors. |
| */ |
| protected Convention() { |
| } |
| |
| /** |
| * Finds the convention to apply to the file opened by the given decoder, or {@code null} if none. |
| * This method does not change the state of the given {@link Decoder}. |
| */ |
| static Convention find(final Decoder decoder) { |
| final Iterator<Convention> it; |
| Convention c; |
| synchronized (AVAILABLES) { |
| it = AVAILABLES.iterator(); |
| if (!it.hasNext()) { |
| return DEFAULT; |
| } |
| c = it.next(); |
| } |
| /* |
| * We want the call to isApplicableTo(…) to be outside the synchronized block in order to avoid contentions. |
| * This is also a safety against dead locks if that method acquire other locks. Only Iterator methods should |
| * be invoked inside the synchronized block. |
| */ |
| while (!c.isApplicableTo(decoder)) { |
| synchronized (AVAILABLES) { |
| if (!it.hasNext()) { |
| c = DEFAULT; |
| break; |
| } |
| c = it.next(); |
| } |
| } |
| return c; |
| } |
| |
| /** |
| * Detects if this set of conventions applies to the given netCDF file. |
| * This method shall not change the state of the given {@link Decoder}. |
| * |
| * @param decoder the netCDF file to test. |
| * @return {@code true} if this set of conventions can apply. |
| */ |
| protected boolean isApplicableTo(final Decoder decoder) { |
| return false; |
| } |
| |
| /** |
| * Specifies a list of groups where to search for named attributes, in preference order. |
| * The {@code null} name stands for the root group. |
| * |
| * @return name of groups where to search in for global attributes, in preference order. |
| * Never null, never empty, but can contain null values to specify root as search path. |
| * |
| * @see Decoder#setSearchPath(String...) |
| */ |
| public String[] getSearchPath() { |
| return SEARCH_PATH.clone(); |
| } |
| |
| /** |
| * Returns the name of an attribute in this convention which is equivalent to the attribute of given name in CF-convention. |
| * The given parameter is a name from <cite>CF conventions</cite> or from <cite>Attribute Convention for Dataset Discovery |
| * (ACDD)</cite>. Some of those attribute names are listed in the {@link org.apache.sis.storage.netcdf.AttributeNames} class. |
| * |
| * <p>In current version of netCDF reader, this method is invoked only for global attributes, |
| * not for the attributes on variables.</p> |
| * |
| * <p>The default implementation returns {@code name} unchanged.</p> |
| * |
| * @param name an attribute name from CF or ACDD convention. |
| * @return the attribute name expected to be found in a netCDF file structured according this {@code Convention}. |
| * If this convention does not know about attribute of the given name, then {@code name} is returned unchanged. |
| */ |
| public String mapAttributeName(final String name) { |
| return name; |
| } |
| |
| /** |
| * Returns whether the given variable is used as a coordinate system axis, a coverage or something else. |
| * In particular this method shall return {@link VariableRole#AXIS} if the given variable seems to be a |
| * coordinate system axis instead than the actual data. By netCDF convention, coordinate system axes |
| * have the name of one of the dimensions defined in the netCDF header. |
| * |
| * <p>The default implementation returns {@link VariableRole#COVERAGE} if the given variable can be used |
| * for generating an image, by checking the following conditions:</p> |
| * |
| * <ul> |
| * <li>Images require at least {@value Grid#MIN_DIMENSION} dimensions of size equals or greater than {@value Grid#MIN_SPAN}. |
| * They may have more dimensions, in which case a slice will be taken later.</li> |
| * <li>Exclude axes. Axes are often already excluded by the above condition because axis are usually 1-dimensional, |
| * but some axes are 2-dimensional (e.g. a localization grid).</li> |
| * <li>Excludes characters, strings and structures, which can not be easily mapped to an image type. |
| * In addition, 2-dimensional character arrays are often used for annotations and we do not want |
| * to confuse them with images.</li> |
| * </ul> |
| * |
| * @param variable the variable for which to get the role. |
| * @return role of the given variable. |
| */ |
| public VariableRole roleOf(final Variable variable) { |
| if (variable.isCoordinateSystemAxis()) { |
| return VariableRole.AXIS; |
| } |
| int numVectors = 0; // Number of dimension having more than 1 value. |
| for (final Dimension dimension : variable.getGridDimensions()) { |
| if (dimension.length() >= Grid.MIN_SPAN) { |
| numVectors++; |
| } |
| } |
| if (numVectors >= Grid.MIN_DIMENSION) { |
| final DataType dataType = variable.getDataType(); |
| if (dataType.rasterDataType != DataBuffer.TYPE_UNDEFINED) { |
| return VariableRole.COVERAGE; |
| } |
| } |
| return VariableRole.OTHER; |
| } |
| |
| |
| // ┌────────────────────────────────────────────────────────────────────────────────────────────┐ |
| // │ COVERAGE DOMAIN │ |
| // └────────────────────────────────────────────────────────────────────────────────────────────┘ |
| |
| |
| /** |
| * Returns the names of the variables containing data for all dimension of a variable. |
| * Each netCDF variable can have an arbitrary number of dimensions identified by their name. |
| * The data for a dimension are usually stored in a variable of the same name, but not always. |
| * This method gives an opportunity for subclasses to select the axis variables using other criterion. |
| * This happen for example if a netCDF file defines two grids for the same dimensions. |
| * The order in returned array will be the axis order in the Coordinate Reference System. |
| * |
| * <p>This information is normally provided by the {@value ucar.nc2.constants.CF#COORDINATES} attribute, |
| * which is processed by the UCAR library (which is why we do not read this attribute ourselves here). |
| * This method is provided as a fallback when no such attribute is found. |
| * The default implementation returns {@code null}.</p> |
| * |
| * @param data the variable for which the list of axis variables are desired, in CRS order. |
| * @return names of the variables containing axis values, or {@code null} if this |
| * method performs applies no special convention for the given variable. |
| */ |
| public String[] namesOfAxisVariables(Variable data) { |
| return null; |
| } |
| |
| /** |
| * Returns the attribute-specified name of the dimension at the given index, or {@code null} if unspecified. |
| * This is not the name of the dimension encoded in netCDF binary file format, but rather a name specified |
| * by a customized attribute. This customized name can be used when the dimensions of the raster data are |
| * not the same than the dimensions of the localization grid. In such case, the names returned by this method |
| * are used for mapping the raster dimensions to the localization grid dimensions. |
| * |
| * <div class="note"><b>Example:</b> |
| * consider the following netCDF file (simplified): |
| * |
| * {@preformat text |
| * dimensions: |
| * grid_y = 161 ; |
| * grid_x = 126 ; |
| * data_y = 1599 ; |
| * data_x = 1250 ; |
| * variables: |
| * float Latitude(grid_y, grid_x) ; |
| * long_name = "Latitude (degree)" ; |
| * dim0 = "Line grids" ; |
| * dim1 = "Pixel grids" ; |
| * resampling_interval = 10 ; |
| * float Longitude(grid_y, grid_x) ; |
| * long_name = "Longitude (degree)" ; |
| * dim0 = "Line grids" ; |
| * dim1 = "Pixel grids" ; |
| * resampling_interval = 10 ; |
| * ushort SST(data_y, data_x) ; |
| * long_name = "Sea Surface Temperature" ; |
| * dim0 = "Line grids" ; |
| * dim1 = "Pixel grids" ; |
| * } |
| * |
| * In this case, even if {@link #namesOfAxisVariables(Variable)} explicitly returns {@code {"Latitude", "Longitude"}} |
| * we are still unable to associate the {@code SST} variable to those axes because they have no dimension in common. |
| * However if we interpret {@code dim0} and {@code dim1} attributes as <cite>"Name of dimension 0"</cite> and |
| * <cite>"Name of dimension 1"</cite> respectively, then we can associate the same dimension <strong>names</strong> |
| * to all those variables: namely {@code "Line grids"} and {@code "Pixel grids"}. Using those names, we deduce that |
| * the {@code (data_y, data_x)} dimensions in the {@code SST} variable are mapped to the {@code (grid_y, grid_x)} |
| * dimensions in the localization grid.</div> |
| * |
| * This feature is an extension to CF-conventions. |
| * |
| * @param dataOrAxis the variable for which to get the attribute-specified name of the dimension. |
| * @param index zero-based index of the dimension for which to get the name. |
| * @return dimension name as specified by attributes, or {@code null} if none. |
| */ |
| public String nameOfDimension(final Variable dataOrAxis, final int index) { |
| return dataOrAxis.getAttributeAsString("dim" + index); |
| } |
| |
| /** |
| * Returns the factor by which to multiply a grid index in order to get the corresponding data index. |
| * This is usually 1, meaning that there is an exact match between grid indices and data indices. |
| * This value may be different than 1 if the localization grid is smaller than the data grid, |
| * as documented in the {@link #nameOfDimension(Variable, int)}. |
| * |
| * <p>Default implementation returns the {@code "resampling_interval"} attribute value. |
| * This feature is an extension to CF-conventions.</p> |
| * |
| * @param axis the axis for which to get the "grid indices to data indices" scale factor. |
| * @return the "grid indices to data indices" scale factor, or {@link Double#NaN} if none. |
| */ |
| public double gridToDataIndices(final Variable axis) { |
| return axis.getAttributeAsNumber("resampling_interval"); |
| } |
| |
| /** |
| * Returns an enumeration of two-dimensional non-linear transforms (usually map projections) that may result |
| * in more linear localization grids. The enumerated transforms will be tested in "trials and errors" and the |
| * one resulting in best {@linkplain org.apache.sis.math.Plane#fit linear regression correlation coefficients} |
| * will be selected. |
| * |
| * <p>Default implementation returns an empty set.</p> |
| * |
| * @param decoder the netCDF file for which to get linearizer candidates. |
| * @return enumeration of two-dimensional non-linear transforms to try on the localization grid. |
| */ |
| public Set<Linearizer> linearizers(final Decoder decoder) { |
| return Collections.emptySet(); |
| } |
| |
| /** |
| * Returns the name of nodes (variables or groups) that may define the map projection parameters. |
| * The variables or groups will be inspected in the order they are declared in the returned set. |
| * For each string in the set, {@link Decoder#findNode(String)} is invoked and the return value |
| * (if non-null) is given to {@link #projection(Node)} until a non-null map is obtained. |
| * |
| * <div class="note"><b>API note:</b> |
| * this method name is singular because even if a set is returned, in the end only one value is used.</div> |
| * |
| * The default implementation returns the value of {@link CF#GRID_MAPPING} attribute, or an empty set |
| * if the given variable does not contain that attribute. Subclasses may override for example if grid |
| * mapping information are hard-coded in a particular node for a specific product. |
| * |
| * @param data the variable for which to get the grid mapping node. |
| * @return name of nodes that may contain the grid mapping, or an empty set if none. |
| */ |
| public Set<String> nameOfMappingNode(final Variable data) { |
| final String mapping = data.getAttributeAsString(CF.GRID_MAPPING); |
| return (mapping != null) ? Collections.singleton(mapping) : Collections.emptySet(); |
| } |
| |
| /** |
| * The {@value} attribute name from CF-convention, defined here because not yet provided in {@link CF}. |
| * Associated value shall be an instance of {@link Number}. This field may be removed in a future SIS |
| * version if this constant become defined in {@link ucar.nc2.constants}. |
| */ |
| protected static final String LONGITUDE_OF_PRIME_MERIDIAN = "longitude_of_prime_meridian"; |
| |
| /** |
| * The {@value} attribute name from CF-convention, defined here because not yet provided in {@link CF}. |
| * Associated value shall be an instance of {@link String}. This field may be removed in a future SIS |
| * version if this constant become defined in {@link ucar.nc2.constants}. |
| */ |
| protected static final String ELLIPSOID_NAME = "reference_ellipsoid_name", |
| PRIME_MERIDIAN_NAME = "prime_meridian_name", |
| GEODETIC_DATUM_NAME = "horizontal_datum_name", |
| GEOGRAPHIC_CRS_NAME = "geographic_crs_name", |
| PROJECTED_CRS_NAME = "projected_crs_name"; |
| |
| /** |
| * The {@value} attribute name, not yet part of CF-convention. |
| */ |
| protected static final String CONVERSION_NAME = "conversion_name"; |
| |
| /** |
| * The {@value} attribute name from CF-convention, defined here because not yet provided in {@link CF}. |
| * Associated value shall be an instance of {@link BursaWolfParameters}. |
| */ |
| protected static final String TOWGS84 = "towgs84"; |
| |
| /** |
| * Returns the map projection defined by the given node. The given {@code node} argument is one of the nodes |
| * named by {@link #nameOfMappingNode(Variable)} (typically a variable referenced by {@value CF#GRID_MAPPING} |
| * attribute on the data variable), or if no grid mapping attribute is found {@code node} may be directly the |
| * data variable (not a CF-compliant approach, but found in practice). If non-null, the returned map contains |
| * the following information ({@value CF#GRID_MAPPING_NAME} is mandatory, all other entries are optional): |
| * |
| * <table class="sis"> |
| * <caption>Content of the returned map</caption> |
| * <tr> |
| * <th>Key</th> |
| * <th>Value type</th> |
| * <th>Description</th> |
| * </tr><tr> |
| * <td>{@value CF#GRID_MAPPING_NAME}</td> |
| * <td>{@link String}</td> |
| * <td>Operation method name <strong>(mandatory)</strong></td> |
| * </tr><tr> |
| * <td>{@code "*_name"}</td> |
| * <td>{@link String}</td> |
| * <td>Name of a component (datum, base CRS, …)</td> |
| * </tr><tr> |
| * <td>{@value #LONGITUDE_OF_PRIME_MERIDIAN}</td> |
| * <td>{@link Number}</td> |
| * <td>Value in degrees relative to reference meridian.</td> |
| * </tr><tr> |
| * <td>(projection-dependent)</td> |
| * <td>{@link Number} or {@code double[]}</td> |
| * <td>Map projection parameter values</td> |
| * </tr><tr> |
| * <td>{@value #TOWGS84}</td> |
| * <td>{@link BursaWolfParameters}</td> |
| * <td>Datum shift information.</td> |
| * </tr> |
| * </table> |
| * |
| * The returned map must be modifiable for allowing callers to modify its content. |
| * |
| * @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 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> |
| */ |
| public Map<String,Object> projection(final Node node) { |
| final String method = node.getAttributeAsString(CF.GRID_MAPPING_NAME); |
| if (method == null) { |
| return null; |
| } |
| final Map<String,Object> definition = new HashMap<>(); |
| definition.put(CF.GRID_MAPPING_NAME, method); |
| for (final String name : node.getAttributeNames()) try { |
| final String ln = name.toLowerCase(Locale.US); |
| Object value; |
| switch (ln) { |
| case CF.GRID_MAPPING_NAME: continue; // Already stored. |
| case TOWGS84: { |
| /* |
| * Conversion to WGS 84 datum may be specified as Bursa-Wolf parameters. Encoding this information |
| * with the CRS is deprecated (the hard-coded WGS84 target datum is not always suitable) but still |
| * a common practice as of 2019. We require at least the 3 translation parameters. |
| */ |
| final Vector values = node.getAttributeAsVector(name); |
| if (values == null || values.size() < 3) continue; |
| final BursaWolfParameters bp = new BursaWolfParameters(CommonCRS.WGS84.datum(), null); |
| bp.setValues(values.doubleValues()); |
| 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")) { |
| value = node.getAttributeAsString(name); |
| if (value == null) continue; |
| } |
| /* |
| * Assume that all map projection parameters in netCDF files are numbers or array of numbers. |
| * If values are array, then they are converted to an array of {@code double[]} type. |
| */ |
| value = node.getAttributeValue(name); |
| if (value == null) continue; |
| if (value instanceof Vector) { |
| value = ((Vector) value).doubleValues(); |
| } |
| break; |
| } |
| } |
| if (definition.putIfAbsent(name, value) != null) { |
| node.error(Convention.class, "projection", null, Errors.Keys.DuplicatedIdentifier_1, name); |
| } |
| } catch (NumberFormatException e) { |
| // May happen in the vector contains number stored as texts. |
| node.decoder.illegalAttributeValue(name, node.getAttributeAsString(name), e); |
| } |
| return definition; |
| } |
| |
| /** |
| * Returns the <cite>grid to CRS</cite> transform for the given node. This method is invoked after call |
| * to {@link #projection(Node)} method resulted in creation of a projected coordinate reference system. |
| * The {@linkplain ProjectedCRS#getBaseCRS() base CRS} is fixed to (latitude, longitude) axes in degrees, |
| * but the projected CRS axes may have any order and units. In the particular case of "latitude_longitude" |
| * pseudo-projection, the "projected" CRS is actually a {@link GeographicCRS} instance. |
| * The returned transform, if non-null, shall map cell corners. |
| * |
| * <div class="note"><b>API notes:</b> |
| * <ul> |
| * <li>We do not provide a {@link ProjectedCRS} argument because of the {@code "latitude_longitude"} special case.</li> |
| * <li>Base CRS axis order is (latitude, longitude) for increasing the chances to have a CRS identified by EPSG.</li> |
| * </ul></div> |
| * |
| * The default implementation returns {@code null}. |
| * |
| * @param node the same node than the one given to {@link #projection(Node)}. |
| * @param baseToCRS conversion from (latitude, longitude) in degrees to the projected CRS. |
| * @return the <cite>grid corner to CRS</cite> transform, or {@code null} if none or unknown. |
| * @throws TransformException if a coordinate operation was required but failed. |
| */ |
| public MathTransform gridToCRS(final Node node, final MathTransform baseToCRS) throws TransformException { |
| return null; |
| } |
| |
| /** |
| * Returns an identification of default geodetic components to use if no corresponding information is found in the |
| * netCDF file. The default implementation returns <cite>"Unknown datum based upon the GRS 1980 ellipsoid"</cite>. |
| * Note that the GRS 1980 ellipsoid is close to WGS 84 ellipsoid. |
| * |
| * <div class="note"><b>Maintenance note:</b> |
| * if this default is changed, search also for "GRS 1980" strings in {@link CRSBuilder} class.</div> |
| * |
| * @param spherical whether to restrict the ellipsoid to a sphere. |
| * @return information about geodetic objects to use if no explicit information is found in the file. |
| */ |
| public CommonCRS defaultHorizontalCRS(final boolean spherical) { |
| return spherical ? CommonCRS.SPHERE : CommonCRS.GRS1980; |
| } |
| |
| |
| // ┌────────────────────────────────────────────────────────────────────────────────────────────┐ |
| // │ COVERAGE RANGE │ |
| // └────────────────────────────────────────────────────────────────────────────────────────────┘ |
| |
| |
| /** |
| * Returns the range of valid values, or {@code null} if unknown. |
| * The default implementation takes the range of values from the following properties, in precedence order: |
| * |
| * <ol> |
| * <li>{@code "valid_range"} — expected "reasonable" range for variable.</li> |
| * <li>{@code "actual_range"} — actual data range for variable.</li> |
| * <li>{@code "valid_min"} — ignored if {@code "valid_range"} is present, as specified in UCAR documentation.</li> |
| * <li>{@code "valid_max"} — idem.</li> |
| * </ol> |
| * |
| * Whether the returned range is a range of packed values or a range of real values is ambiguous. |
| * An heuristic rule is documented in UCAR {@link ucar.nc2.dataset.EnhanceScaleMissing} interface. |
| * If both types of range are available, then this method should return the range of packed value. |
| * Otherwise if this method returns the range of real values, then that range shall be an instance |
| * of {@link MeasurementRange} for allowing the caller to distinguish the two cases. |
| * |
| * @param data the variable to get valid range of values for (usually a variable containing raster data). |
| * @param nodataValues the fill values and padding values. |
| * @return the range of valid values, or {@code null} if unknown. |
| * |
| * @see Variable#getRangeFallback() |
| */ |
| public NumberRange<?> validRange(final Variable data, final Set<Number> nodataValues) { |
| Number minimum = null; |
| Number maximum = null; |
| Class<? extends Number> type = null; |
| for (final String attribute : RANGE_ATTRIBUTES) { |
| final Vector values = data.getAttributeAsVector(attribute); |
| if (values != null) { |
| final int length = values.size(); |
| for (int i=0; i<length; i++) try { |
| Number value = values.get(i); // May throw NumberFormatException if value was stored as text. |
| if (value instanceof Float) { |
| final float fp = (Float) value; |
| if (fp == +Float.MAX_VALUE) value = Float.POSITIVE_INFINITY; |
| else if (fp == -Float.MAX_VALUE) value = Float.NEGATIVE_INFINITY; |
| } else if (value instanceof Double) { |
| final double fp = (Double) value; |
| if (fp == +Double.MAX_VALUE) value = Double.POSITIVE_INFINITY; |
| else if (fp == -Double.MAX_VALUE) value = Double.NEGATIVE_INFINITY; |
| } |
| type = Numbers.widestClass(type, value.getClass()); |
| minimum = Numbers.cast(minimum, type); |
| maximum = Numbers.cast(maximum, type); |
| value = Numbers.cast(value, type); |
| if (!attribute.endsWith("max") && (minimum == null || compare(value, minimum) < 0)) minimum = value; |
| if (!attribute.endsWith("min") && (maximum == null || compare(value, maximum) > 0)) maximum = value; |
| } catch (NumberFormatException e) { |
| data.decoder.illegalAttributeValue(attribute, values.stringValue(i), e); |
| } |
| } |
| /* |
| * Stop the loop and return a range as soon as we have enough information. |
| * Note that we may loop over many attributes before to complete information. |
| */ |
| if (minimum != null && maximum != null) { |
| /* |
| * Heuristic rule defined in UCAR documentation (see EnhanceScaleMissing interface): |
| * if the type of the range is equal to the type of the scale, and the type of the |
| * data is not wider, then assume that the minimum and maximum are real values. |
| */ |
| final Class<?> scaleType = data.getAttributeType(CDM.SCALE_FACTOR); |
| final Class<?> offsetType = data.getAttributeType(CDM.ADD_OFFSET); |
| final int rangeType = Numbers.getEnumConstant(type); |
| if ((scaleType != null || offsetType != null) |
| && rangeType >= data.getDataType().number |
| && rangeType >= Math.max(Numbers.getEnumConstant(scaleType), |
| Numbers.getEnumConstant(offsetType))) |
| { |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| final NumberRange<?> range = new MeasurementRange(type, minimum, true, maximum, true, data.getUnit()); |
| return range; |
| } else { |
| /* |
| * The range use sample values (before conversion to the unit of measurement). |
| * Before to return that range, check if the minimum or maximum overlaps with |
| * a pad value. If this is the case, resolve the overlapping by making that |
| * value exclusive instead than inclusive. |
| */ |
| boolean isMinIncluded = true; |
| boolean isMaxIncluded = true; |
| if (!nodataValues.isEmpty()) { |
| final double minValue = minimum.doubleValue(); |
| final double maxValue = maximum.doubleValue(); |
| for (final Number pad : nodataValues) { |
| final double value = pad.doubleValue(); |
| isMinIncluded &= (minValue != value); |
| isMaxIncluded &= (maxValue != value); |
| } |
| } |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| final NumberRange<?> range = new NumberRange(type, minimum, isMinIncluded, maximum, isMaxIncluded); |
| return range; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Compares two numbers which shall be of the same class. |
| * This is a helper method for {@link #validRange(Variable, Set)}. |
| */ |
| @SuppressWarnings("unchecked") |
| private static int compare(final Number n1, final Number n2) { |
| return ((Comparable) n1).compareTo((Comparable) n2); |
| } |
| |
| /** |
| * Returns all no-data values declared for the given variable, or an empty map if none. |
| * The map keys are the no-data values (pad sample values or missing sample values). |
| * The map values can be either {@link String} or {@link org.opengis.util.InternationalString} values |
| * containing the description of the no-data value, or an {@link Integer} set to a bitmask identifying |
| * the role of the pad/missing sample value: |
| * |
| * <ul> |
| * <li>If bit 0 is set, then the value is a pad value. Those values can be used for background.</li> |
| * <li>If bit 1 is set, then the value is a missing value.</li> |
| * </ul> |
| * |
| * Pad values should be first in the map, followed by missing values. |
| * The same value may have more than one role. |
| * |
| * <p>The default implementation returns a modifiable {@link LinkedHashMap}. |
| * Subclasses can add their own entries to the returned map.</p> |
| * |
| * @param data the variable for which to get no-data values. |
| * @return no-data values with bitmask of their roles or textual descriptions. |
| */ |
| public Map<Number,Object> nodataValues(final Variable data) { |
| final Map<Number,Object> pads = new LinkedHashMap<>(); |
| for (int i=0; i < NODATA_ATTRIBUTES.length; i++) { |
| final String name = NODATA_ATTRIBUTES[i]; |
| final Vector values = data.getAttributeAsVector(name); |
| if (values != null) { |
| final int length = values.size(); |
| for (int j=0; j<length; j++) try { |
| pads.merge(values.get(j), 1 << i, (v1, v2) -> ((Integer) v1) | ((Integer) v2)); |
| } catch (NumberFormatException e) { |
| data.decoder.illegalAttributeValue(name, values.stringValue(i), e); |
| } |
| } |
| } |
| return pads; |
| } |
| |
| /** |
| * Builds the function converting values from their packed formats in the variable to "real" values. |
| * The transfer function is typically built from the {@code "scale_factor"} and {@code "add_offset"} |
| * attributes associated to the given variable, but other conventions could use different attributes. |
| * The returned function will be a component of the {@link org.apache.sis.coverage.SampleDimension} |
| * to be created for each variable. |
| * |
| * <p>This method is invoked in contexts where a transfer function is assumed to exist, for example |
| * because {@link #validRange(Variable, Set)} returned a non-null value. Consequently this method |
| * shall never return {@code null}, but can return the identity function.</p> |
| * |
| * @param data the variable from which to determine the transfer function. |
| * This is usually a variable containing raster data. |
| * |
| * @return a transfer function built from the attributes defined in the given variable. Never null; |
| * if no information is found in the given {@code data} variable, then the return value |
| * shall be an identity function. |
| */ |
| public TransferFunction transferFunction(final Variable data) { |
| /* |
| * If scale_factor and/or add_offset variable attributes are present, then this is |
| * a "packed" variable. Otherwise the transfer function is the identity transform. |
| */ |
| final TransferFunction tr = new TransferFunction(); |
| final double scale = data.getAttributeAsNumber(CDM.SCALE_FACTOR); |
| final double offset = data.getAttributeAsNumber(CDM.ADD_OFFSET); |
| if (!Double.isNaN(scale)) tr.setScale (scale); |
| if (!Double.isNaN(offset)) tr.setOffset(offset); |
| return tr; |
| } |
| } |