package org.autoplot.netCDF; import java.text.ParseException; import java.util.logging.Logger; import org.das2.datum.Units; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import org.das2.datum.LoggerManager; import org.das2.datum.TimeParser; import org.das2.datum.UnitsConverter; import org.das2.util.monitor.NullProgressMonitor; import org.das2.util.monitor.ProgressMonitor; import org.das2.qds.AbstractDataSet; import org.das2.qds.DataSetOps; import org.das2.qds.DataSetUtil; import org.das2.qds.QDataSet; import org.autoplot.datasource.MetadataModel; import org.das2.qds.ops.Ops; import org.autoplot.metatree.IstpMetadataModel; import org.autoplot.metatree.MetadataUtil; import ucar.ma2.DataType; import ucar.ma2.InvalidRangeException; import ucar.ma2.Range; import ucar.nc2.Variable; import ucar.nc2.Attribute; import ucar.nc2.dataset.NetcdfDataset; /** * wraps a netCDF variable (or HDF5 variable) to present it as a QDataSet. * * @author jbf */ public class NetCdfVarDataSet extends AbstractDataSet { Variable v; double[] data; int[] shape; private static final Logger logger= LoggerManager.getLogger("apdss.netcdf"); public static NetCdfVarDataSet create( Variable variable, String constraint, NetcdfDataset ncfile, ProgressMonitor mon ) throws IOException { NetCdfVarDataSet result = new NetCdfVarDataSet( ); result.read(variable, ncfile, constraint, null, false, mon ); return result; } private NetCdfVarDataSet( ) { putProperty(QDataSet.QUBE, Boolean.TRUE); } private static String sliceConstraints( String constraints, int i ) { if ( constraints==null ) { return null; } else { if ( constraints.startsWith("[") && constraints.endsWith("]") ) { constraints= constraints.substring(1,constraints.length()-1); } String[] cc= constraints.split(","); if ( i>=cc.length ) { return null; } else if ( cc[i].equals(":") ) { return null; } else { return cc[i]; // TODO: this doesn't address depend variable that is rank 2, but this is not supported in NetCDF anyway. } } } /** * returns [ start, stop, stride ] or [ start, -1, -1 ] for slice. This is * provided to reduce code and for uniform behavior. * * See CdfJavaDataSource, which is where this was copied from. * @param constraint, such as "[0:100:2]" for even records between 0 and 100, non-inclusive. * @param recCount the number of records for when negative indeces are used. * @return [ start, stop, stride ] or [ start, -1, -1 ] for slice. * @throws java.text.ParseException */ public static long[] parseConstraint(String constraint, long recCount) throws ParseException { long[] result = new long[]{0, recCount, 1}; if (constraint == null) { return result; } else { if ( constraint.startsWith("[") && constraint.endsWith("]") ) { constraint= constraint.substring(1,constraint.length()-1); } try { String[] ss= constraint.split(":",-2); if ( ss.length>0 && ss[0].length()>0 ) { result[0]= Integer.parseInt(ss[0]); if ( result[0]<0 ) result[0]= recCount+result[0]; } if ( ss.length>1 && ss[1].length()>0 ) { result[1]= Integer.parseInt(ss[1]); if ( result[1]<0 ) result[1]= recCount+result[1]; } if ( ss.length>2 && ss[2].length()>0 ) { result[2]= Integer.parseInt(ss[2]); } if ( ss.length==1 ) { // slice result[1]= -1; result[2]= -1; } } catch ( NumberFormatException ex ) { throw new ParseException("expected integer: "+ex.toString(),0); } return result; } } private int sliceCount( boolean [] slice, int idim ) { int result= 0; for ( int i=0; i-1 ) return null; // we found a non-digit preceeding a digit, so this isn't a block of digits like expected. } } switch (digitCount) { case 16: tp= TimeParser.create("$Y$j$H$M$S$(subsec,places=3)"); break; case 17: tp= TimeParser.create("$Y$m$d$H$M$S$(subsec,places=3)"); break; default: } return tp; } /** * Read the NetCDF data. * @param variable the NetCDF variable. * @param ncfile the NetCDF file. * @param constraints null, or string like "[0:10]" Note it's allowed for the constraint to not have [] because this is called recursively. * @param mm if non-null, a metadata model, like IstpMetadataModel, is asserted. If null, then any variable containing DEPEND_0 implies IstpMetadataModel. * @param mon * @throws IOException */ private void read( Variable variable, NetcdfDataset ncfile, String constraints, MetadataModel mm, boolean isDepend, ProgressMonitor mon) throws IOException { this.v= variable; if ( !mon.isStarted() ) mon.started(); //das2 bug: monitor blinks if we call started again here mon.setProgressMessage( "reading "+v.getNameAndDimensions() ); if ( mm==null ) { long t0= System.currentTimeMillis(); List vvs=ncfile.getVariables(); for ( Variable vv: vvs ) { if ( vv.findAttribute("DEPEND_0" )!=null ) { mm= new IstpMetadataModel(); } } logger.log(Level.FINER, "look for DEPEND_0 (ms):{0}", (System.currentTimeMillis()-t0)); } logger.finer("v.getShape()"); shape= v.getShape(); boolean[] slice= new boolean[shape.length]; ucar.ma2.Array a; if ( constraints!=null ) { if ( constraints.startsWith("[") && constraints.endsWith("]") ) { constraints= constraints.substring(1,constraints.length()-1); } try { String[] cc= constraints.split(","); List ranges= new ArrayList( v.getRanges() ); for ( int i=0; i1 ) properties.put( QDataSet.QUBE, Boolean.TRUE ); if ( v.getParentStructure()!=null ) { //TODO: this is probably wrong for structure of rank 2 data. shape= new int[] { data.length }; slice= new boolean[shape.length]; } boolean isCoordinateVariable= false; for ( int ir=0; ir attributes= new HashMap(); mon.setProgressMessage("reading attributes"); logger.finer("v.getAttributes()"); List attrs= v.getAttributes(); for ( Iterator i= attrs.iterator(); i.hasNext(); ) { Attribute attr= (Attribute) i.next(); if ( !attr.isArray() ) { if ( attr.isString() ) { attributes.put( attr.getName(), attr.getStringValue() ); } else { attributes.put( attr.getName(), String.valueOf( attr.getNumericValue() ) ); } } } if ( attributes.containsKey("units") ) { String unitsString= (String)attributes.get("units"); if ( unitsString.contains(" since ") ) { Units u; try { u = Units.lookupTimeUnits(unitsString); } catch (ParseException ex) { throw new RuntimeException(ex); } properties.put( QDataSet.UNITS, u ); properties.put( QDataSet.MONOTONIC, Boolean.TRUE ); } else { properties.put( QDataSet.UNITS, Units.lookupUnits(unitsString) ); } } // GEOS files have a number of standard-looking metadata attributes, however I can't identify the name of the standard. // See https://satdat.ngdc.noaa.gov/sem/goes/data/avg/2010/05/goes13/netcdf/g13_magneto_1m_20100501_20100531.nc?BX_1&x=time_tag Object o; o= attributes.get("description"); if ( o!=null && o instanceof String ) { properties.put( QDataSet.DESCRIPTION, (String)o ); } o= attributes.get("long_label"); if ( o!=null && o instanceof String ) { properties.put( QDataSet.TITLE, (String)o ); } o= attributes.get("short_label"); if ( o!=null && o instanceof String ) { properties.put( QDataSet.LABEL, (String)o ); } o= attributes.get("lin_log"); if ( o!=null && ( o.equals("lin") || o.equals("log") ) ) { properties.put( QDataSet.SCALE_TYPE, o.equals("lin") ? "linear" : (String)o ); } o= attributes.get("nominal_min"); if ( o!=null && o instanceof String ) { properties.put( QDataSet.TYPICAL_MIN, Double.parseDouble( (String)o ) ); } o= attributes.get("nominal_max"); if ( o!=null && o instanceof String ) { properties.put( QDataSet.TYPICAL_MAX, Double.parseDouble( (String)o ) ); } o= attributes.get("format"); if ( o!=null && o instanceof String ) { properties.put( QDataSet.FORMAT, MetadataUtil.normalizeFormatSpecifier( (String)o ) ); } if ( data==null ) { if ( cdata==null ) { throw new RuntimeException("Either data or cdata should be defined at this point"); } //20110101T00:00 is 14 chars long. "2011-Jan-01T00:00:00.000000000 " is 35 chars long. (LANL has padding after the times to make it 35 characters long.) if ( shape.length==2 && shape[1]>=14 && shape[1]<=35 ) { // NASA/Goddard translation service formats Times as strings, check for this. logger.fine("parsing times formatted in char arrays"); data= new double[shape[0]]; String ss= new String(cdata); TimeParser tp= null; boolean tryGuessTimeParser= true; for ( int i=0; i istpProps= mm.properties(attributes); if ( properties.get( QDataSet.UNITS )==Units.us2000 ) { UnitsConverter uc= UnitsConverter.getConverter(Units.cdfEpoch, Units.us2000 ); if ( istpProps.containsKey(QDataSet.VALID_MIN) ) istpProps.put( QDataSet.VALID_MIN, uc.convert( (Number)istpProps.get(QDataSet.VALID_MIN ) ) ); if ( istpProps.containsKey(QDataSet.VALID_MAX) ) istpProps.put( QDataSet.VALID_MAX, uc.convert( (Number)istpProps.get(QDataSet.VALID_MAX ) ) ); if ( istpProps.containsKey(QDataSet.TYPICAL_MIN) ) istpProps.put( QDataSet.TYPICAL_MIN, uc.convert( (Number)istpProps.get(QDataSet.TYPICAL_MIN ) ) ); if ( istpProps.containsKey(QDataSet.TYPICAL_MAX) ) istpProps.put( QDataSet.TYPICAL_MAX, uc.convert( (Number)istpProps.get(QDataSet.TYPICAL_MAX ) ) ); istpProps.put(QDataSet.UNITS,Units.us2000); } if ( istpProps.containsKey(QDataSet.RENDER_TYPE) ) { String s= (String)istpProps.get(QDataSet.RENDER_TYPE); if ( s.equals("image") ) { logger.fine("removing DISPLAY_TYPE=image because it's incorrect"); istpProps.remove(QDataSet.RENDER_TYPE); } } properties.putAll(istpProps); for ( int ir=0; ir newShape= new ArrayList(shape.length); for ( int i=0; i=data.length) { throw new IllegalArgumentException("index out of bounds"); } return data[ index ]; } @Override public double value( int i, int j, int k ) { //int index= i + shape[0] * j + shape[0] * shape[1] * k; int index= k + shape[2] * j + shape[2] * shape[1] * i; if ( index>=data.length) { throw new IllegalArgumentException("index out of bounds"); } return data[index]; } @Override public double value( int i, int j, int k, int l ) { int index= l + shape[3] * k + shape[3] * shape[2] * j + shape[3] * shape[2] * shape[1] * i; if ( index>=data.length) { throw new IllegalArgumentException("index out of bounds"); } return data[index]; } @Override public int length() { return shape[0]; } @Override public int length( int dim ) { return shape[1]; } @Override public int length( int dim0, int dim1 ) { return shape[2]; } @Override public int length( int dim0, int dim1, int dim2 ) { return shape[3]; } @Override public QDataSet trim(int start, int end) { return super.trim(start, end); // TODO: introduce offset so we don't need to copy. } @Override public QDataSet slice(int i) { return super.slice(i); } }