/*
* 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 Station = niviz.Station;
var Profile = niviz.Profile;
var Config = niviz.Config;
var Parser = niviz.Parser;
var Feature = niviz.Feature;
var util = niviz.util;
var alias = util.alias;
var assert = util.assert;
var literal = util.literal;
var underscore = util.underscore;
var zip = util.zip;
/** @module niviz */
/**
* Snowpack PRO file parser. A PRO file is parsed line
* by line in a single pass (supports streamed data).
*
* @class ProParser
* @constructor
*
* @extends StringParser
*/
function ProParser(data) {
ProParser.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 PRO file:
*
* - 'station_parameters'
* - 'header'
* - 'data'
*
* @property mode
* @type String
*/
this.mode;
/**
* The current PRO version used. Niviz supports 1.0 and 1.4
*
* @property version
* @type Number
*/
this.version = 1;
/**
* Contains the parsed property headers.
*
* @property properties
* @type Object
*/
this.properties = {};
/**
* The snow station instance; the instance should
* be populated by the parsing methods and represents
* the parse result.
*
* @property station
* @type Station
*/
this.station = new Station();
alias(this, 'result', 'station');
}
Parser.implement(ProParser, 'pro', 'string');
Parser.format['apro'] = ProParser;
/**
* Default conversion table for PRO property codes
* to names used by Niviz. Editable by the user.
*
* @property codes
* @type Config
* @static
*/
ProParser.codes = new Config('Pro Parser', [
{ name: 'pro', type: 'pro',
default: [
{ name: 'Legacy version', version: 1.0, codes: [
{ code: '0502', name: 'density', display: 'Snow density', unit: 'kg/m\u00b3',
editable: false},
{ code: '0503', name: 'temperature', display: 'Snow temperature', unit: '°C',
editable: false},
{ code: '0506', name: 'wetness', display: 'Liquid water content', unit: '%',
editable: false},
{ code: '0508', name: 'dendricity', display: 'Dendricity', editable: false},
{ code: '0509', name: 'sphericity', display: 'Sphericity', editable: false},
{ code: '0510', name: 'cn', display: 'Coordination number', editable: false},
{ code: '0511', name: 'bondsize', display: 'Bondsize', editable: false},
{ code: '0512', name: 'grainsize', display: 'Grain size', unit: 'mm',
editable: false},
{ code: '0513', name: 'grainshape', display: 'Grain shape', editable: false},
{ code: '0530', name: 'properties', editable: false},
{ code: '0534', name: 'hardness', display: 'Snow hardness', unit: 'N',
editable: false}
]},
{ name: 'Version 1.4', version: 1.4, codes: [
{ code: '0503', name: 'density', display: 'Snow density', unit: 'kg/m\u00b3',
editable: false},
{ code: '0506', name: 'wetness', display: 'Liquid water content', unit: '%',
editable: false},
{ code: '0510', name: 'temperature', display: 'Snow temperature', unit: '°C',
editable: false},
{ code: '0513', name: 'grainshape', display: 'Grain shape', editable: false},
{ code: '0516', name: 'grainsize', display: 'Grain size', unit: 'mm',
editable: false},
{ code: '0517', name: 'bondsize', display: 'Bondsize', editable: false},
{ code: '0518', name: 'dendricity', display: 'Dendricity', editable: false},
{ code: '0519', name: 'sphericity', display: 'Sphericity', editable: false},
{ code: '0520', name: 'cn', display: 'Coordination number', editable: false},
{ code: '0530', name: 'properties', editable: false},
{ code: '0540', name: 'hardness', display: 'Snow hardness', unit: 'N',
editable: false},
{ code: '0910', name: 'tn', display: 'Snow node temperature', unit: '°C',
editable: false}
]}
]
}
]);
ProParser.codes.load();
/**
* Get the conversion table which is a key/value object,
* key being the PRO file code and the value being the property
* name used within niViz.
*
* @method getcodes
* @static
* @return {Object}
*/
ProParser.getcodes = function (version) {
version = version || 1;
var pro = ProParser.codes.values.pro, i, ii = pro.length, result = {};
for (i = 0; i < ii; ++i) {
if (pro[i].version === version) {
pro = pro[i].codes;
ii = pro.length;
break;
}
}
for (i = 0; i < ii; ++i) {
result[pro[i].code] = pro[i].name;
if (!Feature.type[pro[i].name] && pro[i].code !== '0530')
Feature.register(pro[i].name, {
name: pro[i].display,
unit: pro[i].unit
});
}
result['0500'] = 'date';
result['0501'] = 'height';
result['0530'] = 'properties';
return result;
};
/**
* A list of date/time formats
* recognized by the parser.
*
* @property dates
* @type Array<String>
* @static
*/
ProParser.dates = [
'DD.MM.YYYY HH:mm',
'DD.MM.YYYY HH:mm:ss',
moment.ISO_8601
];
/**
* The conversion table for PRO property codes
* to names used by Niviz.
*
* @property code
* @type Object
* @static
*/
ProParser.code = ProParser.getcodes();
ProParser.codelist = {
'0500': 'date',
'0501': 'height',
'0502': 'density',
'0503': 'temperature',
'0506': 'wetness',
'0508': 'dendricity',
'0509': 'sphericity',
'0510': 'cn',
'0511': 'bondsize',
'0512': 'grainsize',
'0513': 'grainshape',
//'0515': 'ice volume fraction (%)',
//'0516': 'air volume fraction (%)',
//'0517': 'stress',
//'0518': 'viscosity',
//'0519': 'soil volume fraction (%)',
//'0520': 'temperature gradient',
//'0521': 'thermal conductivity',
//'0522': 'absorbed shortwave radiation (W m-2)',
//'0523': 'viscous deformation rate (1.e-6 s-1)',
'0530': 'properties',
//'0531': 'deformation rate stability index Sdef',
//'0532': 'natural stability index Sn38',
//'0533': 'stability index Sk38',
'0534': 'hardness'
//'0535': 'inverse texture index ITI (Mg m-4)',
//'0601': 'snow shear strength (kPa)',
//'0602': 'grain size difference (mm)',
//'0603': 'hardness difference (1)'
};
ProParser.reserved = [];
for (var key in ProParser.codelist) {
ProParser.reserved.push(ProParser.codelist[key]);
}
ProParser.propertyNames = ['profile_type', 'stability_class',
'z_sdef', 'sdef', 'z_sn38', 'sn38', 'z_sk38', 'sk38'];
properties(ProParser, {
/**
* A list of all known data value types.
* For each type, there is a corresponding
* parser/converter in `.converters`.
*
* @property types
* @type Array<String>
* @static
*/
types: {
get: function () { return Object.keys(this.converters); }
}
});
/**
* Type converter functions for PRO data values.
*
* @property converter
* @type Object
* @static
*/
ProParser.converter = {
'Date': function pro$date(value) {
return moment.utc(value, ProParser.dates, true);
},
'nElems': function pro$numbers(value) {
var numbers = value.split(/\s*,\s*/);
var max = parseInt(numbers.shift(), 10);
if (numbers.length > max)
throw new Error('possible buffer overflow detected');
return numbers.map(literal);
},
'nNodes': function pro$numbers(value) {
var numbers = value.split(/\s*,\s*/);
var max = parseInt(numbers.shift(), 10);
if (numbers.length > max)
throw new Error('possible buffer overflow detected');
return numbers.map(literal);
},
'properties': function pro$properties(value) {
var numbers = value.split(/\s*,\s*/).map(literal);
var max = parseInt(numbers.shift(), 10);
var object = {};
if (numbers.length !== 8)
throw new Error('0530: wrong number of propeties');
ProParser.propertyNames.forEach(function (item, i) {
object[item] = numbers[i];
});
return object;
}
};
// --- Lexical Line Patterns ---
var VERSION = /\#\s+PRO\s+(\d{1}.\d{1,3})\s+ASCII/i;
var BLANK = /^\s*([#";]|$)/;
var SECTION = /^\[(\w+)\]/;
var PARAM = /^([\w:\.-]+)\s*=\s*(.+)$/;
//var HEADER = /^(\d{4}),([a-z0-9\-]+)(,(.+))?$/i;
//var HEADER = /^(\d{4}),([a-z0-9]+)(\+\d){0,1}/i;
//var HEADER = /^(\d{4}),([a-z0-9]+)(\+\d){0,1}(,(([\w\s\/\%\<\>\:]))+)*/i;
var HEADER = /^(\d{4}),([a-z0-9]+)(\+\d){0,1}((,[\w\s\/\%\<\>\:=\(\)\-\+.]*)*)/i;
var DATA = /^(\d{4}),(.+)$/i;
//var LABELS = /:\s+([\w\s,]+)$/i;
// --- Parse Methods ---
ProParser.prototype.meta = function () {
ProParser.codes.load();
ProParser.code = ProParser.getcodes(this.version);
this.station.meta = {};
for (var property in ProParser.code) {
if (ProParser.code.hasOwnProperty(property)) {
this.station.meta[ProParser.code[property]] = this.properties[property];
}
}
};
/**
* 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
*/
ProParser.prototype.parse_line = function (line) {
var m;
switch (true) {
case !!(m = VERSION.exec(line)):
if (m.length) this.version = parseFloat(m[1]);
break;
case BLANK.test(line):
break;
case !!(m = SECTION.exec(line)):
this.mode = underscore(m[1]);
if (this.mode === 'data') {
this.meta();
this.parse_line = function (line) {
var m;
switch (true) {
case BLANK.test(line):
break;
case !!(m = DATA.exec(line)):
this.parse_data(m[1], m[2]);
break;
default:
throw this.error('unmatched line: ' + line);
}
};
}
break;
case !!(m = PARAM.exec(line)):
this.parse_parameter(m[1], m[2]);
break;
case !!(m = HEADER.exec(line)):
this.parse_header(m[1], m[2], m[4] || '');
break;
case !!(m = DATA.exec(line)):
this.parse_data(m[1], m[2]);
break;
default:
throw this.error('unmatched line');
}
};
/**
* Take all the layer tops and create three arrays: one with all the layer bottoms,
* one with all the tops above ground, and one with all the bottoms above ground.
*
* @method sanitize
* @private
*
* @param {Array<Number>} tops The layer tops
* @return {Object} Object with three arrays: bottoms, above0 and bottoms0
*/
ProParser.prototype.sanitize = function (tops) {
var below0 = tops.length && tops[0] < 0, ii = tops.length, bottoms, above0, bottoms0,
nodes, nodes0;
above0 = tops.filter(function (top) {
return top > 0;
});
if (ii) bottoms = tops.slice(0, ii - 1);
if (above0.length) {
bottoms0 = above0.slice(0, above0.length - 1);
bottoms0.unshift(0);
}
if (!below0 && ii) bottoms.unshift(0);
if (below0 && ii) tops.shift();
nodes = tops.slice(0, ii);
if (bottoms && bottoms.length) nodes.unshift(bottoms[0]);
nodes0 = above0.slice(0, above0.length);
if (bottoms0 && bottoms0.length) nodes0.unshift(bottoms0[0]);
return {
bottoms: bottoms,
above0: above0,
bottoms0: bottoms0,
nodes: nodes,
nodes0: nodes0
};
};
/**
* Parses profile data and adds it to the station.
*
* This method consumes lines until a property id
* repeats or the following line does not start
* with an id.
*
* @method parse_data
* @private
*
* @chainable
*/
ProParser.prototype.parse_data = function (id, data) {
ProParser.codes.load();
ProParser.code = ProParser.getcodes(this.version);
this.restrict('data');
assert(id === '0500', 'DATA 0500 expected');
var next = this.skip('0500'), type, props, values, m, hoar;
if (!next) return this; // v1.4 allows to skip 0501
if (!(m = DATA.exec(next)) || m[1] !== '0501')
throw this.error('DATA 0501 expected');
var tops = this.convert(m[1], m[2]);
var heights = this.sanitize(tops);
var profile = new Profile(this.convert(id, data));
profile.top = tops[tops.length - 1] || 0;
profile.bottom = heights.bottoms ? heights.bottoms[0] : 0;
// Add comments profile property
add(profile, 'comments', 'nElems', heights,
Array.apply(null, {length: tops.length}).map(function() { return ''; }),
tops);
while ((next = this.skip('0500'))) {
if (!(m = DATA.exec(next)))
throw this.error('DATA expected');
type = ProParser.code[m[1]];
props = this.properties[m[1]];
if (!type) continue;
values = this.convert(m[1], m[2]);
if (type !== 'properties' && !values.length) continue;
switch (type) {
case 'properties':
if (values.sk38 === undefined || values.z_sk38 === undefined) break;
profile.add('sk38', zip([values.z_sk38],
[{ classnum: values.stability_class,
sk38: values.sk38 }]));
break;
case 'hardness':
if (tops.length === values.length) {
profile.add(type, zip(tops, values.map(Math.abs), heights.bottoms));
} else if (heights.above0.length === values.length) {
profile.add(type, zip(heights.above0, values.map(Math.abs), heights.bottoms0));
}
break;
case 'grainshape':
hoar = false;
values[values.length - 1] === 660 ? hoar = true : hoar = false;
values = values.slice(0, values.length - 1);
if (!values.length) continue;
if (hoar) profile.sh = true; // SH present, legacy version
if (tops.length === values.length) {
profile.add(type, zip(tops, values, heights.bottoms));
} else if (heights.above0.length === values.length) {
profile.add(type, zip(heights.above0, values, heights.bottoms0));
}
break;
default:
add(profile, type, props, heights, values, tops);
}
}
this.station.push(profile);
this.emit('profile', profile);
return this;
};
function add (profile, type, props, heights, values, tops) {
if (!tops.length) return;
if (props.type === 'nNodes') {
if (heights.nodes.length === values.length) {
profile.add(type, zip(heights.nodes, values, heights.nodes));
} else if (heights.nodes0.length === values.length) {
profile.add(type, zip(heights.nodes0, values, heights.nodes0));
} else {
//throw this.error('expected value for each layer!');
}
} else if (tops.length === values.length) {
profile.add(type, zip(tops, values, heights.bottoms));
} else if (heights.above0.length === values.length) {
profile.add(type, zip(heights.above0, values, heights.bottoms0));
} else {
//throw this.error('expected value for each layer!');
}
}
/**
* Returns the next line that is not a blank line
* or comment; if given, the next line that starts
* with `until` is treated like EOD.
*
* @method skip
*
* @param {String} [until]
* @returns {String|undefined} The next line.
*/
ProParser.prototype.skip = function (until) {
return this.constructor.uber.prototype.skip.call(this, BLANK, until);
};
/**
* @method parse_parameter
* @private
*
* @chainable
*/
ProParser.prototype.parse_parameter = function (name, value) {
this.restrict('station_parameters');
name = name.toLowerCase();
value = literal(value);
switch (name) {
case 'stationname':
this.station.name = value;
break;
case 'latitude':
case 'longitude':
case 'altitude':
this.station.position[name] = value;
break;
case 'slopeangle':
this.station.position.angle = value;
break;
case 'slopeazi':
this.station.position.azimuth = value;
break;
default:
// skip parameter
}
return this;
};
/**
* Parses a profile header line and registers the
* property and the corresponding value converter
* function in `properties`.
*
* Note: Since header definitions can span multiple
* lines, this method peeks ahead and consumes
* subsequent indented lines!
*
* @method parse_header
* @private
*
* @param {String} id
* @param {String} type
* @param {String} description
*
* @chainable
*/
ProParser.prototype.parse_header = function (id, type, description) {
var labels = [];
this.restrict('header');
// Support multi-line headers!
while ((/\s|[a-z]/i).test(this.peek()))
description += ' ' + this.next(); // HACK: Deal with linebreaks properly
// Extract labels (if present)!
// if ((m = LABELS.exec(description)))
// labels = m[1].split(/\s*,\s*/).map(underscore);
labels = description.split(',');
if (type.match(/^\d+$/)) type = 'nElems'; // type consists of digits only
if (type.match(/^nNodes$/i)) type = 'nNodes'; // type consists of digits only
this.properties[id] = { id: id, type: type };
if (labels.length) {
this.properties[id].display = labels.length > 1 ? labels[1] : null;
this.properties[id].name = labels.length > 2 ? labels[2] : null;
this.properties[id].unit = labels.length > 3 ? labels[3] : null;
this.properties[id].left = labels.length > 4 ? labels[4] : null;
this.properties[id].right = labels.length > 5 ? labels[5] : null;
this.properties[id].log = labels.length > 6 ? labels[6] : null;
this.properties[id].gradient = labels.length > 7 ? labels[7] : null;
}
return this;
};
/**
* Looks up the converter for type of `id` and converts
* the value; throws an error if there is no
* suitable converter available.
*
* @method convert
* @private
*
* @param {String} id
* @param {String} value
*
* @return {Moment|Array|Object} The converted value.
*/
ProParser.prototype.convert = function (id, value) {
var property = this.properties[id];
if (id === '0530') property.type = 'properties';
if (!property)
throw this.error('no type definition for "%s"', id);
var convert = ProParser.converter[property.type];
if (!convert)
throw this.error('no converter for "%s"', property.type);
value = convert(value);
if (property.labels)
value = label(property.labels, value);
return value;
};
/**
* Ensure that the current mode is `mode`; throws
* an Error otherwise.
*
* Note: Does not throw if `.strict` is set to false.
*
* @method restrict
* @private
*
* @param {String} mode
*
* @returns {Boolean} Whether or not `mode` is current.
*/
ProParser.prototype.restrict = function (mode) {
var matches;
if (!(matches = (this.mode === mode)) && this.strict)
throw this.error('bad section: %s expected, was %s', mode, this.mode);
return matches;
};
// --- Helpers ---
function label(labels, values) {
if (labels.length !== values.length) return values;
assert(labels.length === values.length);
var i, ii, obj = {};
for (i = 0, ii = labels.length; i < ii; ++i)
obj[labels[i]] = values[i];
return obj;
}
// --- Module Exports ---
niviz.ProParser = ProParser;
}(niviz, moment));