/*
* niViz -- snow profiles visualization
* Copyright (C) 2015 WSL/SLF - Fluelastrasse 11 - 7260 Davos Dorf - Switzerland.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
(function (niviz, moment) {
'use strict';
// --- Module Dependencies ---
var properties = Object.defineProperties;
var keys = Object.keys;
var Meteo = niviz.Meteo;
var Parser = niviz.Parser;
var Position = niviz.Position;
var ProParser = Parser.format.pro;
var util = niviz.util;
var alias = util.alias;
var literal = util.literal;
var underscore = util.underscore;
var borrow = util.borrow;
/** @module niviz */
/**
* A SMET Weather Station Meteorological Data Format parser.
* Reads the input by line in a single pass (supports streamed data).
*
* @class SMETParser
* @constructor
*
* @extends StringParser
*/
function SMETParser(data) {
SMETParser.uber.call(this, data);
/**
* When true (default), the parser will only accept
* lines in their respective sections.
*
* @property strict
* @type Boolean
*/
this.strict = true;
/**
* The current parsing mode. The mode corresponds to
* the different sections in a SMET file:
*
* - 'signature'
* - 'header'
* - 'data'
*
* @property mode
* @type String
*/
this.mode = 'signature';
/**
* @property type
* @type String
*/
this.type;
/**
* @property version
* @type String
*/
this.version;
/**
* @property fields
* @type Array<String>
*/
this.fields;
/**
* @property nodata
* @type Object
*/
this.nodata = null;
/**
* @property tz
* @type Number
*/
this.tz = 'Z';
/**
* @property units
* @type Object
*/
this.units = {};
/**
* @property meta
* @type Object
*/
this.meta = {};
/**
* @property meteo
* @type Meteo
*/
this.meteo = new Meteo();
alias(this, 'result', 'meteo');
}
Parser.implement(SMETParser, 'smet', 'string');
borrow(SMETParser, ProParser, [
/**
* @method restrict
* @see ProParser.prototype.restrict
*/
'restrict'
]);
/**
* A list of date/time formats
* recognized by the parser.
*
* @property dates
* @type Array<String>
* @static
*/
SMETParser.dates = [
moment.ISO_8601
];
properties(SMETParser, {
/**
* A list of all known fields. For each field,
* there is a corresponding parser/converter
* in `.converters`.
*
* @property fields
* @type Array<String>
* @static
*/
fields: {
get: function () { return keys(this.converters); }
}
});
/**
* Type converter functions for PRO data values.
*
* @property converter
* @type Object
* @static
*/
SMETParser.converter = {
'timestamp': function smet$timestamp(value) {
return moment.utc(value, SMETParser.dates, true);
},
'julian': function smet$julian(value) {
throw new Error('julian day format not supported!');
}
};
// --- Lexical Line Patterns ---
var BLANK = /^\s*([#;]|$)/;
var SIGNATURE = /^SMET\s+(\d+(\.\d+)+)\s+(ASCII|BINARY)\s*$/;
var SECTION = /^\[(\w+)\]/;
var HEADER = /^\s*([\w:\.-]+)\s*=\s*([^#;]+)/;
var DATA = /^\s*([^#;]+)/;
// --- Header Patterns ---
var LOCATION = /^(latitude|longitude|altitude|easting|northing|epsg)$/;
var STATION = /^station_(\w+)/;
var UNITS = /^units_(offset|multiplier)$/;
var PLOT = /^(plot_unit|plot_description|plot_color|plot_min|plot_max)$/;
var EXTRA = /^(creation|comment|source)$/;
// --- Parse Methods ---
/**
* Lexically analyses a line and parses it accordingly.
*
* Note: When strict parsing is active (default), this
* method will reject out-of-section lines even if they
* are syntactically correct (e.g., header definitions
* outside of a header section).
*
* @method parse_line
* @private
*
* @param {String} line
*/
SMETParser.prototype.parse_line = function (line) {
var m;
line = line.trim();
switch (true) {
case BLANK.test(line):
break;
case !!(m = SIGNATURE.exec(line)):
this.parse_signature(m[1], m[3]);
break;
case !!(m = SECTION.exec(line)):
this.mode = underscore(m[1]);
if (this.mode === 'data') this.generate_meta();
break;
case !!(m = HEADER.exec(line)):
this.parse_header(underscore(m[1]), m[2]);
break;
case !!(m = DATA.exec(line)):
this.parse_data(m[1]);
break;
default:
throw this.error('unmatched line');
}
};
/**
* @method generate_meta
* @private
*
* @chainable
*/
SMETParser.prototype.generate_meta = function () {
if (!this.meta) return this;
var plot_fields = ['unit', 'description', 'min', 'max', 'color'];
if (this.meteo.fields === undefined || !this.meteo.fields.length) {
throw new Error('SMET header requires a list of fields.');
}
plot_fields.forEach(function (pf) {
var array = this.meta['plot_' + pf];
if (array && array.length === this.fields.length) {
this.fields.forEach(function (parameter, index) {
if (array[index] === 'na' || Number(array[index]) === this.nodata
|| array[index] === '-') return;
this.meteo.plot[parameter] = this.meteo.plot[parameter] || {};
if (pf === 'color') {
this.meteo.plot[parameter][pf] = array[index].replace('0x', '#');
} else if (pf === 'description') {
this.meteo.plot[parameter][pf] = array[index]
.replace(/_/g, ' ')
.replace(/\w\S*/g, function(txt){
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}).
replace(/ Of /g, ' of ');
} else if (pf === 'unit') {
this.meteo.plot[parameter][pf] = array[index]
.replace(/m2/g, 'm\u00B2').replace(/m3/g, 'm\u00B3');
} else {
this.meteo.plot[parameter][pf] = Number(array[index]);
}
}, this);
}
}, this);
delete this.meta;
return this;
};
/**
* @method parse_signature
* @private
*
* @param {String} version
* @param {String} type
*
* @chainable
*/
SMETParser.prototype.parse_signature = function (version, type) {
this.restrict('signature');
this.version = version;
this.type = type;
this.parse_data = this[['parse', underscore(type), 'data'].join('_')];
if (typeof this.parse_data !== 'function')
throw this.error('no data parse available for type: ' + type);
delete this.mode;
return this;
};
/**
* @method parse_header
* @private
*
* @param {String} name
* @param {String} value
*
* @chainable
*/
SMETParser.prototype.parse_header = function (name, value) {
var m;
this.restrict('header');
if (LOCATION.test(name)) {
if (name === 'easting') name = 'x';
if (name === 'northing') name = 'y';
this.meteo.position[name] = literal(value);
} else if (PLOT.test(name)) {
this.meta = this.meta || {};
this.meta[name] = value.split(/\s+/);
} else if ((m = STATION.exec(name)))
this.meteo[m[1]] = value;
else if ((m = UNITS.exec(name)))
this.units[m[1]] = numbers(value);
else if (name === 'fields') {
this.fields = value.split(/\s+/).map(underscore);
this.meteo.fields = this.fields;
} else if (name === 'nodata')
this.nodata = Number(value);
else if (name === 'tz')
this.tz = value + ':00';
else if (EXTRA.test(name))
this[name] = value;
return this;
};
/**
* @method parse_data
* @private
*
* @param {String} name
* @param {String} value
*
* @chainable
*/
SMETParser.prototype.parse_ascii_data = function (string) {
var data = new Meteo.Data();
var values = string.trim().split(/\s+/);
var i, meteo = this.meteo, name;
if (values.length !== this.fields.length)
throw this.error('invalid number of values (' + values.length + ', but '
+ this.fields.length + ' fields defined in header)');
for (i = 0; i < values.length; ++i) {
name = this.fields[i];
data[name] = this.convert(i, values[i]);
if (i && data[name] !== null) {
if (meteo.min[name] === undefined || data[name] < meteo.min[name])
meteo.min[name] = data[name];
if (meteo.max[name] === undefined || data[name] > meteo.max[name])
meteo.max[name] = data[name];
}
}
this.meteo.push(data);
return this;
};
SMETParser.prototype.parse_binary_data = function (data) {
throw this.error('not implemented yet!');
};
/**
* @method convert
* @private
*
* @param {Number} idx
* @param {String} value
*
* @return {Moment|Number} The converted value.
*/
SMETParser.prototype.convert = function (idx, value) {
var type = this.fields[idx];
var convert = SMETParser.converter[type];
value = convert ?
convert.call(this, value) : Number(value);
if (typeof value === 'number') {
if (value === this.nodata) return null;
if (this.units.multiplier)
value = value * this.units.multiplier[idx];
if (this.units.offset)
value = value + this.units.offset[idx];
}
return value;
};
/**
* @method verify
*/
SMETParser.prototype.verify = function () {
var p = this.meteo && this.meteo.position;
if (!p || p.altitude === undefined || p.latitude === undefined
|| p.longitude === undefined) {
throw new Error('SMET header requires latitude/longitude/altitude present.');
}
if (this.nodata === undefined) {
throw new Error('SMET header requires a nodata value.');
}
if (this.meteo.id === undefined) {
throw new Error('SMET header requires a station_id value.');
}
if (this.meteo.fields === undefined || !this.meteo.fields.length) {
throw new Error('SMET header requires a list of fields.');
}
};
// --- Helpers ---
function numbers(string) {
return string.split(/\s+/).map(Number);
}
}(niviz, moment));