package org.autoplot.pds; import java.io.ByteArrayOutputStream; import java.net.URL; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.autoplot.datasource.URISplit; import org.das2.util.LoggerManager; import org.json.JSONException; import org.json.JSONObject; import org.json.XML; import org.w3c.dom.Node; /** * * @author jbf */ public class PDS3DataObject { private static final Logger logger= LoggerManager.getLogger("apdss.pds"); private String name; private String uri; /** * number of records or bytes into the file, 1 is the first offset into the file. */ private FilePointer filePointer; private int recordBytes; // number of bytes in each record. private int rowBytes; // number of bytes taken by each row of the dataset. private int rowPrefixBytes; // additional bytes offset for each record (often 0). private int rowSuffixBytes; // additional bytes after the bytes of each record (often 0). private int rows; private String interchangeFormat; private String dataType; private int startByte; private int items; private int itemBytes; private int bytes; private String dims; // dimensions for rank 2 or higher data, like "[64,48]" or "[3]" private double validMinimum; private double validMaximum; private double missingConstant; private String unit; private String description; /** * spreadsheet field number, 1 is the first column. */ private int fieldNumber; JSONObject labelJSONObject; JSONObject columnJSONObject; JSONObject tableJSONObject; /* 86365 3 4 12 -1600000.0 1600000.0 9990000.0 nT MAG vector in J */ public PDS3DataObject( Node label, Node table, Node column) { try { labelJSONObject= toJSONObject( label ); labelJSONObject.toString(4); JSONObject jtable= toJSONObject(table); tableJSONObject= jtable; tableJSONObject.toString(4); interchangeFormat= jtable.optString("INTERCHANGE_FORMAT", "ASCII"); rowBytes= jtable.getInt("ROW_BYTES"); recordBytes= labelJSONObject.optInt("RECORD_BYTES",-1); rowPrefixBytes= jtable.optInt("ROW_PREFIX_BYTES",0); rowSuffixBytes= jtable.optInt("ROW_SUFFIX_BYTES",0); rows= jtable.optInt("ROWS",-1); JSONObject j= toJSONObject(column); columnJSONObject= j; if ( j.has("ITEMS") ) { items= j.getInt("ITEMS"); } else { items= -1; } startByte= j.optInt("START_BYTE",0); if ( items>1 ) { bytes= j.optInt("BYTES",-1); if ( bytes>-1 ) bytes= bytes / items; } else { bytes= j.optInt("BYTES",-1); } if ( column.getNodeName().equals("CONTAINER") ) { // all other properties come from the innermost node. XPathFactory factory = XPathFactory.newInstance(); XPath xpath = factory.newXPath(); Node column1= (Node) xpath.evaluate("COLUMN",column,XPathConstants.NODE); if ( column1==null ) { column1= (Node) xpath.evaluate("CONTAINER/COLUMN",column,XPathConstants.NODE); String dim0=(String)xpath.evaluate("REPETITIONS/text()",column,XPathConstants.STRING); String dim1=(String)xpath.evaluate("CONTAINER/REPETITIONS/text()",column,XPathConstants.STRING); dims= "["+dim0+","+dim1+"]"; itemBytes= bytes/(Integer.parseInt(dim1)); } else { String sitems= (String)xpath.evaluate("REPETITIONS/text()",column,XPathConstants.STRING); dims= "["+sitems+"]"; itemBytes= bytes/(Integer.parseInt(sitems)); } j= toJSONObject(column1); } else { itemBytes = bytes; } dataType= j.optString("DATA_TYPE",""); fieldNumber= j.optInt("FIELD_NUMBER",-1); unit= j.optString("UNIT",""); validMaximum= j.optDouble("VALID_MAXIMUM",Double.POSITIVE_INFINITY); validMinimum= j.optDouble("VALID_MINIMUM",Double.NEGATIVE_INFINITY); missingConstant= j.optDouble("MISSING_CONSTANT",Double.NaN); if ( Double.isNaN(missingConstant) && j.has("MISSING") ) { logger.fine("MISSING used instead of MISSING_CONSTANT, which is okay"); missingConstant= j.optDouble("MISSING",missingConstant); } if ( Double.isNaN(missingConstant) ) { missingConstant= j.optDouble("INVALID_CONSTANT",Double.NaN); } description= j.optString("DESCRIPTION", ""); } catch (TransformerException | JSONException ex) { throw new IllegalArgumentException("unable to run",ex); } catch (XPathExpressionException ex) { Logger.getLogger(PDS3DataObject.class.getName()).log(Level.SEVERE, null, ex); } } /** * convert the Document into a JSON object which is easier to work with. * @param n * @return * @throws TransformerConfigurationException * @throws TransformerException * @throws JSONException */ protected static JSONObject toJSONObject(Node n) throws TransformerConfigurationException, TransformerException, JSONException { TransformerFactory transfac = TransformerFactory.newInstance(); transfac.setAttribute("indent-number", 4); Transformer trans = transfac.newTransformer(); // Set up desired output format trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); trans.setOutputProperty(OutputKeys.INDENT, "yes"); ByteArrayOutputStream out= new ByteArrayOutputStream(); //create string from xml tree // StringWriter sw = new StringWriter(); StreamResult streamResult = new StreamResult(out); DOMSource source = new DOMSource(n); trans.transform(source, streamResult); JSONObject result= XML.toJSONObject(out.toString()); return result.getJSONObject(n.getNodeName()); } /** * return the Autoplot URI to load the data. * @param resource the granule (binary or ASCII) file to load. * @return the URI. */ public String resolveUri(URL resource) { if ( interchangeFormat.equals("ASCII") && fieldNumber>-1 ) { return getAsciiUri(resource); } else { return getBinaryUri(resource) ; } } private String getAsciiUri(URL resource) { Map args= new LinkedHashMap<>(); if ( filePointer!=null ) { switch (filePointer.getOffsetUnits()) { case LINES: args.put("skipLines",String.valueOf(filePointer.getOffset())); break; case BYTES: int offsetBytes=filePointer.getOffset(); offsetBytes= (int)(offsetBytes * 0.98); // fudge factor, because of newlines? args.put("skipBytes",String.valueOf(offsetBytes)); break; default: throw new IllegalArgumentException("unsupported file pointer"); } } args.put("column",String.valueOf(fieldNumber-1)); return "vap+txt:" + resource.toString() + "?" + URISplit.formatParams(args) ; } /** * return a several line description of the data. * @return a several line description of the data. */ private String getBinaryUri(URL resource) throws IllegalArgumentException { Map args= new LinkedHashMap<>(); if ( recordBytes>-1 ) { args.put( "recLength", String.valueOf(recordBytes) ); } else { args.put( "recLength", String.valueOf(rowPrefixBytes+rowBytes+rowSuffixBytes) ); } if ( Pds3DataSource.isTimeTag( dataType, unit) ) { args.put("type", "time"+bytes); } else if ( dataType.equals("ASCII_REAL") ) { args.put("type", "ascii"+bytes); } else if ( dataType.equals("ASCII_INTEGER") ) { args.put("type", "ascii"+bytes); } else if ( dataType.equals("PC_REAL") ) { args.put("type", "float"); args.put("byteOrder", "little"); } else if ( dataType.equals("SUN_REAL") || dataType.equals("IEEE_REAL") || dataType.equals("FLOAT") || dataType.equals("MAC_REAL") ) { args.put("type", "float"); args.put("byteOrder", "big"); } else if ( dataType.equals("LSB_UNSIGNED_INTEGER" ) ) { switch (itemBytes) { case 1: args.put("type", "ubyte"); break; case 2: args.put("type", "ushort"); break; case 4: args.put("type", "uint"); break; case 8: args.put("type", "ulong"); break; default: throw new IllegalArgumentException("PDS label has LSB_UNSIGNED_INTEGER with "+bytes+" bytes, must be 2, 4, or 8"); } args.put("byteOrder", "little"); } else if ( dataType.equals("LSB_INTEGER" ) ) { switch (itemBytes) { case 1: args.put("type", "byte"); break; case 2: args.put("type", "short"); break; case 4: args.put("type", "int"); break; case 8: args.put("type", "long"); break; default: throw new IllegalArgumentException("PDS label has LSB_INTEGER with "+bytes+" bytes, must be 2, 4, or 8"); } args.put("byteOrder", "little"); } else if ( dataType.equals("MSB_UNSIGNED_INTEGER" ) ) { switch (itemBytes) { case 1: args.put("type", "ubyte"); break; case 2: args.put("type", "ushort"); break; case 4: args.put("type", "uint"); break; case 8: args.put("type", "ulong"); break; default: throw new IllegalArgumentException("PDS label has MSB_UNSIGNED_INTEGER with "+bytes+" bytes, must be 2, 4, or 8"); } args.put("byteOrder", "big"); } else if ( dataType.equals("MSB_INTEGER") || dataType.equals("INTEGER") ) { // section 3.2 says INTEGER is the same as switch (itemBytes) { case 1: args.put("type", "byte"); break; case 2: args.put("type", "short"); break; case 4: args.put("type", "int"); break; case 8: args.put("type", "long"); break; default: throw new IllegalArgumentException("PDS label has MSB_INTEGER with "+bytes+" bytes, must be 2, 4, or 8"); } args.put("byteOrder", "big"); } else if ( dataType.equals("UNSIGNED_INTEGER") && bytes==1 ) { args.put("type","ubyte"); } else if ( dataType.equals("LSB_BIT_STRING" ) ) { args.put("type","ubyte"); args.put("dims","["+bytes+"]"); } else if ( dataType.equals("CHARACTER" ) ) { args.put("type","ascii"+bytes); unit= "nominal"; } else if ( dataType.equals("BIT_STRING" ) ) { args.put("type","ubyte"); args.put("dims","["+bytes+"]"); } else { throw new IllegalArgumentException("unsupported type:" +dataType); } if ( this.filePointer!=null ) { switch (this.filePointer.getOffsetUnits()) { case LINES: args.put("byteOffset", String.valueOf(this.filePointer.getOffset()*this.rowBytes) ); break; case BYTES: args.put("byteOffset", String.valueOf(this.filePointer.getOffset()) ); break; default: throw new IllegalArgumentException("Hmmm, uncoded case. Contact Jeremy Faden."); } } args.put( "recOffset", String.valueOf(startByte-1+rowPrefixBytes)); if ( missingConstant!=Double.NaN ) args.put( "fillValue", String.valueOf(missingConstant)); if ( validMaximum!=Double.POSITIVE_INFINITY ) args.put( "validMax", String.valueOf(validMaximum) ); if ( validMinimum!=Double.NEGATIVE_INFINITY ) args.put( "validMin", String.valueOf(validMinimum) ); if ( unit.trim().length()!=0 && !args.get("type").startsWith("time") ) args.put( "units", unit ); if ( items>-1 ) { args.put( "dims", "["+items+"]"); } else if ( dims!=null ) { args.put( "dims", dims); } return "vap+bin:" + resource.toString() + "?" + URISplit.formatParams(args) ; } /** * return a several line description of the data. * @return a several line description of the data. */ public String getDescription() { return this.description; } private Map getMetadata( JSONObject jo ) { Map result= new LinkedHashMap<>(); Iterator it= jo.keys(); while( it.hasNext() ) { String k= (String)it.next(); try { Object v= jo.get(k); if ( v instanceof JSONObject ) { result.put( k, getMetadata((JSONObject)v) ); } else { result.put( k, v ); } } catch (JSONException ex) { } } return result; } public Map getMetadata() { Map result= getMetadata(columnJSONObject); result.put("_table", getMetadata(tableJSONObject)); result.put("_label", getMetadata(labelJSONObject)); return result; } public FilePointer getFilePointer() { return filePointer; } /** * the offset into the file, where 1 is the beginning. * @param fileOffset */ public void setFilePointer( FilePointer p ) { this.filePointer = p; } }