/*
* CdfUtil.java
*
* Created on July 24, 2007, 12:56 PM
*/
package org.autoplot.cdf;
import gov.nasa.gsfc.spdf.cdfj.AttributeEntry;
import gov.nasa.gsfc.spdf.cdfj.CDFException;
import gov.nasa.gsfc.spdf.cdfj.CDFReader;
import java.util.logging.Level;
import org.das2.datum.DatumRange;
import org.das2.datum.EnumerationUnits;
import org.das2.datum.Units;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.das2.datum.InconvertibleUnitsException;
import org.das2.datum.UnitsConverter;
import org.das2.datum.UnitsUtil;
import org.das2.util.LoggerManager;
import org.das2.util.monitor.ProgressMonitor;
import org.das2.qds.buffer.BufferDataSet;
import org.das2.qds.ArrayDataSet;
import org.das2.qds.DDataSet;
import org.das2.qds.DataSetUtil;
import org.das2.qds.QDataSet;
import org.das2.qds.MutablePropertyDataSet;
import org.das2.qds.Slice0DataSet;
import org.autoplot.datasource.DataSourceUtil;
import org.autoplot.datasource.MetadataModel;
import org.autoplot.metatree.IstpMetadataModel;
import org.das2.qds.SemanticOps;
import org.das2.qds.ops.Ops;
import org.das2.util.monitor.NullProgressMonitor;
/**
* static methods supporting CdfFileDataSource
*
* @author jbf
*/
public class CdfUtil {
private final static Logger logger= LoggerManager.getLogger("apdss.cdf");
/**
* if "true", show empty records (true is default).
*/
public static final String OPTION_INCLUDE_EMPTY_RECORDS= "includeEmptyRecords";
/**
* if "true", then don't show the number of records.
*/
public static final String OPTION_IS_MASTER = "isMaster";
/**
* if "true" then return more detailed descriptions in HTML
*/
public static final String OPTION_DEEP = "deep";
/**
* if "true" return only the data variables, not the support data.
*/
public static final String OPTION_DATA_ONLY = "dataOnly";
/**
* if greater than -1, then only read variables up to this rank.
*/
public static final String OPTION_RANK_LIMIT = String.valueOf( QDataSet.MAX_RANK );
/**
* return the Java type used to store the CDF data type.
* @param type 45, 44, or 51
* @return String like double, float or string
*/
private static String getTargetType(int type) {
switch (type) {
case (int)CDFConstants.CDF_DOUBLE:
case (int)CDFConstants.CDF_REAL8:
case (int)CDFConstants.CDF_EPOCH:
return "double";
case (int)CDFConstants.CDF_EPOCH16:
return "double";
case (int)CDFConstants.CDF_FLOAT:
case (int)CDFConstants.CDF_REAL4:
return "float";
case (int)CDFConstants.CDF_UINT4:
return "double";
case (int)CDFConstants.CDF_INT8:
case (int)CDFConstants.CDF_TT2000:
return "long";
case (int)CDFConstants.CDF_INT4:
case (int)CDFConstants.CDF_UINT2:
return "int";
case (int)CDFConstants.CDF_INT2:
case (int)CDFConstants.CDF_UINT1:
return "short";
case (int)CDFConstants.CDF_INT1:
case (int)CDFConstants.CDF_BYTE:
return "byte";
case (int)CDFConstants.CDF_CHAR:
case (int)CDFConstants.CDF_UCHAR:
return "string";
default:
throw new IllegalArgumentException("unsupported type: "+type);
}
}
private static Object byteBufferType( int type ) {
switch (type) {
case (int)CDFConstants.CDF_DOUBLE:
case (int)CDFConstants.CDF_REAL8:
case (int)CDFConstants.CDF_EPOCH:
return BufferDataSet.DOUBLE;
case (int)CDFConstants.CDF_FLOAT:
case (int)CDFConstants.CDF_REAL4:
return BufferDataSet.FLOAT;
case (int)CDFConstants.CDF_UINT4:
return BufferDataSet.DOUBLE;
case (int)CDFConstants.CDF_INT8:
case (int)CDFConstants.CDF_TT2000:
return BufferDataSet.LONG;
case (int)CDFConstants.CDF_INT4:
case (int)CDFConstants.CDF_UINT2:
return BufferDataSet.INT;
case (int)CDFConstants.CDF_INT2:
case (int)CDFConstants.CDF_UINT1:
return BufferDataSet.SHORT;
case (int)CDFConstants.CDF_INT1:
case (int)CDFConstants.CDF_BYTE:
return BufferDataSet.BYTE;
case (int)CDFConstants.CDF_CHAR:
return BufferDataSet.BYTE; // determined experimentally: vap+cdfj:file:///home/jbf/ct/hudson/data.backup/cdf/ac_k0_mfi_20080602_v01.cdf?BGSEc
case (int)CDFConstants.CDF_UCHAR:
return BufferDataSet.BYTE; // TODO: I think...
case (int)CDFConstants.CDF_EPOCH16:
return BufferDataSet.DOUBLE;
default:
throw new IllegalArgumentException("unsupported type: "+type);
}
}
/**
* column major files require a transpose of each record. This makes a copy of the input, because I'm nervous
* that this might be backed by a writable cdf file.
* @param recLenBytes length of each record in bytes. (qube=2,3 bbType=float, then this is 2*4=8.)
* @param qube dimensions, a 0,1,..,4 element array.
* @param byteBuffer
* @param bbType
* @return the byte buffer.
*/
private static ByteBuffer transpose( int recLenBytes, int[] qube, ByteBuffer byteBuffer, Object bbType ) {
if ( qube.length<3 ) {
return byteBuffer;
}
ByteBuffer temp= ByteBuffer.allocate(recLenBytes);
ByteBuffer result= ByteBuffer.allocate(recLenBytes * qube[0]);
result.order(byteBuffer.order());
int fieldBytes= BufferDataSet.byteCount(bbType);
switch (qube.length) {
case 3:
{
int len1= qube[1];
int len2= qube[2];
for ( int i0=0; i0 props, MutablePropertyDataSet ds ) {
Units pu= (Units) props.get(QDataSet.UNITS);
Units u= (Units) ds.property( QDataSet.UNITS );
UnitsConverter uc;
if ( pu==null || u==null ) {
uc= UnitsConverter.IDENTITY;
} else if ( u==Units.cdfEpoch ) {
uc= UnitsConverter.IDENTITY;
} else if ( pu==Units.microseconds && u==Units.us2000 ) { // epoch16
uc= UnitsConverter.IDENTITY;
} else {
if ( pu==u ) {
uc= UnitsConverter.IDENTITY;
} else if ( UnitsUtil.isOrdinalMeasurement(u) || UnitsUtil.isOrdinalMeasurement(pu) ) {
return;
} else {
try {
uc= UnitsConverter.getConverter( pu, u );
} catch ( InconvertibleUnitsException ex ) { // PlasmaWave group Polar H7 files
uc= UnitsConverter.IDENTITY;
}
}
}
double dmin=Double.NEGATIVE_INFINITY;
double dmax=Double.POSITIVE_INFINITY;
if ( ds.rank()==1 && ds.length()>0 ) {
QDataSet range= Ops.extent(ds,null,null);
dmin= uc.convert(range.value(0));
dmax= uc.convert(range.value(1));
}
Number nmin= (Number)props.get(QDataSet.VALID_MIN);
double vmin= nmin==null ? Double.POSITIVE_INFINITY : nmin.doubleValue();
Number nmax= (Number)props.get(QDataSet.VALID_MAX);
double vmax= nmax==null ? Double.POSITIVE_INFINITY : nmax.doubleValue();
boolean intersects= false;
if ( dmax>vmin && dmin1e30 ) { //bugfix 3235447: all data invalid
if ( nmax!=null ) ds.putProperty(QDataSet.VALID_MAX, uc.convert(nmax) );
if ( nmin!=null ) ds.putProperty(QDataSet.VALID_MIN, uc.convert(nmin) );
}
String t= (String) props.get(QDataSet.SCALE_TYPE);
if ( t!=null ) ds.putProperty( QDataSet.SCALE_TYPE, t );
}
/**
* returns the size of the data type in bytes.
* @param itype type of data, such as CDFConstants.CDF_FLOAT
* @return the size the data atom in bytes
* TODO: this needs to be verified. Unsigned numbers may come back as next larger size.
*/
protected static int sizeOf( long itype ) {
int sizeBytes;
if ( itype==CDFConstants.CDF_EPOCH16 ) {
sizeBytes= 16;
} else if(itype == CDFConstants.CDF_DOUBLE || itype == CDFConstants.CDF_REAL8 || itype == CDFConstants.CDF_EPOCH || itype==CDFConstants.CDF_TT2000 || itype==CDFConstants.CDF_INT8 || itype==CDFConstants.CDF_UINT4 ) {
sizeBytes= 8;
} else if( itype == CDFConstants.CDF_FLOAT || itype == CDFConstants.CDF_REAL4 || itype==CDFConstants.CDF_INT4 || itype == CDFConstants.CDF_UINT2 ) {
sizeBytes=4; //sizeBytes= 4;
} else if( itype == CDFConstants.CDF_INT2 || itype == CDFConstants.CDF_UINT1 || itype==CDFConstants.CDF_UCHAR ) {
sizeBytes=2; //sizeBytes= 2;
} else if( itype == CDFConstants.CDF_INT1 || itype==CDFConstants.CDF_BYTE || itype==CDFConstants.CDF_CHAR ) {
sizeBytes=1; //sizeBytes= 1;
} else {
throw new IllegalArgumentException("didn't code for type");
}
return sizeBytes;
}
/**
* returns the size of the variable in bytes.
* @param dims number of dimensions in each record
* @param dimSizes dimensions of each record
* @param itype type of data, such as CDFConstants.CDF_FLOAT
* @param rc number of records (rec count)
* @return the size the variable in bytes
*/
private static long sizeOf( int dims, int[] dimSizes, long itype, long rc ) {
long size= dims==0 ? rc : rc * DataSetUtil.product( dimSizes );
size= size*sizeOf(itype);
return size;
}
/**
* returns effective rank. Nand's code looks for 1-element dimensions, which messes up Seth's file rbspb_pre_ect-mageisHIGH.
* See files:
* - vap+cdfj:ftp://cdaweb.gsfc.nasa.gov/pub/data/geotail/lep/2011/ge_k0_lep_20111016_v01.cdf?V0
*
- vap+cdfj:file:///home/jbf/ct/autoplot/data.backup/examples/cdf/seth/rbspb_pre_ect-mageisHIGH-sp-L1_20130709_v1.0.0.cdf?Histogram_prot
*
* @param varies array of boolean indicating if a dimension varies.
* @return the rank
*/
protected static int getEffectiveRank( boolean[] varies ) {
int rank = 0;
for (int i = 0; i < varies.length; i++) {
if (!varies[i]) continue;
rank++;
}
return rank;
}
/**
* implements slice1 by packing all the remaining elements towards the front and trimming.
* TODO: this needs to be verified. See https://sourceforge.net/p/autoplot/bugs/2639/
* @param buf the byte buffer, which can be read-only.
* @param varType the variable type, see sizeOf(varType)
* @param qube the dimensions of the unsliced dataset
* @param slice1 the index to slice
* @param rowMajority true if the buffer is row majority.
* @return a copy containing just the slice1 of the input buffer.
*/
private static ByteBuffer doSlice1( ByteBuffer buf, long varType, int[] qube, int slice1, boolean rowMajority ) {
int recSizeBytes= DataSetUtil.product(qube) / qube[0] * sizeOf(varType);
ByteBuffer result= ByteBuffer.allocate( recSizeBytes / qube[1] * qube[0] );
result.order(buf.order());
if ( rowMajority ) { // one of these two is wrong.
int p1= slice1 * recSizeBytes / qube[1];
int p2= ( slice1 * recSizeBytes / qube[1] + recSizeBytes / qube[1] );
for ( int irec=0; irec 3 ) {
if (recCount != -1) {
throw new IllegalArgumentException("rank 5 not implemented");
}
}
int varRecCount= cdf.getNumberOfValues(svariable);
if ( recCount==-1 && recStart>0 && varRecCount==1 ) { // another kludge for Rockets, where depend was assigned variance
recStart= 0;
}
if ( recCount>1 ) { // check for length limit
int bytesPerRecord= DataSetUtil.product(dimSizes) * sizeOf(varType);
int limit= (int)(Integer.MAX_VALUE)/1000; // KB
if ( limit<(recCount/1000/recInterval*bytesPerRecord) ) {
int newRecCount= (int)( limit * recInterval * 1000 / bytesPerRecord );
String suggest;
if ( recInterval>1 ) {
suggest= "[0:"+newRecCount+":"+recInterval+"]";
} else {
suggest= "[0:"+newRecCount+"]";
}
throw new IllegalArgumentException("data read would result in more than 2GB read, which is not yet supported. Use "+svariable+suggest+" to read first records.");
}
}
long rc= recCount;
if ( rc==-1 ) rc= 1; // -1 is used as a flag for a slice, we still really read one record.
logger.log( Level.FINEST, "size of {0}: {1}MB type: {2}", new Object[]{svariable, sizeOf(dims, dimSizes, varType, rc) / 1024. / 1024., varType});
String stype = getTargetType( cdf.getType(svariable) );
ByteBuffer buff;
long t0= System.currentTimeMillis();
logger.entering("gov.nasa.gsfc.spdf.cdfj.CDFReader", "getBuffer" );
if ( recInterval==1 ) {
try {
boolean preserve= true;
if ( stype.equals("string") ) {
buff= null;
} else {
buff= cdf.getBuffer(svariable, stype, new int[] { (int)recStart,(int)(recStart+recInterval*(rc-1)) }, preserve );
}
} catch ( CDFException ex ) {
buff= myGetBuffer(cdf, svariable, (int)recStart, (int)(recStart+rc*recInterval), (int)recInterval );
}
} else {
buff= myGetBuffer(cdf, svariable, (int)recStart, (int)(recStart+rc*recInterval), (int)recInterval );
}
logger.exiting("gov.nasa.gsfc.spdf.cdfj.CDFReader", "getBuffer" );
logger.log(Level.FINE, "read variable {0} in (ms): {1}", new Object[]{svariable, System.currentTimeMillis()-t0});
Object bbType= byteBufferType( cdf.getType(svariable) );
int recLenBytes= BufferDataSet.byteCount(bbType);
if ( dimSizes.length>0 ) recLenBytes= recLenBytes * DataSetUtil.product( dimSizes );
MutablePropertyDataSet result;
int[] qube;
qube= new int[ 1+dimSizes.length ];
for ( int i=0; i-1 && qube.length>1 ) {
buff= doSlice1( buff, varType, qube, slice1, cdf.rowMajority() );
if ( recCount==-1 ) {
// throw new IllegalArgumentException("recCount==-1 and slice1>-1 when loading "+svariable);
logger.log(Level.FINE, "recCount==-1 and slice1>-1 when loading {0}", svariable);
}
int[] nqube= new int[qube.length-1];
nqube[0]= qube[0];
for ( int i=2;i0 ) { // vap+cdfj:file:///home/jbf/ct/hudson/data.backup/cdf/c4_cp_fgm_spin_20030102_v01.cdf?B_vec_xyz_gse__C4_CP_FGM_SPIN
boolean reform= true;
for ( int i=1; i1 ) {
// result= new RepeatIndexDataSet( result, i+1, repeatDimensions[i] );
// }
// //}
// }
if ( varType == CDFConstants.CDF_CHAR || varType==CDFConstants.CDF_UCHAR ) {
throw new IllegalArgumentException("We shouldn't get here because stype=string");
} else if ( varType == CDFConstants.CDF_EPOCH ) {
result.putProperty(QDataSet.UNITS, Units.cdfEpoch);
result.putProperty(QDataSet.VALID_MIN, 1.); // kludge for Timas, which has zeros.
} else if ( varType==CDFConstants.CDF_EPOCH16 ) {
result.putProperty(QDataSet.UNITS, Units.cdfEpoch);
result.putProperty(QDataSet.VALID_MIN, 1.); // kludge for Timas, which has zeros.
DDataSet result1= DDataSet.createRank1(result.length());
for ( int i=0; i1 ) {
uri= uri + "["+recStart+":"+(recStart+recCount)+":"+recInterval+"]";
}
CdfDataSource.dsCachePut( uri, result );
}
}
return result;
}
private static MutablePropertyDataSet readStringData(String svariable, long recInterval, CDFReader cdf, long recCount, int[] qube ) throws ArrayIndexOutOfBoundsException, IllegalArgumentException, CDFException.ReaderError {
EnumerationUnits units = EnumerationUnits.create(svariable);
Object o;
if ( recInterval>1 ) throw new IllegalArgumentException("recInterval>1 not supported here");
o = cdf.get(svariable);
Object o0= Array.get(o,0);
String[] sdata;
if ( o0.getClass().isArray() ) {
sdata= new String[ Array.getLength(o0) ];
for ( int j=0; j0 ? ( dimVary[0]==true ? 0 : 1 ) : 0 ;
int lastVary=-1;
for ( int iv=dimVary.length-1; iv>=shift; iv-- ) {
if ( dimVary[iv] ) {
lastVary= iv;
break;
}
}
if ( lastVary>-1 ) {
if ( shift==0 ) {
int[] newDims= Arrays.copyOfRange( dims, 0, lastVary+1 );
return newDims;
} else {
int[] newDims= Arrays.copyOfRange( dims, 1, lastVary+1 );
return newDims;
}
} else {
return new int[0];
}
} else {
return dims;
}
}
/**
* factor out common code that gets the properties for each dimension.
* @param cdf
* @param var
* @param rank
* @param dims
* @param dim
* @param warn
* @return
*/
private static DepDesc getDepDesc( CDFReader cdf, String svar, int rank, int[] dims, int dim, List warn, boolean isMaster ) {
DepDesc result= new DepDesc();
result.nrec=-1;
try {
if ( hasAttribute( cdf, svar, "DEPEND_"+dim ) ) { // check for metadata for DEPEND_
Object att= getAttribute( cdf, svar, "DEPEND_"+dim );
if ( att!=null && rank>1 ) {
logger.log(Level.FINER, "get attribute DEPEND_"+dim+" entry for {0}", svar );
result.dep = String.valueOf(att);
if ( cdf.getDimensions( result.dep ).length>0 && ( isMaster || cdf.getNumberOfValues( result.dep )>1 ) && cdf.recordVariance( result.dep ) ) {
result.rank2= true;
result.nrec = cdf.getDimensions( result.dep )[0];
warn.add( "NOTE: " + result.dep + " is record varying" );
} else {
result.nrec = cdf.getNumberOfValues( result.dep );
if (result.nrec == 1) {
result.nrec = getDimensions( cdf, result.dep )[0];
}
}
if ( dims.length>(dim-1) && (result.nrec)!=dims[dim-1] ) {
warn.add("data dim "+dim+" length ("+dims[dim-1]+") is inconsistent with DEPEND_"+ dim +" length ("+result.nrec+")" );
}
}
}
} catch ( CDFException e) {
warn.add( "problem with DEPEND_"+dim+": " + e.getMessage() );//e.printStackTrace();
}
try {
if (result.nrec==-1 && hasAttribute( cdf, svar, "LABL_PTR_"+dim ) ) { // check for metadata for LABL_PTR_1
Object att= getAttribute( cdf, svar, "LABL_PTR_"+dim );
if ( att!=null && rank>1 ) {
logger.log(Level.FINER, "get attribute LABL_PTR_"+dim+" entry for {0}", svar );
result.labl = String.valueOf(att);
if ( !cdf.existsVariable(result.labl) ) throw new Exception("No such variable: "+String.valueOf(att));
result.nrec = cdf.getNumberOfValues( result.labl );
if (result.nrec == 1) {
result.nrec = cdf.getDimensions(svar)[0];
}
if ( dim==1 && dims.length>(dim-1) && (result.nrec)!=dims[dim-1] ) {
warn.add("data dim "+dim+" length ("+dims[dim-1]+") is inconsistent with LABL_PTR_"+dim+" length ("+result.nrec+")" );
}
}
} else if ( hasAttribute( cdf, svar, "LABL_PTR_"+dim ) ) { // check that the LABL_PTR_i is the right length as well.
Object att= getAttribute( cdf, svar, "LABL_PTR_"+dim );
if ( att!=null && rank>1 ) {
logger.log(Level.FINER, "get attribute LABL_PTR_"+dim+" entry for {0}", svar );
result.labl= String.valueOf(att);
int nrec = cdf.getNumberOfValues(result.labl);
if ( nrec == 1 ) {
nrec = cdf.getDimensions(result.labl)[0];
}
if ( dim==1 && dims.length>(dim-1) && (nrec)!=dims[dim-1] ) {
warn.add("data dim "+dim+" length ("+dims[dim-1]+") is inconsistent with LABL_PTR_"+dim+" length ("+nrec+")" );
}
}
}
} catch (CDFException e) {
warn.add( "problem with LABL_PTR_"+dim+": " + e.getMessage() );//e.printStackTrace();
} catch (Exception e) {
warn.add( "problem with LABL_PTR_"+dim+": " + e.getMessage() );//e.printStackTrace();
}
return result;
}
private static boolean hasVariable( CDFReader cdf, String var ) {
List names= Arrays.asList( cdf.getVariableNames() );
return names.contains(var);
}
/**
* Return a map where keys are the names of the variables, and values are descriptions.
* @param cdf the cdf reader reference.
* @param dataOnly show only the DATA and not SUPPORT_DATA. Note I reclaimed this parameter because I wasn't using it.
* @param rankLimit show only variables with no more than this rank.
* @return map of parameter name to short description
* @throws Exception
*/
public static Map getPlottable(CDFReader cdf, boolean dataOnly, int rankLimit) throws Exception {
return getPlottable(cdf, dataOnly, rankLimit, new HashMap() );
}
/**
* information about a variable. This might be used for CDF and HDF as well, so this code shouldn't
* get too married to CDF. TODO: Why not just use QDataSet bundle descriptor?
*/
public static class CdfVariableDescription {
public String name;
public String description;
public String htmlDescription;
public String variableType;
public boolean isSupport;
public long numberOfRecords;
/**
* null or the name of the DEPEND_0
*/
public String depend0Name;
public int dimensions[];
public String[] depends;
}
/**
* retrieve information about all the variables. A LinkedHashMap will be returned
* to preserve the order.
* @param cdf
* @param options map of options like OPTION_DEEP and OPTION_DATA_ONLY
* @return
* @throws gov.nasa.gsfc.spdf.cdfj.CDFException.ReaderError
*/
public static LinkedHashMap getPlottable( CDFReader cdf, Map options ) throws CDFException.ReaderError {
LinkedHashMap result = new LinkedHashMap<>();
if ( options==null ) options= Collections.emptyMap();
boolean isMaster= getOption( options, OPTION_IS_MASTER, "false" ).equals("true");
boolean deep= getOption( options, OPTION_DEEP, "true" ).equals("true");
boolean showEmpty= getOption( options, OPTION_INCLUDE_EMPTY_RECORDS, "true" ).equals("true");
boolean dataOnly= getOption( options, OPTION_DATA_ONLY, "false" ).equals("true");
int rankLimit = Integer.parseInt( getOption( options, OPTION_RANK_LIMIT, String.valueOf(QDataSet.MAX_RANK) ) );
logger.fine("getting CDF variables");
String[] v = cdf.getVariableNames();
logger.log(Level.FINE, "got {0} variables", v.length);
logger.fine("getting CDF attributes");
boolean[] isData= new boolean[v.length];
int i=-1;
int skipCount=0;
for (String svar : v) {
i=i+1;
if ( dataOnly ) {
Object attr= getAttribute(cdf, svar, "VAR_TYPE" );
if ( attr==null ) {
for ( String s: cdf.variableAttributeNames(svar) ) {
if ( s.equalsIgnoreCase("VAR_TYPE") ) {
attr= getAttribute(cdf,svar,s);
}
}
if ( attr!=null ) {
logger.log(Level.INFO, "Wrong-case VAR_TYPE attribute found, should be \"VAR_TYPE\"");
}
}
if ( attr!=null && "data".equalsIgnoreCase(attr.toString()) ) {
if ( !attr.equals("data") ) {
logger.log(Level.INFO, "var_type is case-sensitive, should be \"data\", not {0}", attr);
attr= "data";
}
}
if ( attr==null || !attr.equals("data") ) {
skipCount++;
isData[i]= false;
} else {
isData[i]= true;
}
}
}
//if ( skipCount==v.length ) {
// logger.fine( "turning off dataOnly because it rejects everything");
// dataOnly= false;
//}
i=-1;
for (String v1 : v) {
i=i+1;
String svar=null;
List warn= new ArrayList();
String xDependVariable=null;
boolean isVirtual= false;
long xMaxRec = -1;
long maxRec= -1;
long recCount= -1;
String scatDesc = null;
String svarNotes = null;
StringBuilder vdescr=null;
int rank=-1;
int[] dims=new int[0];
int varType=0;
try {
svar = v1;
try {
varType= cdf.getType(svar);
} catch ( CDFException ex ) {
throw new RuntimeException(ex);
}
// reject variables that are ordinal data that do not have DEPEND_0.
boolean hasDep0= hasAttribute( cdf, svar, "DEPEND_0" );
if ( ( varType==CDFConstants.CDF_CHAR || varType==CDFConstants.CDF_UCHAR ) && ( !hasDep0 ) ) {
logger.log(Level.FINER, "skipping because ordinal and no depend_0: {0}", svar );
continue;
}
maxRec = cdf.getNumberOfValues(svar);
recCount= maxRec;
if ( recCount==0 && !showEmpty ) {
logger.log(Level.FINER, "skipping because variable is empty: {0}", svar );
continue;
}
dims = getDimensions(cdf, svar);
if (dims == null) {
rank = 1;
} else {
rank = dims.length + 1;
}
if (rank > rankLimit) {
continue;
}
if ( svar.equals("Time_PB5") ) {
logger.log(Level.FINER, "skipping {0} because we always skip Time_PB5", svar );
continue;
}
if ( dataOnly ) {
if ( !isData[i] ) continue;
}
Object att= getAttribute( cdf, svar, "VIRTUAL" );
if ( att!=null ) {
logger.log(Level.FINER, "get attribute VIRTUAL entry for {0}", svar );
if ( String.valueOf(att).toUpperCase().equals("TRUE") ) {
String funct= (String)getAttribute( cdf, svar, "FUNCTION" );
if ( funct==null ) funct= (String) getAttribute( cdf, svar, "FUNCT" ) ; // in alternate_view in IDL: 11/5/04 - TJK - had to change FUNCTION to FUNCT for IDL6.* compatibili
if ( !CdfVirtualVars.isSupported(funct) ) {
if ( !funct.startsWith("comp_themis") ) {
logger.log(Level.FINER, "virtual function not supported: {0}", funct);
}
continue;
} else {
vdescr= new StringBuilder(funct);
vdescr.append( "( " );
int icomp=0;
String comp= (String)getAttribute( cdf, svar, "COMPONENT_"+icomp );
if ( comp!=null ) {
vdescr.append( comp );
icomp++;
}
for ( ; icomp<5; icomp++ ) {
comp= (String)getAttribute( cdf, svar, "COMPONENT_"+icomp );
if ( comp!=null ) {
vdescr.append(", ").append(comp);
} else {
break;
}
}
vdescr.append(" )");
}
isVirtual= true;
}
}
}catch (CDFException | RuntimeException e) {
logger.fine(e.getMessage());
}
try {
if ( hasAttribute( cdf, svar, "DEPEND_0" )) { // check for metadata for DEPEND_0
Object att= getAttribute( cdf, svar, "DEPEND_0" );
if ( att!=null ) {
logger.log(Level.FINER, "get attribute DEPEND_0 entry for {0}", svar);
xDependVariable = String.valueOf(att);
if ( !hasVariable(cdf,xDependVariable ) ) throw new Exception("No such variable: "+String.valueOf(att));
xMaxRec = cdf.getNumberOfValues( xDependVariable );
if ( xMaxRec!=maxRec && vdescr==null && cdf.recordVariance(svar) ) {
if ( maxRec==-1 ) maxRec+=1; //why?
if ( maxRec==0 ) {
warn.add("data contains no records" );
} else {
warn.add("depend0 length ("+xDependVariable+"["+xMaxRec+"]) is inconsistent with length ("+(maxRec)+")" );
}
//TODO: warnings are incorrect for Themis data.
}
} else {
if ( dataOnly ) {
continue; // vap+cdaweb:ds=GE_K0_PWI&id=freq_b&timerange=2012-06-12
}
}
}
} catch (CDFException e) {
warn.add( "problem with DEPEND_0: " + e.getMessage() );
} catch (Exception e) {
warn.add( "problem with DEPEND_0: " + e.getMessage() );
}
CdfVariableDescription description= new CdfVariableDescription();
//TODO: this could probably recurse into this routine now.
DepDesc dep1desc= getDepDesc( cdf, svar, rank, dims, 1, warn, isMaster );
DepDesc dep2desc= getDepDesc( cdf, svar, rank, dims, 2, warn, isMaster );
DepDesc dep3desc= getDepDesc( cdf, svar, rank, dims, 3, warn, isMaster );
if (deep) {
Object o= (Object) getAttribute( cdf, svar, "CATDESC" );
if ( o != null && o instanceof String ) {
logger.log(Level.FINER, "get attribute CATDESC entry for {0}", svar );
scatDesc = (String)o ;
}
o= getAttribute( cdf, svar, "VAR_NOTES" );
if ( o!=null && o instanceof String ) {
logger.log(Level.FINER, "get attribute VAR_NOTES entry for {0}", svar );
svarNotes = (String)o ;
}
}
String htmlDescription = svar;
if (xDependVariable != null) {
htmlDescription += "[" + maybeShorten( svar, xDependVariable );
if ( ( xMaxRec>0 || !isMaster ) && xMaxRec==maxRec ) { // small kludge for CDAWeb, where we expect masters to be empty.
htmlDescription+= "=" + (xMaxRec);
}
if ( dep1desc.dep != null) {
htmlDescription += "," + maybeShorten( svar, dep1desc.dep ) + "=" + dims[0] + ( dep1desc.rank2 ? "*": "" );
if ( dep2desc.dep != null) {
htmlDescription += "," + maybeShorten( svar, dep2desc.dep ) + "=" + dims[1] + ( dep2desc.rank2 ? "*": "" );
if (dep3desc.dep != null) {
htmlDescription += "," + maybeShorten( svar, dep3desc.dep ) + "=" + dims[2] + ( dep3desc.rank2 ? "*": "" );
}
}
} else if ( rank>1 ) {
htmlDescription += ","+DataSourceUtil.strjoin( dims, ",");
}
htmlDescription += "]";
}
if (deep) {
StringBuilder descbuf = new StringBuilder("" + htmlDescription + "
");
int itype= -1;
try {
//assert svar is valid.
itype= cdf.getType(svar);
} catch ( CDFException ex ) {}
String recDesc= ""+ CdfUtil.getStringDataType( itype );
if ( dims!=null && dims.length>0 ) {
recDesc= recDesc+"["+ DataSourceUtil.strjoin( dims, ",") + "]";
}
if (scatDesc != null) {
descbuf.append(scatDesc).append("
");
}
if (svarNotes !=null ) {
descbuf.append("").append(svarNotes).append("
");
}
Vector variablePurpose= cdf.getAttributeEntries(svar,"VARIABLE_PURPOSE");
if ( variablePurpose.size()>0 ) {
AttributeEntry e= (AttributeEntry)variablePurpose.get(0);
StringBuilder s= new StringBuilder( String.valueOf(e.getValue()) );
for ( int i1=1; i1VARIABLE_PURPOSE: ").append(s).append("
");
}
if (maxRec != xMaxRec) {
if ( isVirtual ) {
descbuf.append("(virtual function ").append(vdescr).append( ")
");
} else {
if ( isMaster ) {
descbuf.append("records of ").append(recDesc).append("
");
} else {
descbuf.append( recCount ).append(" records of ").append(recDesc).append("
");
}
}
} else {
if ( isMaster ) {
descbuf.append("records of ").append(recDesc).append("
");
} else {
descbuf.append( recCount ).append(" records of ").append(recDesc).append("
");
}
}
for ( String s: warn ) {
descbuf.append("
");
if ( s.startsWith("NOTE") ) {
descbuf.append(s);
} else {
descbuf.append("WARNING: ").append(s);
}
}
descbuf.append("");
htmlDescription=descbuf.toString();
}
if ( svarNotes==null ) svarNotes="";
description.name= svar;
description.description= svarNotes;
description.isSupport= ! isData[i];
description.htmlDescription= htmlDescription;
description.variableType= getStringDataType( varType );
description.numberOfRecords= maxRec;
description.depends= new String[rank-1];
description.dimensions= dims;
description.depend0Name= xDependVariable; // might be null;
String desc = svar;
if (xDependVariable != null) {
desc += "[" + maybeShorten( svar, xDependVariable );
if ( ( xMaxRec>0 || !isMaster ) && xMaxRec==maxRec ) { // small kludge for CDAWeb, where we expect masters to be empty.
desc+= "=" + (xMaxRec);
}
if ( dep1desc.dep != null) {
desc += "," + maybeShorten( svar, dep1desc.dep ) + "=" + dims[0] + ( dep1desc.rank2 ? "*": "" );
if ( dep2desc.dep != null) {
desc += "," + maybeShorten( svar, dep2desc.dep ) + "=" + dims[1] + ( dep2desc.rank2 ? "*": "" );
if (dep3desc.dep != null) {
desc += "," + maybeShorten( svar, dep3desc.dep ) + "=" + dims[2] + ( dep3desc.rank2 ? "*": "" );
}
}
} else if ( rank>1 ) {
desc += ","+DataSourceUtil.strjoin( dims, ",");
}
desc += "]";
}
result.put(svar, description);
} // for each variable
logger.fine("done, get plottable ");
return result;
}
/**
* abbreviate names, motivated by Cluster CDF files which have
* Data__C1_CP_PEA_3DRH_cnts with DEPEND_0 of
* time_tags__C1_CP_PEA_3DRH_cnts.
* @param context
* @param name
* @return
*/
public static String maybeShorten( String context, String name ) {
int i1= context.length()-1;
int i2= name.length()-1;
while( i1>0 && i2>0 && context.charAt(i1)==name.charAt(i2) ) {
i1=i1-1;
i2=i2-1;
}
i2++;
if ( i2<(name.length()-3) ) {
return name.substring(0,i2)+"...";
} else {
return name;
}
}
private static String getOption( Map options, String key, String deft ) {
if ( options.containsKey(key) ) {
return options.get(key);
} else {
return deft;
}
}
/**
* Return a map where keys are the names of the variables, and values are descriptions. This
* allows for a deeper query, getting detailed descriptions within the values, and also supports the
* mode where the master CDFs (used by the CDAWeb plugin) don't contain data and record counts should
* not be supported.
* @param cdf
* @param dataOnly show only the DATA and not SUPPORT_DATA. Note I reclaimed this parameter because I wasn't using it.
* @param rankLimit show only variables with no more than this rank.
* @param options see constants for parameter names.
* @return map of parameter name to short description
* @throws Exception
*/
public static Map getPlottable(CDFReader cdf, boolean dataOnly, int rankLimit, Map options) throws Exception {
Map result = new LinkedHashMap<>();
Map dependent= new LinkedHashMap<>();
boolean isMaster= getOption( options, OPTION_IS_MASTER, "false" ).equals("true");
boolean deep= getOption( options, OPTION_DEEP, "false" ).equals("true");
boolean showEmpty= getOption( options, OPTION_INCLUDE_EMPTY_RECORDS, "true" ).equals("true");
logger.fine("getting CDF variables");
String[] v = cdf.getVariableNames();
logger.log(Level.FINE, "got {0} variables", v.length);
logger.fine("getting CDF attributes");
boolean[] isData= new boolean[v.length];
int i=-1;
int skipCount=0;
for (String svar : v) {
i=i+1;
if ( dataOnly ) {
Object attr= getAttribute(cdf, svar, "VAR_TYPE" );
if ( attr==null ) {
for ( String s: cdf.variableAttributeNames(svar) ) {
if ( s.equalsIgnoreCase("VAR_TYPE") ) {
attr= getAttribute(cdf,svar,s);
}
}
if ( attr!=null ) {
logger.log(Level.INFO, "Wrong-case VAR_TYPE attribute found, should be \"VAR_TYPE\"");
}
}
if ( attr!=null && "data".equalsIgnoreCase(attr.toString()) ) {
if ( !attr.equals("data") ) {
logger.log(Level.INFO, "var_type is case-sensitive, should be \"data\", not {0}", attr);
attr= "data";
}
}
if ( attr==null || !attr.equals("data") ) {
skipCount++;
isData[i]= false;
} else {
isData[i]= true;
}
}
}
//if ( skipCount==v.length ) {
// logger.fine( "turning off dataOnly because it rejects everything");
// dataOnly= false;
//}
i=-1;
for (String v1 : v) {
i=i+1;
String svar=null;
List warn= new ArrayList();
String xDependVariable=null;
boolean isVirtual= false;
long xMaxRec = -1;
long maxRec= -1;
long recCount= -1;
String scatDesc = null;
String svarNotes = null;
StringBuilder vdescr=null;
int rank=-1;
int[] dims=new int[0];
int varType=0;
try {
svar = v1;
try {
varType= cdf.getType(svar);
} catch ( CDFException ex ) {
throw new RuntimeException(ex);
}
// reject variables that are ordinal data that do not have DEPEND_0.
boolean hasDep0= hasAttribute( cdf, svar, "DEPEND_0" );
if ( ( varType==CDFConstants.CDF_CHAR || varType==CDFConstants.CDF_UCHAR ) && ( !hasDep0 ) ) {
logger.log(Level.FINER, "skipping because ordinal and no depend_0: {0}", svar );
continue;
}
maxRec = cdf.getNumberOfValues(svar);
recCount= maxRec;
if ( recCount==0 && !showEmpty ) {
logger.log(Level.FINER, "skipping because variable is empty: {0}", svar );
continue;
}
dims = getDimensions(cdf, svar);
rank = dims.length + 1;
if (rank > rankLimit) {
continue;
}
if ( svar.equals("Time_PB5") ) {
logger.log(Level.FINER, "skipping {0} because we always skip Time_PB5", svar );
continue;
}
if ( dataOnly ) {
if ( !isData[i] ) continue;
}
Object att= getAttribute( cdf, svar, "VIRTUAL" );
if ( att!=null ) {
logger.log(Level.FINER, "get attribute VIRTUAL entry for {0}", svar );
if ( String.valueOf(att).toUpperCase().equals("TRUE") ) {
String funct= (String)getAttribute( cdf, svar, "FUNCTION" );
if ( funct==null ) funct= (String) getAttribute( cdf, svar, "FUNCT" ) ; // in alternate_view in IDL: 11/5/04 - TJK - had to change FUNCTION to FUNCT for IDL6.* compatibili
if ( !CdfVirtualVars.isSupported(funct) ) {
if ( !funct.startsWith("comp_themis") ) {
logger.log(Level.FINER, "virtual function not supported: {0}", funct);
}
continue;
} else {
vdescr= new StringBuilder(funct);
vdescr.append( "( " );
int icomp=0;
String comp= (String)getAttribute( cdf, svar, "COMPONENT_"+icomp );
if ( comp!=null ) {
vdescr.append( comp );
icomp++;
}
for ( ; icomp<5; icomp++ ) {
comp= (String)getAttribute( cdf, svar, "COMPONENT_"+icomp );
if ( comp!=null ) {
vdescr.append(", ").append(comp);
} else {
break;
}
}
vdescr.append(" )");
}
isVirtual= true;
}
}
}catch (CDFException | RuntimeException e) {
logger.fine(e.getMessage());
}
try {
if ( hasAttribute( cdf, svar, "DEPEND_0" )) { // check for metadata for DEPEND_0
Object att= getAttribute( cdf, svar, "DEPEND_0" );
if ( att!=null ) {
logger.log(Level.FINER, "get attribute DEPEND_0 entry for {0}", svar);
xDependVariable = String.valueOf(att);
if ( !hasVariable(cdf,xDependVariable ) ) throw new Exception("No such variable: "+String.valueOf(att));
xMaxRec = cdf.getNumberOfValues( xDependVariable );
if ( xMaxRec!=maxRec && vdescr==null && cdf.recordVariance(svar) ) {
if ( maxRec==-1 ) maxRec+=1; //why?
if ( maxRec==0 ) {
warn.add("data contains no records" );
} else {
warn.add("depend0 length ("+xDependVariable+"["+xMaxRec+"]) is inconsistent with length ("+(maxRec)+")" );
}
//TODO: warnings are incorrect for Themis data.
}
} else {
if ( dataOnly ) {
continue; // vap+cdaweb:ds=GE_K0_PWI&id=freq_b&timerange=2012-06-12
}
}
}
} catch (CDFException e) {
warn.add( "problem with DEPEND_0: " + e.getMessage() );
} catch (Exception e) {
warn.add( "problem with DEPEND_0: " + e.getMessage() );
}
DepDesc dep1desc= getDepDesc( cdf, svar, rank, dims, 1, warn, isMaster );
DepDesc dep2desc= getDepDesc( cdf, svar, rank, dims, 2, warn, isMaster );
DepDesc dep3desc= getDepDesc( cdf, svar, rank, dims, 3, warn, isMaster );
if (deep) {
Object o= (Object) getAttribute( cdf, svar, "CATDESC" );
if ( o != null && o instanceof String ) {
logger.log(Level.FINER, "get attribute CATDESC entry for {0}", svar );
scatDesc = (String)o ;
}
o= getAttribute( cdf, svar, "VAR_NOTES" );
if ( o!=null && o instanceof String ) {
logger.log(Level.FINER, "get attribute VAR_NOTES entry for {0}", svar );
svarNotes = (String)o ;
}
}
String desc = svar;
if (xDependVariable != null) {
desc += "[" + maybeShorten( svar, xDependVariable );
if ( ( xMaxRec>0 || !isMaster ) && xMaxRec==maxRec ) { // small kludge for CDAWeb, where we expect masters to be empty.
desc+= "=" + (xMaxRec);
}
if ( dep1desc.dep != null) {
desc += "," + maybeShorten( svar, dep1desc.dep ) + "=" + dims[0] + ( dep1desc.rank2 ? "*": "" );
if ( dep2desc.dep != null) {
desc += "," + maybeShorten( svar, dep2desc.dep ) + "=" + dims[1] + ( dep2desc.rank2 ? "*": "" );
if (dep3desc.dep != null) {
desc += "," + maybeShorten( svar, dep3desc.dep ) + "=" + dims[2] + ( dep3desc.rank2 ? "*": "" );
} else if ( rank>3 ) {
desc += "," + DataSourceUtil.strjoin( Arrays.copyOfRange(dims,2,dims.length),"," );
}
} else if ( rank>2 ) {
desc += "," + DataSourceUtil.strjoin( Arrays.copyOfRange(dims,1,dims.length),"," );
}
} else if ( rank>1 ) {
desc += ","+DataSourceUtil.strjoin( dims, ",");
}
desc += "]";
}
if (deep) {
StringBuilder descbuf = new StringBuilder("" + desc + "
");
int itype= -1;
try {
//assert svar is valid.
itype= cdf.getType(svar);
} catch ( CDFException ex ) {}
String recDesc= ""+ CdfUtil.getStringDataType( itype );
if ( dims!=null && dims.length>0 ) {
recDesc= recDesc+"["+ DataSourceUtil.strjoin( dims, ",") + "]";
}
if (scatDesc != null) {
descbuf.append(scatDesc).append("
");
}
if (svarNotes !=null ) {
descbuf.append("").append(svarNotes).append("
");
}
Vector variablePurpose= cdf.getAttributeEntries(svar,"VARIABLE_PURPOSE");
if ( variablePurpose.size()>0 ) {
AttributeEntry e= (AttributeEntry)variablePurpose.get(0);
StringBuilder s= new StringBuilder( String.valueOf(e.getValue()) );
for ( int i1=1; i1VARIABLE_PURPOSE: ").append(s).append("
");
}
if (maxRec != xMaxRec) {
if ( isVirtual ) {
descbuf.append("(virtual function ").append(vdescr).append( ")
");
} else {
if ( isMaster ) {
descbuf.append("records of ").append(recDesc).append("
");
} else {
descbuf.append( recCount ).append(" records of ").append(recDesc).append("
");
}
}
} else {
if ( isMaster ) {
descbuf.append("records of ").append(recDesc).append("
");
} else {
descbuf.append( recCount ).append(" records of ").append(recDesc).append("
");
}
}
for ( String s: warn ) {
descbuf.append("
");
if ( s.startsWith("NOTE") ) {
descbuf.append(s);
} else {
descbuf.append("WARNING: ").append(s);
}
}
descbuf.append("");
if ( xDependVariable!=null ) {
dependent.put(svar, descbuf.toString());
} else {
result.put(svar, descbuf.toString());
}
} else {
if ( xDependVariable!=null ) {
dependent.put(svar, desc);
} else {
result.put(svar, desc);
}
}
} // for
logger.fine("done, get plottable ");
dependent.putAll(result);
return dependent;
}
/**
* apply the ISTP metadata to the dataset. This is used to implement master files, where metadata from one file
* can override the data within another. Do not use this, as its location will probably change.
*
* @param attr1 the ISTP metadata
* @param result the data
* @param os1 if non-null, then modify the metadata for slice1
* @param constraint if non-null, then drop the render type.
*/
public static void doApplyAttributes(Map attr1, MutablePropertyDataSet result, String os1, String constraint) {
Map istpProps;
MetadataModel model = new IstpMetadataModel();
istpProps= model.properties(attr1);
CdfUtil.maybeAddValidRange(istpProps, result);
Number n= (Number)istpProps.get(QDataSet.FILL_VALUE);
if ( result instanceof BufferDataSet ) {
Class c= ((BufferDataSet)result).getCompatibleComponentType();
if ( n instanceof Double ) {
if ( c==float.class ) {
istpProps.put( QDataSet.FILL_VALUE, (float)n.doubleValue() );
}
}
}
result.putProperty(QDataSet.FILL_VALUE, istpProps.get(QDataSet.FILL_VALUE));
if ( constraint==null ) {
result.putProperty(QDataSet.LABEL, istpProps.get(QDataSet.LABEL) );
} else if ( constraint.matches("\\[:\\,\\d+\\]") ) {
QDataSet labels= (QDataSet)attr1.get("slice1_labels");
if ( labels!=null ) {
Pattern p= Pattern.compile("\\[:\\,(\\d+)\\]");
Matcher m= p.matcher(constraint);
if ( m.matches() ) {
result.putProperty(QDataSet.LABEL, labels.slice(Integer.parseInt(m.group(1))).svalue() );
}
} else {
result.putProperty(QDataSet.LABEL, istpProps.get(QDataSet.LABEL) );
}
} else {
result.putProperty(QDataSet.LABEL, istpProps.get(QDataSet.LABEL) );
}
result.putProperty(QDataSet.TITLE, istpProps.get(QDataSet.TITLE) );
result.putProperty(QDataSet.DESCRIPTION, istpProps.get(QDataSet.DESCRIPTION) );
String renderType= (String)istpProps.get(QDataSet.RENDER_TYPE);
if ( renderType!=null && renderType.equals( "time_series" ) ) {
// kludge for rbsp-a_WFR-waveform_emfisis-L2_20120831_v1.2.1.cdf. This is actually a waveform.
// Note Seth (RBSP/ECT Team) has a file with 64 channels. Dan's file rbsp-a_HFR-spectra_emfisis-L2_20120831_v1.2.3.cdf has 82 channels.
if ( result.rank()>1 && result.length(0)>QDataSet.MAX_UNIT_BUNDLE_COUNT ) {
logger.log(Level.FINE, "result.length(0)>QDataSet.MAX_UNIT_BUNDLE_COUNT={0}, this cannot be treated as a time_series", QDataSet.MAX_UNIT_BUNDLE_COUNT);
renderType=null;
}
}
if ( renderType !=null && renderType.startsWith("image") ) {
logger.fine("renderType=image not supported in CDF files");
renderType= null;
}
if ( UnitsUtil.isNominalMeasurement(SemanticOps.getUnits(result)) ) {
renderType= "eventsbar";
}
if ( constraint!=null ) {
logger.finer("dropping render type because of constraint");
} else if ( os1!=null && os1.length()>0 ) {
logger.finer("dropping render type because of slice1");
for ( int i1=1; i10 && result.length(0) depProps= (Map) istpProps.get("DEPEND_"+j);
if ( depds!=null && depProps!=null ) {
CdfUtil.maybeAddValidRange( depProps, depds );
Map istpProps2 = model.properties(depProps);
depds.putProperty(QDataSet.FILL_VALUE, istpProps2.get(QDataSet.FILL_VALUE));
if ( !UnitsUtil.isTimeLocation( SemanticOps.getUnits(depds) ) ) {
depds.putProperty(QDataSet.LABEL, istpProps2.get(QDataSet.LABEL) );
depds.putProperty(QDataSet.TITLE, istpProps2.get(QDataSet.TITLE) );
}
}
}
result.putProperty( QDataSet.METADATA, attr1 );
result.putProperty( QDataSet.METADATA_MODEL, QDataSet.VALUE_METADATA_MODEL_ISTP );
}
}