001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2007, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.image.io.netcdf;
017:
018: import java.awt.image.DataBuffer;
019:
020: // NetCDF dependencies
021: import ucar.ma2.DataType;
022: import ucar.nc2.Attribute;
023: import ucar.nc2.Variable;
024: import ucar.nc2.VariableIF;
025: import ucar.nc2.dataset.VariableEnhanced;
026:
027: // Geotools dependencies
028: import org.geotools.resources.XArray;
029: import org.geotools.image.io.metadata.Band;
030:
031: /**
032: * Parses the offset, scale factor, minimum, maximum and fill values from a variable. This class
033: * duplicate UCAR's {@code EnhanceScaleMissingImpl} functionality, but we have to do that because:
034: * <p>
035: * <ul>
036: * <li>I have not been able to find any method giving me directly the offset and scale factor.
037: * We can use some trick with {@link VariableEnhanced#convertScaleOffsetMissing}, but
038: * they are subject to rounding errors and there is no efficient way I can see to take
039: * missing values in account.</li>
040: * <li>The {@link VariableEnhanced} methods are available only if the variable is enhanced.
041: * Our variable is not, because we want raw (packed) data.</li>
042: * <li>We want minimum, maximum and fill values in packed units (as opposed to the geophysics
043: * values provided by the UCAR's API), because we check for missing values before to
044: * convert them.</li>
045: * </ul>
046: *
047: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio-netcdf/src/main/java/org/geotools/image/io/netcdf/VariableMetadata.java $
048: * @version $Id: VariableMetadata.java 27583 2007-10-23 11:29:26Z desruisseaux $
049: * @author Martin Desruisseaux
050: */
051: final class VariableMetadata {
052: /**
053: * Raw image type as one of {@link DataBuffer} constants.
054: */
055: private final int imageType;
056:
057: /**
058: * The scale and and offset values, or {@link Double#NaN NaN} if none.
059: */
060: public final double scale, offset;
061:
062: /**
063: * The minimal and maximal valid values in geophysics units, or infinity if none.
064: * They are converted from the packed values if needed, as UCAR does.
065: */
066: public final double minimum, maximum;
067:
068: /**
069: * The fill and missing values in <strong>packed</strong> units, or {@code null} if none.
070: * Note that this is different from UCAR, who converts to geophysics values. We keep packed
071: * values in order to avoir rounding error. This array contains both the fill value and the
072: * missing values, without duplicated values.
073: */
074: public final double[] missingValues;
075:
076: /**
077: * The widest type found in attributes scanned by the {@link #attribute} method
078: * since the last time this field was set. This is a temporary variable used by
079: * the constructor only.
080: */
081: private transient DataType widestType;
082:
083: /**
084: * Extracts metadata from the specified variable using UCAR's API. This approach suffers
085: * from rounding errors and is unable to get the missing values. Use this constructor
086: * only for comparing our own results with the results from the UCAR's API.
087: */
088: public VariableMetadata(final VariableEnhanced variable) {
089: imageType = getRawDataType(variable);
090: offset = variable.convertScaleOffsetMissing(0.0);
091: scale = variable.convertScaleOffsetMissing(1.0) - offset;
092: minimum = (variable.getValidMin() - offset) / scale;
093: maximum = (variable.getValidMax() - offset) / scale;
094: missingValues = null; // No way to get this information.
095: }
096:
097: /**
098: * Extracts metadata from the specified variable using our own method.
099: *
100: * @param variable The variable to extract metadata from.
101: * @param forceRangePacking {@code true} if the valid range is encoded in geophysics units
102: * (which is a violation of CF convention), or {@code false} in order to autodetect
103: * using the UCAR heuristic rule.
104: */
105: public VariableMetadata(final Variable variable,
106: boolean forceRangePacking) {
107: final DataType dataType, scaleType, rangeType;
108: /*
109: * Gets the scale factors, if present. Also remember its type
110: * for the heuristic rule to be applied later on the valid range.
111: */
112: imageType = getRawDataType(variable);
113: dataType = widestType = variable.getDataType();
114: scale = attribute(variable, "scale_factor");
115: offset = attribute(variable, "add_offset");
116: scaleType = widestType;
117: widestType = dataType; // Reset before we scan the other attributes.
118: /*
119: * Gets minimum and maximum. If a "valid_range" attribute is presents, it as precedence
120: * over "valid_min" and "valid_max" as specified in UCAR documentation.
121: */
122: double minimum = Double.NaN;
123: double maximum = Double.NaN;
124: Attribute attribute = variable.findAttribute("valid_range");
125: if (attribute != null) {
126: widestType = widest(attribute.getDataType(), widestType);
127: Number value = attribute.getNumericValue(0);
128: if (value != null) {
129: minimum = value.doubleValue();
130: }
131: value = attribute.getNumericValue(1);
132: if (value != null) {
133: maximum = value.doubleValue();
134: }
135: }
136: if (Double.isNaN(minimum)) {
137: minimum = attribute(variable, "valid_min");
138: }
139: if (Double.isNaN(maximum)) {
140: maximum = attribute(variable, "valid_max");
141: }
142: rangeType = widestType;
143: widestType = dataType; // Reset before we scan the other attributes.
144: if (!forceRangePacking) {
145: // Heuristic rule defined in UCAR documentation (see EnhanceScaleMissing interface)
146: forceRangePacking = rangeType.equals(scaleType)
147: && rangeType.equals(widest(rangeType, dataType));
148: }
149: if (forceRangePacking) {
150: final double offset = Double.isNaN(this .offset) ? 0
151: : this .offset;
152: final double scale = Double.isNaN(this .scale) ? 1
153: : this .scale;
154: minimum = (minimum - offset) / scale;
155: maximum = (maximum - offset) / scale;
156: if (!isFloatingPoint(rangeType)) {
157: if (!Double.isNaN(minimum)
158: && !Double.isInfinite(minimum)) {
159: minimum = Math.round(minimum);
160: }
161: if (!Double.isNaN(maximum)
162: && !Double.isInfinite(maximum)) {
163: maximum = Math.round(maximum);
164: }
165: }
166: }
167: if (Double.isNaN(minimum))
168: minimum = Double.NEGATIVE_INFINITY;
169: if (Double.isNaN(maximum))
170: maximum = Double.POSITIVE_INFINITY;
171: this .minimum = minimum;
172: this .maximum = maximum;
173: /*
174: * Gets fill and missing values. According UCAR documentation, they are
175: * always in packed units. We keep them "as-is" (as opposed to UCAR who
176: * converts them to geophysics units), in order to avoid rounding errors.
177: * Note that we merge missing and fill values in a single array, without
178: * duplicated values.
179: */
180: widestType = dataType;
181: attribute = variable.findAttribute("missing_value");
182: final double fillValue = attribute(variable, "_FillValue");
183: final int fillCount = Double.isNaN(fillValue) ? 0 : 1;
184: final int missingCount = (attribute != null) ? attribute
185: .getLength() : 0;
186: final double[] missings = new double[fillCount + missingCount];
187: if (fillCount != 0) {
188: missings[0] = fillValue;
189: }
190: int count = fillCount;
191: scan: for (int i = 0; i < missingCount; i++) {
192: final Number number = attribute.getNumericValue(i);
193: if (number != null) {
194: final double value = number.doubleValue();
195: if (!Double.isNaN(value)) {
196: for (int j = 0; j < count; j++) {
197: if (value == missings[j]) {
198: // Current value duplicates a previous one.
199: continue scan;
200: }
201: }
202: missings[count++] = value;
203: }
204: }
205: }
206: missingValues = (count != 0) ? XArray.resize(missings, count)
207: : null;
208: }
209:
210: /**
211: * Returns the attribute value as a {@code double}.
212: */
213: private double attribute(final Variable variable, final String name) {
214: final Attribute attribute = variable.findAttribute(name);
215: if (attribute != null) {
216: widestType = widest(attribute.getDataType(), widestType);
217: final Number value = attribute.getNumericValue();
218: if (value != null) {
219: return value.doubleValue();
220: }
221: }
222: return Double.NaN;
223: }
224:
225: /**
226: * Returns the widest of two data types.
227: */
228: private static DataType widest(final DataType type1,
229: final DataType type2) {
230: if (type1 == null)
231: return type2;
232: if (type2 == null)
233: return type1;
234: final int size1 = type1.getSize();
235: final int size2 = type2.getSize();
236: if (size1 > size2)
237: return type1;
238: if (size1 < size2)
239: return type2;
240: return isFloatingPoint(type2) ? type2 : type1;
241: }
242:
243: /**
244: * Returns {@code true} if the specified type is a floating point type.
245: */
246: private static boolean isFloatingPoint(final DataType type) {
247: return DataType.FLOAT.equals(type)
248: || DataType.DOUBLE.equals(type);
249: }
250:
251: /**
252: * Returns the data type which most closely represents the "raw" internal data
253: * of the variable. This is the value returned by the default implementation of
254: * {@link NetcdfImageReader#getRawDataType}.
255: *
256: * @param variable The variable.
257: * @return The data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknown.
258: *
259: * @see NetcdfImageReader#getRawDataType
260: */
261: static int getRawDataType(final VariableIF variable) {
262: final DataType type = variable.getDataType();
263: if (DataType.BOOLEAN.equals(type) || DataType.BYTE.equals(type)) {
264: return DataBuffer.TYPE_BYTE;
265: }
266: if (DataType.CHAR.equals(type)) {
267: return DataBuffer.TYPE_USHORT;
268: }
269: if (DataType.SHORT.equals(type)) {
270: return variable.isUnsigned() ? DataBuffer.TYPE_USHORT
271: : DataBuffer.TYPE_SHORT;
272: }
273: if (DataType.INT.equals(type)) {
274: return DataBuffer.TYPE_INT;
275: }
276: if (DataType.FLOAT.equals(type)) {
277: return DataBuffer.TYPE_FLOAT;
278: }
279: if (DataType.LONG.equals(type) || DataType.DOUBLE.equals(type)) {
280: return DataBuffer.TYPE_DOUBLE;
281: }
282: return DataBuffer.TYPE_UNDEFINED;
283: }
284:
285: /**
286: * Copies the value in this variable metadata into the specified band.
287: */
288: public void copyTo(final Band band) {
289: band.setScale(scale);
290: band.setOffset(offset);
291: band
292: .setPackedValues(minimum, maximum, missingValues,
293: imageType);
294: }
295: }
|