import java.io.File; import java.io.IOException; import java.util.LinkedList; import java.util.List; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.format.DateTimeFormatterBuilder; import ucar.ma2.Array; import ucar.ma2.DataType; import ucar.ma2.IndexIterator; import ucar.nc2.Attribute; import ucar.nc2.Dimension; import ucar.nc2.NetcdfFile; import ucar.nc2.Variable; /** * This class serializes a netcdf file to JSON. The netcdf file needs to have * the following dimensions: *
    *
  1. time
  2. *
  3. lat - latitude
  4. *
  5. lon - longitude
  6. *
* and one more more 3 dimensional variables with the time, lat, and lon * dimensions in that order. * * */ public class NetCdfSerializer { /** * The first time variable in the netcdf file. */ private Variable timeVariable = null; /** * The time range from the */ private String timeRangeString = null; /** * The latitude variable in the netcdf file. */ private Variable latitudeVariable = null; /** * The longitude variable in the netcdf file. */ private Variable longitudeVariable = null; /** * The data variables in the netcdf file. */ private List dataVariables = new LinkedList(); /** * Data day variable, if it exists. */ private Variable dataDayVariable = null; /** * Data month variable, if it exists. */ private Variable dataMonthVariable = null; /** * The number of spaces for each indent level. */ private static int NUM_SPACES = 2; /** * The netcdf file we are converting. */ private NetcdfFile file = null; /** * The temporal resolution of the paired data. */ private String temporalResolution = null; /** * @throws Exception * */ public static void main(String[] args) throws Exception { OptionParser parser = new OptionParser(); OptionSpec fileSpec = parser.accepts("file").withRequiredArg() .ofType(File.class); OptionSpec sigDigitsSpec = parser.accepts("significantDigits") .withRequiredArg().ofType(Integer.class); OptionSet options = parser.parse(args); if (!options.has(fileSpec) || !options.has(sigDigitsSpec)) { throw new Exception( "Usage: java NetCdfSerializer --file file --significantDigits number"); } File file = options.valueOf(fileSpec); int significantDigits = options.valueOf(sigDigitsSpec); System.out.println(convert(file, significantDigits)); } /** * Converts a netcdf file to json. See {@link NetCdfSerializer} for a * description of the format of the netcdf file. * * @param netCdfFile * the location of the netcdf file * @param significantDigits * the number of significant digits to print out * @return the json string * @throws IOException */ public static String convert(File netCdfFile, int significantDigits) throws Exception { NetCdfSerializer obj = null; if (!netCdfFile.exists()) { throw new Exception(String.format("File does not exist: %s", netCdfFile.getCanonicalPath())); } try { obj = new NetCdfSerializer(netCdfFile); return obj.convertNotStatic(significantDigits); } finally { try { obj.closeFile(); } catch (Exception e) { // nothing to do } } } /** * Constructs a converter from a netcdf file. * * @param netCdfFile * the file we are going to convert. * @throws Exception */ private NetCdfSerializer(File netCdfFile) throws Exception { // open the netcdf file file = NetcdfFile.open(netCdfFile.toString()); // and the variables List variables = file.getVariables(); // go through the variables and find the latitude variable, longitude // variable, and data variables. for (Variable var : variables) { String name = var.getShortName(); if (name.equals("lat")) { latitudeVariable = var; } else if (name.equals("lon")) { longitudeVariable = var; } else if (name.equals("dataday")) { dataDayVariable = var; } else if (name.equals("datamonth")) { dataMonthVariable = var; } else { // see if this is a serializable data variable List attributes = var.getAttributes(); for (Attribute att : attributes) { if (att.getShortName().equals("plot_hint_axis_title")) { dataVariables.add(var); } } } } // now figure out the time dimension variable of the first data // variable, if it exists if (dataVariables.size() != 2) { throw new Exception( "Expected to find 2 plot hint variables, actually found " + dataVariables.size() + "."); } timeVariable = getTimeDimensionVariable(dataVariables.get(0), variables); // get the time resolution from global attributes List globalAttributes = file.getGlobalAttributes(); for (Attribute attribute : globalAttributes) { if (attribute.getShortName().equals("temporal_resolution") || attribute.getShortName() .equals("input_temporal_resolution")) { temporalResolution = attribute.getStringValue(); } } if (temporalResolution == null) { // default to hourly temporalResolution = "hourly"; } // set the time range string setTimeRangeString(); } /** * Get the time dimension variable associated with a data variable * * @param dataVariable * the data variable * @param variables * the variables in the file * @return the time dimension variable or null if it can't be found */ private Variable getTimeDimensionVariable(Variable dataVariable, List variables) { List dimensions = dataVariable.getDimensions(); for (Dimension dimension : dimensions) { String name = dimension.getShortName(); // find the variable with the same name for (Variable variable : variables) { if (variable.getShortName().equals(name)) { // now see if this variable is a time dimension String standardName = getStandardName(variable); if (standardName != null && standardName.equals("time")) { return variable; } } } } return null; } /** * Get the value of the standard_name attribute for a variable * * @param variable * the variable * @return the standard_name or null if there is no standard_name */ private String getStandardName(Variable variable) { List attributes = variable.getAttributes(); for (Attribute attribute : attributes) { if (attribute.getShortName().equals("standard_name")) { return attribute.getStringValue(); } } return null; } /** * The object's conversion function to convert the file we've opened into * JSON * * @param significantDigits * the number of significant digits to write in the JSON. * @return the JSON string * @throws Exception */ private String convertNotStatic(int significantDigits) throws Exception { StringBuffer buf = new StringBuffer(String.format("{\n")); addTime(buf, significantDigits); buf.append(String.format(",\n")); if (latitudeVariable != null) { addGeoDimension(buf, significantDigits, latitudeVariable); buf.append(String.format(",\n")); } if (longitudeVariable != null) { addGeoDimension(buf, significantDigits, longitudeVariable); buf.append(String.format(",\n")); } addDataVariables(buf, significantDigits, dataVariables); buf.append(String.format("\n}\n")); return buf.toString(); } /** * Add the time variable * * @param buf * @throws Exception */ private void addTime(StringBuffer buf, int significantDigits) throws Exception { buf.append(String.format(getIndentStr(NUM_SPACES) + "\"time\": {\n")); buf.append( String.format(getIndentStr(2 * NUM_SPACES) + "\"data\": \n")); String[] timeStrings = null; if (timeVariable == null) { // we don't have a time variables, so use the the time range string timeStrings = new String[1]; timeStrings[0] = timeRangeString; } else { // we have a time variable, so format it into strings if (dataDayVariable != null) { timeStrings = getFormattedDataDay(); } else if (dataMonthVariable != null) { timeStrings = getFormattedDataMonth(); } else { timeStrings = getFormattedTime(); } } buf.append(getIndentStr(3 * NUM_SPACES) + "["); for (int i = 0; i < timeStrings.length; i++) { buf.append(String.format(" \"%s\"", timeStrings[i])); if (i != (timeStrings.length - 1)) { buf.append(","); } } buf.append("]\n" + getIndentStr(NUM_SPACES) + "}"); } /** * Fomat date strings from the data day * * @return * @throws IOException */ private String[] getFormattedDataDay() throws Exception { Array timeArray = dataDayVariable.read(); // Get the number of index positions. shape[] will be a one element // array. int[] shape = timeArray.getShape(); String[] timeStrings = new String[shape[0]]; for (int i = 0; i < shape[0]; i++) { int timeInt = timeArray.getInt(i); // The format of the time is YYYYDDD. int year = timeInt / 1000; int day = timeInt - year * 1000; DateTime time = new DateTime(year, 1, 1, 0, 0); time = time.plusDays(day - 1); timeStrings[i] = convertTime(time, "daily"); } return timeStrings; } private String[] getFormattedDataMonth() throws Exception { Array timeArray = dataMonthVariable.read(); // Get the number of index positions. shape[] will be a one element // array. int[] shape = timeArray.getShape(); String[] timeStrings = new String[shape[0]]; for (int i = 0; i < shape[0]; i++) { int timeInt = timeArray.getInt(i); // The format of the time is YYYYMM. int year = timeInt / 100; int month = timeInt - year * 100; DateTime time = new DateTime(year, 1, 1, 0, 0); time = time.plusMonths(month - 1); timeStrings[i] = convertTime(time, "monthly"); } return timeStrings; } /** * Format date strings from the time dimension. * * @return string representations of the times in the time dimension * @throws Exception */ private String[] getFormattedTime() throws Exception { Array timeArray = timeVariable.read(); String units = timeVariable.findAttribute("units").getStringValue(); // Get the number of index positions. Time is a dimension, so shape[] // will be a one element array int[] shape = timeArray.getShape(); // get the date out of the units. This is the number after // "seconds since", "days since", etc. DateTime baseDate = getBaseDateTime(units); String[] timeStrings = new String[shape[0]]; for (int i = 0; i < shape[0]; i++) { DateTime time = null; if (units.startsWith("seconds since")) { int value = 0; switch (timeVariable.getDataType()) { case SHORT: value = timeArray.getShort(i); break; case INT: value = timeArray.getInt(i); break; case LONG: value = (int) timeArray.getLong(i); break; case FLOAT: value = Math.round(timeArray.getFloat(i)); break; case DOUBLE: value = (int) Math.round(timeArray.getDouble(i)); break; default: throw new Exception(String.format( "Unable to understand time data type %s", timeVariable.getDataType().toString())); } time = baseDate.plusSeconds(value); } else { throw new Exception(String .format("Unable to understand time units '%s'", units)); } timeStrings[i] = convertTime(time, temporalResolution); } return timeStrings; } /** * Create a DateTime from the stuff after '... since' in the units string. I * assume this string looks like 'YYYY-MM-DD HH:MM:SS' * * @param units * @return the date */ private static DateTime getBaseDateTime(String units) { // TODO: find out about the date format for CF-1 compliance! int beginIndex = units.length() - 19; String dateString = units.substring(beginIndex); // first four numbers are the year return getDateTime(dateString); } /** * Creates a DateTime from strings that look like 'YYYY-MM-DD hh:mm:ss' or * 'YYYY-MM-DDThh:mm:ssZ' * * @param dateString * @return the date */ private static DateTime getDateTime(String dateString) { int year = Integer.parseInt(dateString.substring(0, 4)); int month = Integer.parseInt(dateString.substring(5, 7)); int day = Integer.parseInt(dateString.substring(8, 10)); int hour = Integer.parseInt(dateString.substring(11, 13)); int minute = Integer.parseInt(dateString.substring(14, 16)); int second = Integer.parseInt(dateString.substring(17, 19)); return new DateTime(year, month, day, hour, minute, second, DateTimeZone.UTC); } /** * Figures out the time string based on the matched start and end time * global attributes. * * @throws Exception */ private void setTimeRangeString() throws Exception { List attributes = file.getGlobalAttributes(); String startTimeString = null; String endTimeString = null; for (Attribute att : attributes) { switch (att.getShortName()) { case "matched_start_time": startTimeString = att.getStringValue(); break; case "matched_end_time": endTimeString = att.getStringValue(); break; } } if (startTimeString == null) { throw new Exception("Unable to find matched_start_time"); } else if (endTimeString == null) { throw new Exception("Unable to find matched_end_time"); } String formattedStartTime = convertTime(getDateTime(startTimeString), temporalResolution); String formattedEndTime = convertTime(getDateTime(endTimeString), temporalResolution); if (formattedStartTime.equals(formattedEndTime)) { timeRangeString = formattedStartTime; } else { timeRangeString = formattedStartTime + " - " + formattedEndTime; } } /** * Convert the time into a string. * * @param time * the time to convert * @param temporalResolution * the temporal resolution of the data * @return the date in YYYY-MM-DDTHH:MM:SSZ * @throws Exception */ private static String convertTime(DateTime time, String temporalResolution) throws Exception { // make sure inputTime is UTC time = time.toDateTime(DateTimeZone.UTC); // create a formatter in the specific format we want DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); switch (temporalResolution) { case ("monthly"): builder.appendYear(4, 4); builder.appendLiteral('-'); builder.appendMonthOfYear(2); break; case ("daily"): builder.appendYear(4, 4); builder.appendLiteral('-'); builder.appendMonthOfYear(2); builder.appendLiteral('-'); builder.appendDayOfMonth(2); break; case ("hourly"): // fall through case ("3-hourly"): builder.appendYear(4, 4); builder.appendLiteral('-'); builder.appendMonthOfYear(2); builder.appendLiteral('-'); builder.appendDayOfMonth(2); builder.appendLiteral(' '); builder.appendHourOfDay(2); builder.appendLiteral('Z'); break; case ("half-hourly"): builder.appendYear(4, 4); builder.appendLiteral('-'); builder.appendMonthOfYear(2); builder.appendLiteral('-'); builder.appendDayOfMonth(2); builder.appendLiteral(' '); builder.appendHourOfDay(2); builder.appendLiteral(':'); builder.appendMinuteOfHour(2); builder.appendLiteral('Z'); break; default: throw new Exception("Unrecognized temporal resolution '" + temporalResolution + "'"); } return time.toString(builder.toFormatter()); } /** * Add the data variables to the JSON string * * @param buf * buffer holding the JSON string * @param significantDigits * number of significant digits for doubles/floats * @param dataVariables * list of the data variables * @throws Exception */ private static void addDataVariables(StringBuffer buf, int significantDigits, List dataVariables) throws Exception { int i = 0; for (Variable var : dataVariables) { // get the offset and scale factor, if they exist double addOffset = 0; Attribute att = var.findAttribute("add_offset"); if (att != null) { addOffset = att.getNumericValue().doubleValue(); } double scaleFactor = 1; att = var.findAttribute("scale_factor"); if (att != null) { scaleFactor = att.getNumericValue().doubleValue(); } // element name String name = var.getShortName(); buf.append(String.format( getIndentStr(NUM_SPACES) + "\"" + name + "\": {\n")); // units (TT 26193) Attribute units = var.findAttribute("units"); String unitsStr = "1"; if (units != null) { unitsStr = units.getStringValue(); } buf.append(String.format( getIndentStr(2 * NUM_SPACES) + "\"units\": \"%s\",\n", unitsStr)); // quantity type Attribute quantity_type = var.findAttribute("quantity_type"); buf.append(String.format( getIndentStr(2 * NUM_SPACES) + "\"quantity_type\": \"%s\",\n", quantity_type.getStringValue())); // long name Attribute long_name = var.findAttribute("long_name"); buf.append(String.format( getIndentStr(2 * NUM_SPACES) + "\"long_name\": \"%s\",\n", long_name.getStringValue())); // product Attribute product_short_name = var .findAttribute("product_short_name"); buf.append(String.format( getIndentStr(2 * NUM_SPACES) + "\"product_short_name\": \"%s\",\n", product_short_name.getStringValue())); // version Attribute product_version = var.findAttribute("product_version"); buf.append(String.format( getIndentStr(2 * NUM_SPACES) + "\"product_version\": \"%s\",\n", product_version.getStringValue())); // fill value Attribute fillValue = var.findAttribute("_FillValue"); if (fillValue != null) { double fill = fillValue.getNumericValue().doubleValue(); // correct with bias and offset fill = fill * scaleFactor + addOffset; buf.append(String.format( getIndentStr(2 * NUM_SPACES) + "\"_FillValue\": [%s],\n", printValue(fill, significantDigits))); } // plot hint axis title Attribute plot_hint_axis_title = var .findAttribute("plot_hint_axis_title"); buf.append(String.format( getIndentStr(2 * NUM_SPACES) + "\"plot_hint_axis_title\": \"%s\",\n", plot_hint_axis_title.getStringValue())); // put in the data buf.append(String .format(getIndentStr(2 * NUM_SPACES) + "\"data\": \n")); Array array = var.read(); // see if this is a 2D array or 3D array int[] shape = array.getShape(); if (shape.length == 3) { writeArray(buf, array, significantDigits, 3 * NUM_SPACES, var.getDataType(), scaleFactor, addOffset); } else if (shape.length == 2) { // add a fake dimension layer first before calling write array buf.append(getIndentStr(3 * NUM_SPACES) + "[ " + String.format("\n")); writeArray(buf, array, significantDigits, 4 * NUM_SPACES, var.getDataType(), scaleFactor, addOffset); buf.append(String.format("\n") + getIndentStr(3 * NUM_SPACES) + "]"); } else { throw new Exception( "Expected data fields to be two or three dimensional variables"); } buf.append("\n" + getIndentStr(NUM_SPACES) + "}"); // put an end of line between variables i++; if (i != dataVariables.size()) { buf.append(String.format(",\n")); } } } /** * Add a latitude or longitude dimension * * @param buf * buffer for JSON * @param significantDigits * significant digits for the lat & lon * @param var * the latitude or longitude variable * @throws Exception */ private static void addGeoDimension(StringBuffer buf, int significantDigits, Variable var) throws Exception { String name = var.getShortName(); // element name buf.append(String .format(getIndentStr(NUM_SPACES) + "\"" + name + "\": {\n")); // units Attribute units = var.findAttribute("units"); buf.append(String.format( getIndentStr(2 * NUM_SPACES) + "\"units\": \"%s\",\n", units.getStringValue())); buf.append( String.format(getIndentStr(2 * NUM_SPACES) + "\"data\": \n")); Array array = var.read(); writeArray(buf, array, significantDigits, 3 * NUM_SPACES, var.getDataType(), 1, 0); buf.append("\n" + getIndentStr(NUM_SPACES) + "}"); } /** * Create an an array of spaces of length 'length' * * @param length * the length of the array * @return a 'length' length space array */ private static String getIndentStr(int length) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < length; i++) { buf.append(' '); } return buf.toString(); } /** * Write an n-dimensional array as JSON * * @param buf * buffer for JSON * @param arr * array of data * @param significantDigits * number of significant digits * @param indent * number of indent spaces * @param dataType * array type * @param scaleFactor * the scale factor for this variable. (Set to 1 if there is no * scale factor.) * @param addOffset * the offset for this variable. (Set to 0 if there is no * offset.) * @throws Exception * */ private static void writeArray(StringBuffer buf, Array arr, int significantDigits, int indent, DataType dataType, double scaleFactor, double addOffset) throws Exception { // get the number of index positions in each dimension int[] shape = arr.getShape(); if (shape.length == 1) { // if our shape length is 1, we have a 1-dimensional array, which is // easy to print buf.append(getIndentStr(indent) + "[ "); // create an index into the array int i = 0; IndexIterator it = arr.getIndexIterator(); while (it.hasNext()) { double value = it.getDoubleNext(); value = value * scaleFactor + addOffset; buf.append(printValue(value, significantDigits)); // print a ',' if appropriate i++; if (i != shape[0]) { buf.append(", "); } } buf.append(" ]"); } else { // if we have higher dimensions, we need to call this print code on // each slice of this array buf.append(String.format(getIndentStr(indent) + "[\n")); for (int i = 0; i < shape[0]; i++) { // print a slice writeArray(buf, arr.slice(0, i), significantDigits, indent + NUM_SPACES, dataType, scaleFactor, addOffset); // put a comma between slices if (i != (shape[0] - 1)) { buf.append(String.format(",\n")); } else { buf.append(String.format("\n")); } } buf.append(getIndentStr(indent) + "]"); } } /** * This function checks to see if value is a special number like NaN and * then serializes to JSON appropriately. * * @param value * the value to serialize * @param significantDigits * the number of digits for non-special values * @return json representation */ private static String printValue(double value, int significantDigits) { if (Double.isNaN(value) || Double.isInfinite(value)) { return "null"; } else { // Create the format string for float/double with the right number // of // significant digits String doubleFormatStr = String.format("%%.%dg", significantDigits); return String.format(doubleFormatStr, value); } } /** * Closes the netCDF file. * * @throws IOException */ private void closeFile() throws IOException { file.close(); } }