/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.image.io.netcdf;
import java.awt.image.DataBuffer;
import ucar.ma2.DataType;
import ucar.nc2.Attribute;
import ucar.nc2.Variable;
import ucar.nc2.VariableIF;
import ucar.nc2.dataset.VariableEnhanced;
import org.geotools.resources.XArray;
import org.geotools.image.io.metadata.Band;
/**
* Parses the offset, scale factor, minimum, maximum and fill values from a variable. This class
* duplicate UCAR's {@code EnhanceScaleMissingImpl} functionality, but we have to do that because:
* <p>
* <ul>
* <li>I have not been able to find any method giving me directly the offset and scale factor.
* We can use some trick with {@link VariableEnhanced#convertScaleOffsetMissing}, but
* they are subject to rounding errors and there is no efficient way I can see to take
* missing values in account.</li>
* <li>The {@link VariableEnhanced} methods are available only if the variable is enhanced.
* Our variable is not, because we want raw (packed) data.</li>
* <li>We want minimum, maximum and fill values in packed units (as opposed to the geophysics
* values provided by the UCAR's API), because we check for missing values before to
* convert them.</li>
* </ul>
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*/
final class VariableMetadata {
/**
* Raw image type as one of {@link DataBuffer} constants.
*/
private final int imageType;
/**
* The scale and and offset values, or {@link Double#NaN NaN} if none.
*/
public final double scale, offset;
/**
* The minimal and maximal valid values in geophysics units, or infinity if none.
* They are converted from the packed values if needed, as UCAR does.
*/
public final double minimum, maximum;
/**
* The fill and missing values in <strong>packed</strong> units, or {@code null} if none.
* Note that this is different from UCAR, who converts to geophysics values. We keep packed
* values in order to avoir rounding error. This array contains both the fill value and the
* missing values, without duplicated values.
*/
public final double[] missingValues;
/**
* The widest type found in attributes scanned by the {@link #attribute} method
* since the last time this field was set. This is a temporary variable used by
* the constructor only.
*/
private transient DataType widestType;
/**
* Extracts metadata from the specified variable using UCAR's API. This approach suffers
* from rounding errors and is unable to get the missing values. Use this constructor
* only for comparing our own results with the results from the UCAR's API.
*/
public VariableMetadata(final VariableEnhanced variable) {
imageType = getRawDataType(variable);
offset = variable.convertScaleOffsetMissing(0.0);
scale = variable.convertScaleOffsetMissing(1.0) - offset;
minimum = (variable.getValidMin() - offset) / scale;
maximum = (variable.getValidMax() - offset) / scale;
missingValues = null; // No way to get this information.
}
/**
* Extracts metadata from the specified variable using our own method.
*
* @param variable The variable to extract metadata from.
* @param forceRangePacking {@code true} if the valid range is encoded in geophysics units
* (which is a violation of CF convention), or {@code false} in order to autodetect
* using the UCAR heuristic rule.
*/
public VariableMetadata(final Variable variable, boolean forceRangePacking) {
final DataType dataType, scaleType, rangeType;
/*
* Gets the scale factors, if present. Also remember its type
* for the heuristic rule to be applied later on the valid range.
*/
imageType = getRawDataType(variable);
dataType = widestType = variable.getDataType();
scale = attribute(variable, "scale_factor");
offset = attribute(variable, "add_offset");
scaleType = widestType;
widestType = dataType; // Reset before we scan the other attributes.
/*
* Gets minimum and maximum. If a "valid_range" attribute is presents, it as precedence
* over "valid_min" and "valid_max" as specified in UCAR documentation.
*/
double minimum = Double.NaN;
double maximum = Double.NaN;
Attribute attribute = variable.findAttribute("valid_range");
if (attribute != null) {
widestType = widest(attribute.getDataType(), widestType);
Number value = attribute.getNumericValue(0);
if (value != null) {
minimum = value.doubleValue();
}
value = attribute.getNumericValue(1);
if (value != null) {
maximum = value.doubleValue();
}
}
if (Double.isNaN(minimum)) {
minimum = attribute(variable, "valid_min");
}
if (Double.isNaN(maximum)) {
maximum = attribute(variable, "valid_max");
}
rangeType = widestType;
widestType = dataType; // Reset before we scan the other attributes.
if (!forceRangePacking) {
// Heuristic rule defined in UCAR documentation (see EnhanceScaleMissing interface)
forceRangePacking = rangeType.equals(scaleType) &&
rangeType.equals(widest(rangeType, dataType));
}
if (forceRangePacking) {
final double offset = Double.isNaN(this.offset) ? 0 : this.offset;
final double scale = Double.isNaN(this.scale ) ? 1 : this.scale;
minimum = (minimum - offset) / scale;
maximum = (maximum - offset) / scale;
if (!isFloatingPoint(rangeType)) {
if (!Double.isNaN(minimum) && !Double.isInfinite(minimum)) {
minimum = Math.round(minimum);
}
if (!Double.isNaN(maximum) && !Double.isInfinite(maximum)) {
maximum = Math.round(maximum);
}
}
}
if (Double.isNaN(minimum)) minimum = Double.NEGATIVE_INFINITY;
if (Double.isNaN(maximum)) maximum = Double.POSITIVE_INFINITY;
this.minimum = minimum;
this.maximum = maximum;
/*
* Gets fill and missing values. According UCAR documentation, they are
* always in packed units. We keep them "as-is" (as opposed to UCAR who
* converts them to geophysics units), in order to avoid rounding errors.
* Note that we merge missing and fill values in a single array, without
* duplicated values.
*/
widestType = dataType;
attribute = variable.findAttribute("missing_value");
final double fillValue = attribute(variable, "_FillValue");
final int fillCount = Double.isNaN(fillValue) ? 0 : 1;
final int missingCount = (attribute != null) ? attribute.getLength() : 0;
final double[] missings = new double[fillCount + missingCount];
if (fillCount != 0) {
missings[0] = fillValue;
}
int count = fillCount;
scan: for (int i=0; i<missingCount; i++) {
final Number number = attribute.getNumericValue(i);
if (number != null) {
final double value = number.doubleValue();
if (!Double.isNaN(value)) {
for (int j=0; j<count; j++) {
if (value == missings[j]) {
// Current value duplicates a previous one.
continue scan;
}
}
missings[count++] = value;
}
}
}
missingValues = (count != 0) ? XArray.resize(missings, count) : null;
}
/**
* Returns the attribute value as a {@code double}.
*/
private double attribute(final Variable variable, final String name) {
final Attribute attribute = variable.findAttribute(name);
if (attribute != null) {
widestType = widest(attribute.getDataType(), widestType);
final Number value = attribute.getNumericValue();
if (value != null) {
return value.doubleValue();
}
}
return Double.NaN;
}
/**
* Returns the widest of two data types.
*/
private static DataType widest(final DataType type1, final DataType type2) {
if (type1 == null) return type2;
if (type2 == null) return type1;
final int size1 = type1.getSize();
final int size2 = type2.getSize();
if (size1 > size2) return type1;
if (size1 < size2) return type2;
return isFloatingPoint(type2) ? type2 : type1;
}
/**
* Returns {@code true} if the specified type is a floating point type.
*/
private static boolean isFloatingPoint(final DataType type) {
return DataType.FLOAT.equals(type) || DataType.DOUBLE.equals(type);
}
/**
* Returns the data type which most closely represents the "raw" internal data
* of the variable. This is the value returned by the default implementation of
* {@link NetcdfImageReader#getRawDataType}.
*
* @param variable The variable.
* @return The data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknown.
*
* @see NetcdfImageReader#getRawDataType
*/
static int getRawDataType(final VariableIF variable) {
final DataType type = variable.getDataType();
if (DataType.BOOLEAN.equals(type) || DataType.BYTE.equals(type)) {
return DataBuffer.TYPE_BYTE;
}
if (DataType.CHAR.equals(type)) {
return DataBuffer.TYPE_USHORT;
}
if (DataType.SHORT.equals(type)) {
return variable.isUnsigned() ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_SHORT;
}
if (DataType.INT.equals(type)) {
return DataBuffer.TYPE_INT;
}
if (DataType.FLOAT.equals(type)) {
return DataBuffer.TYPE_FLOAT;
}
if (DataType.LONG.equals(type) || DataType.DOUBLE.equals(type)) {
return DataBuffer.TYPE_DOUBLE;
}
return DataBuffer.TYPE_UNDEFINED;
}
/**
* Copies the value in this variable metadata into the specified band.
*/
public void copyTo(final Band band) {
band.setScale(scale);
band.setOffset(offset);
band.setPackedValues(minimum, maximum, missingValues, imageType);
}
}