/* ================================================================
* Delft FEWS
* ================================================================
*
* Project Info: http://www.wldelft.nl/soft/fews/index.html
* Project Lead: Karel Heynert (karel.heynert@wldelft.nl)
*
* (C) Copyright 2003, by WL | Delft Hydraulics
* P.O. Box 177
* 2600 MH Delft
* The Netherlands
* http://www.wldelft.nl
*
* DELFT-FEWS is a sophisticated collection of modules designed
* for building a FEWS customised to the specific requirements
* of individual agencies. An open modelling approach allows users
* to add their own modules in an efficient way.
*
* ----------------------------------------------------------------
* SwissRadarTimeSeriesParser.java
* ----------------------------------------------------------------
* (C) Copyright 2003, by WL | Delft Hydraulics
*
* Original Author: Jitka Tacoma
*/
package nl.wldelft.fews.system.plugin.dataImport;
import nl.wldelft.util.TextUtils;
import nl.wldelft.util.io.LineReader;
import nl.wldelft.util.io.TextParser;
import nl.wldelft.util.timeseries.DefaultTimeSeriesHeader;
import nl.wldelft.util.timeseries.TimeSeriesContentHandler;
import nl.wldelft.util.timeseries.TimeSeriesHeader;
import nl.wldelft.util.timeseries.TimeSeriesHeadersConsumer;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
/**
* SwissRadarTimeSeriesParser
reads Swiss Radar file.
* The file contains a single date/time set of values. Identification should be on Keywords:
* Line containing “BASIN_TIME” holds the time of the readings
* Line containing “BASIN_MISSING” holds the missing value indicator used
* Line containing “ACCUM_UNIT” holds the unit. Other header lines can be ignored.
* Lines containing “BASIN_VALUE” contain a reading. This also contains the ID of the location (e.g. SchussenGerbetshaus and the reading.
*
* File example:
* SOURCE: ACQUIRE
* VERSION: 3.5 (5Dec06, uge)
* ACCUM_N: 4
* ACCUM_PS: 1 2 6 12
* ACCUM_DT: 5.000000
* ACCUM_UNIT: millimetre
* BASIN_MAP_N: 11
* BASIN_MAP_IDS: 100 101 102 110 500 501 502 503 504 505 506
*
* BASIN_TIME: 20Jan2007020 1200UTC
* BASIN_MISSING: -99.000000
* BASIN_VALUE_505_003_SchussenGerbetshaus: 0.000316
* BASIN_VALUE_505_003_WuttachOberlauchringen: 0.007470
*/
public class SwissRadarTimeSeriesParser implements TextParser, TimeSeriesHeadersConsumer {
private static final Logger log = Logger.getLogger(SwissRadarTimeSeriesParser.class);
private final static String KEY_UNIT = "ACCUM_UNIT";
private final static String KEY_MISSING_VALUE = "BASIN_MISSING";
private final static String KEY_TIME = "BASIN_TIME";
private final static String KEY_VALUE = "BASIN_VALUE";
private final static char KEY_DELIMITER = ':';
private static final long TIME_UNDEFINED = Long.MIN_VALUE;
private static final float VALUE_UNDEFINED = Float.NEGATIVE_INFINITY;
private final DateFormat dateFormat = new SimpleDateFormat("ddMMMyyyy HHmmz",Locale.US);
private final DateFormat debugDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.US);
private LineReader reader;
private TimeSeriesContentHandler contentHandler;
private HashMap wantedLocationsMap;
private String headerUnit;
private long headerTime;
private float headerMissValue;
private TimeSeriesHeader[] wantedHeaders = null;
@SuppressWarnings({"AssignmentToCollectionOrArrayFieldFromParameter"})
@Override
public void setTimeSeriesHeaders(TimeSeriesHeader[] timeSeriesHeaders) {
this.wantedHeaders = timeSeriesHeaders;
}
@Override
public void parse(LineReader reader, String virtualFileName, TimeSeriesContentHandler contentHandler) throws IOException {
this.contentHandler = contentHandler;
dateFormat.setTimeZone(this.contentHandler.getDefaultTimeZone());
this.reader = reader;
readFile();
}
private void readFile() throws IOException {
//Initialize private fields that holds info about the file
initialize();
//Read unit, missing value and time from the file.
String firstDataLine = readHeader();
this.contentHandler.addMissingValue(this.headerMissValue);
if (firstDataLine != null) {
//Create headers and put them to the contentHandler
setTimeSeriesHeaders(this.headerUnit);
//Read data line and put values to the contentHanlder
if (log.isDebugEnabled()) log.debug("Reading data for the header (unit,miss.value,time) : " +
this.headerUnit + " , " + this.headerMissValue + " , " + debugDateFormat.format(new Date(this.headerTime)));
readData(firstDataLine);
}
}
private void initialize() {
this.wantedLocationsMap = new HashMap();
this.headerUnit = null;
this.headerTime = TIME_UNDEFINED;
this.headerMissValue = VALUE_UNDEFINED;
}
/**
* Read lines from the file up to the first line beginning with the key KEY_VALUE.
* Parse unit, time and missing value from the header.
* Return the first data line or null if any error ocurrs while reading header
*
* @throws IOException when any IO error occurs
*/
private String readHeader() throws IOException {
String firstDataLine = null;
boolean allKeys = false;
String line;
while ((line = this.reader.readLine()) != null) {
if (line.startsWith(KEY_UNIT)) {
this.headerUnit = getUnit(line);
if (this.headerUnit == null) break;
allKeys = getAllKeysFound(this.headerUnit, this.headerTime, this.headerMissValue);
continue;
}
if (line.startsWith(KEY_TIME)) {
this.headerTime = getTime(line);
if (this.headerTime == TIME_UNDEFINED) break;
allKeys = getAllKeysFound(this.headerUnit, this.headerTime, this.headerMissValue);
continue;
}
if (line.startsWith(KEY_MISSING_VALUE)) {
this.headerMissValue = getValue(line);
if (Float.isInfinite(this.headerMissValue)) break;
allKeys = getAllKeysFound(this.headerUnit, this.headerTime, this.headerMissValue);
continue;
}
if (line.startsWith(KEY_VALUE)) {
if (allKeys) {
firstDataLine = line;
} else {
log.error("The file has wrong format, not all required keys " +
KEY_UNIT + ',' + KEY_TIME + ',' + KEY_MISSING_VALUE + " specified before the key " + KEY_VALUE);
}
break;
}
}
return firstDataLine;
}
/**
* Initialize the columns in contentHandler with the headers.
*
* @param unit read from file header
*/
private void setTimeSeriesHeaders(String unit) {
for (int i = 0; i < wantedHeaders.length; i++) {
String loc = wantedHeaders[i].getLocationId();
DefaultTimeSeriesHeader header = new DefaultTimeSeriesHeader();
header.setLocationId(wantedHeaders[i].getLocationId());
header.setUnit(unit);
this.contentHandler.createTimeSeriesHeaderAlias(i, header);
if (this.wantedLocationsMap.get(loc) == null) {
this.wantedLocationsMap.put(loc, new Integer(i)); //Remember the column index for each location
} else {
log.error("Location " + loc + " is configured more than once in the timeseries import configfile ? " +
" Please check it. Parser continues reading data ....");
}
}
}
/**
* @param firstDataLine first line in the file with keyword KEY_VALUE
* @throws IOException if any IO error occurs
*/
private void readData(String firstDataLine) throws IOException {
this.contentHandler.setTime(this.headerTime);
//Get location and value from the first line in the file. If loc. required, put the data in the handler
putLocationAndValue(firstDataLine);
String line;
while ((line = this.reader.readLine()) != null) {
if (!line.startsWith(KEY_VALUE)) continue;
//Get location and value from the line. If loc. required, put the data in the handler
putLocationAndValue(line);
}
}
private void putLocationAndValue(String buffer) throws IOException {
//Get location from the line and check whether it is required
String location = getLocation(buffer);
if (location == null) return;
Object objColumn = this.wantedLocationsMap.get(location);
if (objColumn == null) return; //not required
//get value from the line
float value = getValue(buffer);
if (Float.isInfinite(value)) return;
//Get column for this location and put data to the handler
int column = ((Integer) objColumn).intValue();
this.contentHandler.setTimeSeriesHeader(column);
this.contentHandler.setValue(value);
this.contentHandler.applyCurrentFields();
}
private boolean getAllKeysFound(String unit, long time, float missingValue) {
if (unit != null && time != TIME_UNDEFINED && !Float.isInfinite(missingValue)) {
return true;
} else {
return false;
}
}
private String getUnit(String buffer) {
String unit = TextUtils.rightFrom(buffer, KEY_DELIMITER);
if (unit == null || unit.trim().length() < 1) {
log.warn("No unit specified in the line: " + buffer);
return null;
} else {
return unit.trim();
}
}
private long getTime(String buffer) {
long time = TIME_UNDEFINED;
String str = TextUtils.rightFrom(buffer, KEY_DELIMITER);
if (str != null) {
String[] subStr = TextUtils.split(str, ' ');
if (subStr.length > 1) {
String strDate = subStr[0].substring(0, 9); //get date specification to parse it with ddMMMyyyy
String dateTimeString = strDate + ' ' + subStr[1]; //subStr[1] contains time in the format HHmmz
time = parseTime(dateTimeString);
}
}
if (time == TIME_UNDEFINED) {
log.error("Time format not correct. It should be for date ddMMMyyyy and for time HHmmz. Line: " + buffer);
}
return time;
}
private float getValue(String buffer) {
float value = VALUE_UNDEFINED;
String str = TextUtils.rightFrom(buffer, KEY_DELIMITER);
if (str != null) value = parseValue(str);
if (Float.isInfinite(value)) {
log.error("The line has a value in wrong format: " + buffer);
}
return value;
}
private String getLocation(String buffer) {
String location = null;
String substr = TextUtils.leftFrom(buffer, KEY_DELIMITER);
if (substr != null) location = TextUtils.rightFromLast(substr.trim(), '_');
if (location == null || location.trim().length() < 1) {
log.warn("No location specified in the line: " + buffer);
return null;
} else {
return location.trim();
}
}
private long parseTime(String str) {
long time;
try {
time = dateFormat.parse(str).getTime();
} catch (ParseException ex) {
time = TIME_UNDEFINED;
}
return time;
}
private float parseValue(String buffer) {
float value = VALUE_UNDEFINED;
if (buffer != null) {
String valueText = buffer.trim();
if (valueText.length() > 0) {
try {
value = TextUtils.parseFloat(valueText);
} catch (NumberFormatException e) {
value = VALUE_UNDEFINED;
}
}
}
return value;
}
}