decodes.decoder.TimeSeries Maven / Gradle / Ivy
Show all versions of opendcs Show documentation
/*
* $Id$
*/
package decodes.decoder;
import java.util.Vector;
import java.util.Date;
import java.util.Collections;
import java.util.Iterator;
import java.text.NumberFormat;
import ilex.var.TimedVariable;
import ilex.var.NoConversionException;
import ilex.var.IFlags;
import ilex.util.Logger;
import decodes.db.Database;
import decodes.db.EngineeringUnit;
import decodes.db.Constants;
import decodes.db.UnitConverterDb;
import decodes.db.UnitConverter;
import decodes.db.DatabaseException;
import decodes.db.DataPresentation;
import decodes.db.DataType;
import decodes.util.DecodesException;
/**
This holds a series of timed samples for a single sensor on a single
platform. It also contains references to meta-data from the DECODES
database.
*/
public class TimeSeries
implements decodes.comp.ITimeSeries
{
/** Relation to Sensor objects */
private int sensorNumber;
/** EU-converted samples extracted from message */
private Vector samples;
/** Composite object pointing to sensor data. */
private Sensor sensor;
/** Engineering Units for these samples */
private EngineeringUnit eu;
/** Used to EU convert raw samples */
private UnitConverter converter;
/** # seconds between samples */
private int timeInterval;
/** 'A'=ascending, 'D' = descending */
private char dataOrder;
/** dummy EU used if none is supplied */
private static EngineeringUnit unknownEU
= EngineeringUnit.getEngineeringUnit("unknown");
/** Constant defined int RecordedTimeStamp; */
public int timeStatus;
/** Msec value from last call to addSample, without adjustments, if any. */
public long msecAtLastAdd;
/** Msec value from first call to addSample, without adjustments, if any. */
public long msecAtFirstAdd;
private static NumberFormat defaultNumberFormat;
static
{
defaultNumberFormat = NumberFormat.getNumberInstance();
defaultNumberFormat.setMaximumFractionDigits(3);
defaultNumberFormat.setGroupingUsed(false);
}
private boolean _timeJustSet = false;
/**
Constructor.
@param sensorNumber the sensor number -- every time series must have a
unique sensor number.
*/
public TimeSeries(int sensorNumber)
{
this.sensorNumber = sensorNumber;
// beginTime = new Date();
// endTime = new Date(0L);
samples = new Vector();
sensor = null;
eu = unknownEU;
converter = null;
timeInterval = 0;
dataOrder = Constants.dataOrderUndefined;
timeStatus = RecordedTimeStamp.NOTHING;
msecAtLastAdd = Long.MIN_VALUE;
msecAtFirstAdd = Long.MIN_VALUE;
}
/** @return sensor number */
public int getSensorNumber() { return sensorNumber; }
/** @return sensor object */
public Sensor getSensor() { return sensor; }
public String getDataTypeCode() { return sensor.getDataType().getCode(); }
/**
Sets sensor object.
@param sensor the Sensor object in the DECODES database
*/
public void setSensor(Sensor sensor)
{
this.sensor = sensor;
// If EU is not defined, try to determine it from script sensor.
if (eu == unknownEU)
{
if (sensor.scriptSensor == null)
{
Logger.instance().debug1(
"No EU assignment and no script sensor defined for '"
+ sensor.getName() + "'");
}
else if (sensor.scriptSensor.rawConverter == null)
{
Logger.instance().debug1(
"No EU assignment and no raw converter defined for '"
+ sensor.getName() + "'");
}
else
{
UnitConverterDb ucdb = sensor.scriptSensor.rawConverter;
if (!ucdb.isPrepared())
{
try
{
Logger.instance().log(Logger.E_DEBUG3,
"preparing unit converter for raw to " + ucdb.toAbbr);
ucdb.prepareForExec();
}
catch(DatabaseException e)
{
Logger.instance().log(Logger.E_FAILURE,
"Cannot prepare raw EU converter for sensor '"
+ sensor.getName() + "'");
return;
}
}
converter = ucdb.execConverter;
if (converter != null)
eu = converter.getTo();
else
{
Logger.instance().log(Logger.E_DEBUG3,
"No converter defined for sensor '"
+ sensor.getName() + "'");
}
}
}
}
/**
@return the Engineering Units object for this time series.
*/
public EngineeringUnit getEU()
{
return eu;
}
/**
Sets the Engineering Units object for this time series.
Normally the EU is set from the script sensor. However for
derived parameters this method allows direct assignment.
@param eu the EngineeringUnit
*/
public void setEU(EngineeringUnit eu)
{
this.eu = eu;
}
/**
From ITimeSeries interface.
@return units as a string.
*/
public String getUnits()
{
if (eu == null)
return "unknown";
else return eu.abbr;
}
/**
From ITimeSeries interface, sets units as a string.
@param eu the EU abbreviation or name.
*/
public void setUnits(String eu)
{
setEU(EngineeringUnit.getEngineeringUnit(eu));
}
/** @return the time interval value. */
public int getTimeInterval()
{
if (sensor != null && sensor.configSensor!= null
&& Character.toUpperCase(sensor.configSensor.recordingMode)
== Character.toUpperCase(Constants.recordingModeVariable))
return 0;
return timeInterval;
}
/**
Sets the time interval value.
@param v the time interval value
*/
public void setTimeInterval(int v) { timeInterval = v; }
/**
Valid for fixed interval sensors only.
@return the time of the last sample that would be sent prior to
the passed message time.
*/
public Date timeOfLastSampleBefore(Date msgTime)
{
int interval = getTimeInterval();
if (interval == 0)
return msgTime; // Not fixed interval sensor!
long msec = msgTime.getTime();
int msgSecOfDay = (int)((msec / 1000L) % (24 * 60 * 60));
msec -= (msgSecOfDay * 1000L);
int sod = sensor.getTimeOfFirstSample();
//MJM The following was >, it should be >= !!!
if (sod >= msgSecOfDay)
{
// Most recent sample was at the end of yesterday.
sod -= interval; // now negative
}
else while(sod+interval < msgSecOfDay)
sod += interval;
return new Date(msec + (sod * 1000L));
}
/** @return the time of the last sample in the series. */
public Date timeOfLastSampleInSeries()
{
int sz = size();
if (sz == 0)
return null;
return timeAt(sz - 1);
}
/**
Add offset ( in +/- seconds ) to every time in time series
*/
public void addTimeOffset(int offset)
{
for(Iterator it = samples.iterator(); it.hasNext(); )
{
TSSample tss = it.next();
tss.tv.setTime(new Date(tss.tv.getTime().getTime() + (offset * 1000L)));
}
}
/**
Subtracts interval from all samples currently in series.
*/
public void adjustAllTimesBackByInterval()
{
int iv = getTimeInterval();
for(Iterator it = samples.iterator(); it.hasNext(); )
{
TSSample tss = it.next();
tss.tv.setTime(new Date(tss.tv.getTime().getTime() - (iv * 1000L)));
}
}
/**
Adds a sample to the end of the series. No sorting is done in this method.
@param tv the time-stamped value
*/
public void addSample(TimedVariable tv)
{
samples.add(new TSSample(tv));
_timeJustSet = false;
}
/**
@return the time-stamped value at the specified index.
*/
public TimedVariable sampleAt(int idx)
{
if (idx < 0 || idx >= samples.size())
return null;
return (samples.elementAt(idx)).tv;
}
/**
* Deletes the sample at the specified index. All subsequent samples' index
* are moved up.
* @param idx the index within the samples array
* @return true if it was deleted, false if idx is out of bounds.
*/
public boolean deleteSampleAt(int idx)
{
if (idx < 0 || idx >= samples.size())
return false;
samples.remove(idx);
return true;
}
/**
@return the time at the specified index.
*/
public Date timeAt(int idx)
{
return sampleAt(idx).getTime();
}
/**
@return the formated sample at the specified index.
*/
public String formattedSampleAt(int idx)
{
if (idx < 0 || idx >= samples.size())
return null;
TSSample tss = samples.elementAt(idx);
if (tss.fv != null)
{
return tss.fv;
}
try
{
if ((tss.tv.getFlags() & IFlags.IS_MISSING) != 0)
return "missing";
else if ((tss.tv.getFlags() & IFlags.IS_ERROR) != 0)
return "error";
if (!tss.tv.isNumeric())
return tss.tv.getStringValue();
return defaultNumberFormat.format(tss.tv.getDoubleValue());
}
catch(Exception e) { return "nodata"; }
}
public int size() { return samples.size(); }
/*DO NOT USE - we don't set times this way anymore.
public void setTimeAt(int idx, Date timeStamp)
{
if (timeStamp.before(beginTime))
beginTime = timeStamp;
if (timeStamp.after(endTime))
endTime = timeStamp;
TimedVariable tv = sampleAt(idx);
tv.setTime(timeStamp);
}
*/
/**
Sorts samples by time & sets beginning & ending times.
*/
public void sort()
{
sort(false);
}
public void sort(boolean descending)
{
if (samples.size() == 0)
return;
SampleComparator sc = new SampleComparator();
sc.descending = descending;
Collections.sort(samples, sc);
// Find and remove duplicates (samples with same time)
Date prevTime = null;
for(Iterator tsit = samples.iterator(); tsit.hasNext();)
{
TSSample tss = tsit.next();
if (prevTime != null && prevTime.equals(tss.tv.getTime()))
tsit.remove();
prevTime = tss.tv.getTime();
}
}
/**
Returns the time of earliest sample int this time series.
@return Date representing time of earliest sample, or null if empty.
*/
public Date getBeginTime()
{
if (samples.size() == 0)
return null;
Date d = new Date(Long.MAX_VALUE);
for(Iterator it = samples.iterator(); it.hasNext(); )
{
TSSample tss = it.next();
Date tvd = tss.tv.getTime();
if (tvd.before(d))
d = tvd;
}
return d;
}
//
// /** Sets the begin time for this time series. */
// public void setBeginTime(Date d) { beginTime = d; }
//
// /** Returns the end time for this time series. */
// public Date getEndTime() { return endTime; }
//
// /** Sets the end time for this time series. */
// public void setEndTime(Date d) { endTime = d; }
/**
From ITimeSeries interface, sets data order.
@param c 'A' for ascending, 'D' for descending
*/
public void setDataOrder(char c) { dataOrder = Character.toUpperCase(c); }
/** @return true if data order is ascending. */
public boolean isAscending()
{ return dataOrder == Constants.dataOrderAscending; }
public boolean isDescending()
{
return dataOrder == Constants.dataOrderDescending;
}
/**
Convert the units for all values stored in this time series.
*/
public void convertUnits()
{
if (converter == null)
return;
for(int i=0; i it = samples.iterator(); it.hasNext(); )
{
TSSample tss = it.next();
if ((tss.tv.getFlags() & IFlags.IS_MISSING) != 0)
tss.fv = "missing";
else if ((tss.tv.getFlags() & IFlags.IS_ERROR) != 0)
tss.fv = "error";
else
{
try
{
if (!tss.tv.isNumeric())
continue;
double x = tss.tv.getDoubleValue();
if (converter != null)
{
x = converter.convert(x);
tss.tv.setValue(x);
}
if (dp != null)
{
if (dp.getMaxValue() != Constants.undefinedDouble
&& x > dp.getMaxValue())
{
tss.tv.setFlags(IFlags.IS_ERROR);
tss.fv = ">max";
}
else if (dp.getMinValue() != Constants.undefinedDouble
&& x < dp.getMinValue())
{
tss.tv.setFlags(IFlags.IS_ERROR);
tss.fv = " it = samples.iterator(); it.hasNext(); )
{
TSSample tss = it.next();
if (tss.tv == null)
continue;
// Convert value to double, skip if not a number.
double d = 0.0;
try { d = tss.tv.getDoubleValue(); }
catch(NoConversionException ex) { continue; }
if (min != Constants.undefinedDouble && d < min)
{
String what = "discarded";
int f = tss.tv.getFlags() | IFlags.LIMIT_VIOLATION;
if (minReplaceValue != Constants.undefinedDouble)
{
tss.tv.setValue(minReplaceValue);
what = "replaced with " + minReplaceValue;
}
else
f |= IFlags.IS_MISSING;
tss.tv.setFlags(f);
Logger.instance().debug1(sensor.getDisplayName()
+ ": Value " + d + " below minimum of " + min
+ " -- " + what);
}
if (max != Constants.undefinedDouble && d > max)
{
String what = "discarded";
int f = tss.tv.getFlags() | IFlags.LIMIT_VIOLATION;
if (maxReplaceValue != Constants.undefinedDouble)
{
tss.tv.setValue(maxReplaceValue);
what = "replaced with " + maxReplaceValue;
}
else
f |= IFlags.IS_MISSING;
tss.tv.setFlags(f);
Logger.instance().debug1(sensor.getDisplayName()
+ ": Value " + d + " above maximum of " + max
+ " -- " + what);
}
}
}
/**
Discards all samples with times before the passed previous-message-time.
This is called by decoded message when the user has specified that
redundant data should be discarded.
@param prevMsgTime the Java msec time value before which all data is to
be discarded.
*/
public void discardSamplesBefore(long prevMsgTime)
{
for(Iterator it = samples.iterator(); it.hasNext(); )
{
TSSample tss = it.next();
if (tss.tv == null)
continue;
long t = tss.tv.getTime().getTime();
if (t < prevMsgTime)
it.remove();
}
}
/**
Adds a specified value to all samples in the series.
@param v the value to add
*/
public void addToSamples(double v)
{
for(Iterator it = samples.iterator(); it.hasNext(); )
{
TSSample tss = it.next();
if (tss.tv == null)
continue;
try
{
tss.tv.setValue(tss.tv.getDoubleValue() + v);
}
catch(NoConversionException ex) {}
}
}
/**
Multiplies a specified value by all samples in the series.
@param v the value
*/
public void multiplySamplesBy(double v)
{
for(Iterator it = samples.iterator(); it.hasNext(); )
{
TSSample tss = it.next();
if (tss.tv == null)
continue;
try
{
tss.tv.setValue(tss.tv.getDoubleValue() * v);
}
catch(NoConversionException ex) {}
}
}
/**
From ITimeSeries interface
@return the character code representing
data order ('A'=ascending, 'D' = descending)
*/
public char getDataOrder()
{
return dataOrder;
}
/**
From ITimeSeries interface, searches all sensor data for a matching
property in the following order: ConfigSensor, ScriptSensor,
PlatformSensor.
@param name Name of property to search for
@return String property value of null if no match found.
*/
public String getProperty(String name)
{
if (sensor == null)
return null;
return sensor.getProperty(name);
}
/**
* From ITimeSeries interface, sets periodicity parameters
@param mode F=fixed, V=variable
@param firstSamp second-of-day of first sample
@param sampInt interval between samples in seconds
*/
public void setPeriodicity(char mode, int firstSamp, int sampInt)
{
sensor.configSensor.recordingMode = mode;
sensor.configSensor.timeOfFirstSample = firstSamp;
sensor.configSensor.recordingInterval = sampInt;
}
/**
From ITimeSeries interface
@return the sensor number, which is unique
for this time series within this message.
*/
public int getSensorId()
{
return getSensorNumber();
}
/**
From ITimeSeries interface
@return the sensor name.
*/
public String getSensorName()
{
return sensor == null || sensor.configSensor == null ?
(String)null : sensor.configSensor.sensorName;
}
public String getDisplayName()
{
return sensor == null ? "(unknown)" : sensor.getDisplayName();
}
/**
@return time of first sample in this series, assumes samples are sorted.
*/
public int getTimeOfFirstSample()
{
return sensor.getTimeOfFirstSample();
}
/**
* @return recording mode
*/
public char getRecordingMode()
{
return sensor.getRecordingMode();
}
/**
From ITimeSeries interface, associates a data type with this time series.
@param standard the data type standard
@param code the data type code
*/
public void addDataType(String standard, String code)
{
sensor.configSensor.addDataType(DataType.getDataType(standard, code));
}
/** @return true if this sensor has a matching data type. */
public boolean hasDataType(String code)
{
for(Iterator it = sensor.configSensor.getDataTypes();
it.hasNext(); )
{
DataType dt = it.next();
if (code.equalsIgnoreCase(dt.getCode()))
return true;
}
return false;
}
/**
* In order to handle certain times of auto-incrementing of time, as multiple
* samples are added to a sensor, the caller (DecodedMessage) needs to know
* for each time series, if this is the first sample added since time was
* set.
*
* DecodedMessage will call setTimeJustSet whenever time is set by a time or
* date field. The flag is cleared above in this TimeSeries' addSample method.
* @return true if time was set since the last sample-add.
*/
public boolean timeJustSet() { return _timeJustSet; }
/**
* Set flag indicating that time was set. See discussion for timeJustSet().
*/
public void setTimeJustSet() { _timeJustSet = true; }
}
class SampleComparator implements java.util.Comparator
{
public boolean descending = false;
public int compare(TSSample ts1, TSSample ts2)
{
long r = ts1.tv.getTime().getTime() - ts2.tv.getTime().getTime();
int ret = r < 0L ? -1 : r > 0L ? 1 : 0;
return descending ? -ret : ret;
}
public boolean equals(Object obj)
{
return obj == this;
}
}
/** holds a TimedVariable and its formatted string value. */
class TSSample
{
TimedVariable tv; // Contains time and numeric value
String fv; // Value formatted by presentation group (may be null)
TSSample(TimedVariable tv)
{
this.tv = tv;
fv = null;
}
}