001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2007, GeoTools Project Managment Committee (PMC)
005: * (C) 2007, Geomatys
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation;
010: * version 2.1 of the License.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.image.io.netcdf;
018:
019: // J2SE dependencies
020: import java.text.DateFormat;
021: import java.text.Format;
022: import java.text.NumberFormat;
023: import java.text.SimpleDateFormat;
024: import java.util.Date;
025: import java.util.Map;
026: import java.util.HashMap;
027: import java.util.List;
028: import java.util.Locale;
029: import java.util.TimeZone;
030: import java.util.logging.Level;
031: import java.util.logging.LogRecord;
032: import javax.imageio.ImageReader;
033:
034: // OpenGIS dependencies
035: import org.opengis.referencing.cs.AxisDirection;
036:
037: // NetCDF dependencies
038: import ucar.nc2.Variable;
039: import ucar.nc2.Attribute;
040: import ucar.nc2.dataset.AxisType;
041: import ucar.nc2.dataset.CoordinateAxis;
042: import ucar.nc2.dataset.CoordinateAxis1D;
043: import ucar.nc2.dataset.CoordinateSystem;
044: import ucar.nc2.dataset.NetcdfDataset;
045: import ucar.nc2.dataset.VariableDS;
046:
047: // Geotools dependencies
048: import org.geotools.image.io.metadata.Axis;
049: import org.geotools.image.io.metadata.ImageGeometry;
050: import org.geotools.image.io.metadata.ImageReferencing;
051: import org.geotools.image.io.metadata.MetadataAccessor;
052: import org.geotools.image.io.metadata.GeographicMetadata;
053: import org.geotools.image.io.metadata.GeographicMetadataFormat;
054: import org.geotools.util.logging.LoggedFormat;
055: import org.geotools.resources.i18n.Errors;
056: import org.geotools.resources.i18n.ErrorKeys;
057:
058: /**
059: * Metadata from NetCDF file. This implementation assumes that the NetCDF file follows the
060: * <A HREF="http://www.cfconventions.org/">CF Metadata conventions</A>.
061: * <p>
062: * <b>Limitation:</b>
063: * Current implementation retains only the first {@linkplain CoordinateSystem coordinate system}
064: * found in the NetCDF file or for a given variable. The {@link org.geotools.coverage.io} package
065: * would not know what to do with the extra coordinate systems anyway.
066: *
067: * @since 2.4
068: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio-netcdf/src/main/java/org/geotools/image/io/netcdf/NetcdfMetadata.java $
069: * @version $Id: NetcdfMetadata.java 27848 2007-11-12 13:10:32Z desruisseaux $
070: * @author Martin Desruisseaux
071: */
072: public class NetcdfMetadata extends GeographicMetadata {
073: /**
074: * Forces usage of UCAR libraries in some places where we use our own code instead.
075: * This may result in rounding errors and absence of information regarding fill values,
076: * but is useful for checking if we are doing the right thing compared to the UCAR way.
077: */
078: private static final boolean USE_UCAR_LIB = false;
079:
080: /**
081: * The mapping between UCAR axis type and ISO axis directions.
082: */
083: private static final Map/*<AxisType,AxisDirection>*/DIRECTIONS = new HashMap(
084: 16);
085: static {
086: add(AxisType.Time, AxisDirection.FUTURE);
087: add(AxisType.GeoX, AxisDirection.EAST);
088: add(AxisType.GeoY, AxisDirection.NORTH);
089: add(AxisType.GeoZ, AxisDirection.UP);
090: add(AxisType.Lat, AxisDirection.NORTH);
091: add(AxisType.Lon, AxisDirection.EAST);
092: add(AxisType.Height, AxisDirection.UP);
093: add(AxisType.Pressure, AxisDirection.UP);
094: }
095:
096: /**
097: * Adds a mapping between UCAR type and ISO direction.
098: */
099: private static void add(final AxisType type,
100: final AxisDirection direction) {
101: if (DIRECTIONS.put(type, direction) != null) {
102: throw new IllegalArgumentException(String.valueOf(type));
103: }
104: }
105:
106: /**
107: * Creates metadata from the specified file. This constructor is typically invoked
108: * for creating {@linkplain NetcdfReader#getStreamMetadata stream metadata}. Note that
109: * {@link ucar.nc2.dataset.CoordSysBuilder#addCoordinateSystems} should have been invoked
110: * (if needed) before this constructor.
111: */
112: public NetcdfMetadata(final ImageReader reader,
113: final NetcdfDataset file) {
114: super (reader);
115: final List/*<CoordinateSystem>*/systems = file
116: .getCoordinateSystems();
117: if (!systems.isEmpty()) {
118: addCoordinateSystem((CoordinateSystem) systems.get(0));
119: }
120: }
121:
122: /**
123: * Creates metadata from the specified file. This constructor is typically invoked
124: * for creating {@linkplain NetcdfReader#getImageMetadata image metadata}. Note that
125: * {@link ucar.nc2.dataset.CoordSysBuilder#addCoordinateSystems} should have been invoked
126: * (if needed) before this constructor.
127: */
128: public NetcdfMetadata(final ImageReader reader,
129: final VariableDS variable) {
130: super (reader);
131: final List/*<CoordinateSystem>*/systems = variable
132: .getCoordinateSystems();
133: if (!systems.isEmpty()) {
134: addCoordinateSystem((CoordinateSystem) systems.get(0));
135: }
136: setSampleType(GeographicMetadataFormat.PACKED);
137: addSampleDimension(variable);
138: }
139:
140: /**
141: * Adds the specified coordinate system. Current implementation can adds at most one
142: * coordinate system, but this limitation may be revisited in a future Geotools version.
143: *
144: * @param cs The coordinate system to add.
145: */
146: public void addCoordinateSystem(final CoordinateSystem cs) {
147: String crsType, csType;
148: if (cs.isLatLon()) {
149: crsType = cs.hasVerticalAxis() ? GeographicMetadataFormat.GEOGRAPHIC_3D
150: : GeographicMetadataFormat.GEOGRAPHIC;
151: csType = GeographicMetadataFormat.ELLIPSOIDAL;
152: } else if (cs.isGeoXY()) {
153: crsType = cs.hasVerticalAxis() ? GeographicMetadataFormat.PROJECTED_3D
154: : GeographicMetadataFormat.PROJECTED;
155: csType = GeographicMetadataFormat.CARTESIAN;
156: } else {
157: crsType = null;
158: csType = null;
159: }
160: final ImageReferencing referencing = getReferencing();
161: referencing.setCoordinateReferenceSystem(null, crsType);
162: referencing.setCoordinateSystem(cs.getName(), csType);
163: final ImageGeometry geometry = getGeometry();
164: geometry.setPixelOrientation("center");
165: /*
166: * Adds the axis in reverse order, because the NetCDF image reader put the last
167: * dimensions in the rendered image. Typical NetCDF convention is to put axis in
168: * the (time, depth, latitude, longitude) order, which typically maps to
169: * (longitude, latitude, depth, time) order in Geotools referencing framework.
170: */
171: final List/*<CoordinateAxis>*/axis = cs.getCoordinateAxes();
172: for (int i = axis.size(); --i >= 0;) {
173: addCoordinateAxis((CoordinateAxis) axis.get(i));
174: }
175: }
176:
177: /**
178: * Gets the name, as the "description", "title" or "standard name"
179: * attribute if possible, or as the variable name otherwise.
180: */
181: private static String getName(final Variable variable) {
182: String name = variable.getDescription();
183: if (name == null || (name = name.trim()).length() == 0) {
184: name = variable.getName();
185: }
186: return name;
187: }
188:
189: /**
190: * Adds the specified coordinate axis. This method is invoked recursively
191: * by {@link #addCoordinateSystem}.
192: *
193: * @param axis The axis to add.
194: */
195: public void addCoordinateAxis(final CoordinateAxis axis) {
196: final String name = getName(axis);
197: final AxisType type = axis.getAxisType();
198: String units = axis.getUnitsString();
199: /*
200: * Gets the axis direction, taking in account the possible reversal or vertical axis.
201: * Note that geographic and projected CRS have the same directions. We can distinguish
202: * them either using the ISO CRS type ("geographic" or "projected"), the ISO CS type
203: * ("ellipsoidal" or "cartesian") or the units ("degrees" or "m").
204: */
205: String direction = null;
206: AxisDirection directionCode = (AxisDirection) DIRECTIONS
207: .get(type);
208: if (directionCode != null) {
209: if (CoordinateAxis.POSITIVE_DOWN.equalsIgnoreCase(axis
210: .getPositive())) {
211: directionCode = directionCode.opposite();
212: }
213: direction = directionCode.name();
214: final int offset = units.lastIndexOf('_');
215: if (offset >= 0) {
216: final String unitsDirection = units.substring(
217: offset + 1).trim();
218: final String opposite = directionCode.opposite().name();
219: if (unitsDirection.equalsIgnoreCase(opposite)) {
220: warning("addCoordinateAxis",
221: ErrorKeys.INCONSISTENT_AXIS_ORIENTATION_$2,
222: new String[] { name, direction });
223: direction = opposite;
224: }
225: if (unitsDirection.equalsIgnoreCase(direction)) {
226: units = units.substring(0, offset).trim();
227: }
228: }
229: }
230: /*
231: * Gets the axis origin. In the particular case of time axis, units are typically
232: * written in the form "days since 1990-01-01 00:00:00". We extract the part before
233: * "since" as the units and the part after "since" as the date.
234: */
235: final Axis axisNode = getReferencing().addAxis(name, direction,
236: units);
237: if (AxisType.Time.equals(type)) {
238: String origin = null;
239: final String[] unitsParts = units
240: .split("(?i)\\s+since\\s+");
241: if (unitsParts.length == 2) {
242: units = unitsParts[0].trim();
243: origin = unitsParts[1].trim();
244: } else {
245: final Attribute attribute = axis
246: .findAttribute("time_origin");
247: if (attribute != null) {
248: origin = attribute.getStringValue();
249: }
250: }
251: Date epoch = null;
252: if (origin != null) {
253: origin = MetadataAccessor.trimFractionalPart(origin);
254: epoch = (Date) parse(type, origin, Date.class,
255: "addCoordinateAxis");
256: }
257: axisNode.setTimeOrigin(epoch);
258: axisNode.setUnits(units);
259: }
260: /*
261: * If the axis is not numeric, we can't process any further.
262: * If it is, then adds the coordinate and index ranges.
263: */
264: if (!axis.isNumeric()) {
265: return;
266: }
267: if (axis instanceof CoordinateAxis1D) {
268: final CoordinateAxis1D axis1D = (CoordinateAxis1D) axis;
269: final ImageGeometry geometry = getGeometry();
270: final int length = axis1D.getDimension(0).getLength();
271: if (length > 2 && axis1D.isRegular()) {
272: // Reminder: pixel orientation is "center", maximum value is inclusive.
273: final double increment = axis1D.getIncrement();
274: final double start = axis1D.getStart();
275: final double end = start + increment * (length - 1); // Inclusive
276: geometry.addCoordinateRange(0, length - 1, start, end);
277: } else {
278: final double[] values = axis1D.getCoordValues();
279: geometry.addCoordinateValues(0, values);
280: }
281: }
282: }
283:
284: /**
285: * Adds sample dimension information for the specified variable.
286: *
287: * @param variable The variable to add as a sample dimension.
288: */
289: public void addSampleDimension(final VariableDS variable) {
290: final VariableMetadata m;
291: if (USE_UCAR_LIB) {
292: m = new VariableMetadata(variable);
293: } else {
294: m = new VariableMetadata(variable,
295: forcePacking("valid_range"));
296: }
297: m.copyTo(addBand(getName(variable)));
298: }
299:
300: /**
301: * Parses the given string as a value along the specified axis.
302: *
303: * @param type The type of the axis.
304: * @param value The value along that axis.
305: * @param expected The expected type.
306: * @return The value after parsing.
307: */
308: private Object /*<T>*/parse(final AxisType type, String value,
309: final Class/*<T>*/expected, final String caller) {
310: final LoggedFormat format = createLoggedFormat(getAxisFormat(
311: type, value), expected);
312: format.setLogger("org.geotools.image.io.netcdf");
313: format.setCaller(NetcdfMetadata.class, caller);
314: return format.parse(value);
315: }
316:
317: /**
318: * Returns a format to use for parsing values along the specified axis type. This method
319: * is invoked when parsing the date part of axis units like "<cite>days since 1990-01-01
320: * 00:00:00</cite>". Subclasses should override this method if the date part is formatted
321: * in a different way. The default implementation returns the following formats:
322: * <p>
323: * <ul>
324: * <li>For {@linkplain AxisType#Time time axis}, a {@link DateFormat} using the
325: * {@code "yyyy-MM-dd HH:mm:ss"} pattern in UTC {@linkplain TimeZone timezone}.</li>
326: * <li>For all other kind of axis, a {@link NumberFormat}.</li>
327: * </ul>
328: * <p>
329: * The {@linkplain Locale#CANADA Canada locale} is used by default for most formats because
330: * it is relatively close to ISO (for example regarding days and months order in dates) while
331: * using the English symbols.
332: *
333: * @param type The type of the axis.
334: * @param prototype An example of the values to be parsed. Implementations may parse this
335: * prototype when the axis type alone is not suffisient. For example the {@linkplain
336: * AxisType#Time time axis type} should uses the {@code "yyyy-MM-dd"} date pattern,
337: * but some files do not follow this convention and use the default local instead.
338: * @return The format for parsing values along the axis.
339: */
340: protected Format getAxisFormat(final AxisType type,
341: final String prototype) {
342: if (!type.equals(AxisType.Time)) {
343: return NumberFormat.getNumberInstance(Locale.CANADA);
344: }
345: char dateSeparator = '-'; // The separator used in ISO format.
346: boolean yearLast = false; // Year is first in ISO pattern.
347: boolean namedMonth = false; // Months are numbers in the ISO pattern.
348: if (prototype != null) {
349: /*
350: * Performs a quick check on the prototype content. If the prototype seems to use a
351: * different date separator than the ISO one, we will adjust the pattern accordingly.
352: * Also checks if the year seems to appears last rather than first, and if the month
353: * seems to be written using letters rather than digits.
354: */
355: int field = 1;
356: int digitCount = 0;
357: final int length = prototype.length();
358: for (int i = 0; i < length; i++) {
359: final char c = prototype.charAt(i);
360: if (Character.isWhitespace(c)) {
361: break; // Checks only the dates, ignore the hours.
362: }
363: if (Character.isDigit(c)) {
364: digitCount++;
365: continue; // Digits are legal in all cases.
366: }
367: if (field == 2 && Character.isLetter(c)) {
368: namedMonth = true;
369: continue; // Letters are legal for month only.
370: }
371: if (field == 1) {
372: dateSeparator = c;
373: }
374: digitCount = 0;
375: field++;
376: }
377: if (digitCount >= 4) {
378: yearLast = true;
379: }
380: }
381: String pattern;
382: if (yearLast) {
383: pattern = namedMonth ? "dd-MMM-yyyy" : "dd-MM-yyyy";
384: } else {
385: pattern = namedMonth ? "yyyy-MMM-dd" : "yyyy-MM-dd";
386: }
387: pattern = pattern.replace('-', dateSeparator);
388: pattern += " HH:mm:ss";
389: final DateFormat format = new SimpleDateFormat(pattern,
390: Locale.CANADA);
391: format.setTimeZone(TimeZone.getTimeZone("UTC"));
392: return format;
393: }
394:
395: /**
396: * Returns {@code true} if an attribute (usually the <cite>valid range</cite>) should be
397: * converted from unpacked to packed units. The <A HREF="http://www.cfconventions.org/">CF
398: * Metadata conventions</A> states that valid ranges should be in packed units, but not
399: * every NetCDF files follow this advice in practice. The UCAR NetCDF library applies the
400: * following heuristic rules (quoting from {@link ucar.nc2.dataset.EnhanceScaleMissing}):
401: *
402: * <blockquote>
403: * If {@code valid_range} is the same type as {@code scale_factor} (actually the wider of
404: * {@code scale_factor} and {@code add_offset}) and this is wider than the external data,
405: * then it will be interpreted as being in the units of the internal (unpacked) data.
406: * Otherwise it is in the units of the external (packed) data.
407: * <blockquote>
408: *
409: * However some NetCDF files stores unpacked ranges using the same type than packed data.
410: * The above cited heuristic rule can not resolve those cases.
411: * <p>
412: * If this method returns {@code true}, then the attribute is assumed in unpacked units no
413: * matter what the CF convention and the heuristic rules said. If this method returns
414: * {@code false}, then UCAR's heuristic rules applies.
415: * <p>
416: * The default implementation returns {@code false} in all cases.
417: *
418: * @param attribute The attribute (usually {@code "valid_range"}).
419: * @return {@code true} if the attribute should be converted from unpacked to packed units
420: * regardless CF convention and UCAR's heuristic rules.
421: *
422: * @see ucar.nc2.dataset.EnhanceScaleMissing
423: */
424: protected boolean forcePacking(final String attribute) {
425: return false;
426: }
427:
428: /**
429: * Convenience method for logging a warning.
430: */
431: private void warning(final String method, final int key,
432: final Object value) {
433: final LogRecord record = Errors.getResources(getLocale())
434: .getLogRecord(Level.WARNING, key, value);
435: record.setSourceClassName(NetcdfMetadata.class.getName());
436: record.setSourceMethodName(method);
437: warningOccurred(record);
438: }
439: }
|