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:
*
* - time
* - lat - latitude
* - lon - longitude
*
* 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();
}
}