blob: 03c3666537a14031b0d8ff69c4159243daffb777 [file] [log] [blame]
/*
* 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;
}
}