/*
* 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 Station = niviz.Station;
var Profile = niviz.Profile;
var Parser = niviz.Parser;
var util = niviz.util;
var extend = util.extend;
var round = util.round;
var alias = util.alias;
var assert = util.assert;
// CAAML custom data parsers
var CAAMLSLFParser = niviz.CAAMLSLFParser;
/** @module niviz */
/**
* CAAML file parser.
*
* @class CAAMLParser
* @constructor
*
* @extends XMLParser
*/
function CAAMLParser(data) {
CAAMLParser.uber.call(this, data);
/**
* 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(CAAMLParser, 'caaml', 'xml');
Parser.format['xml'] = CAAMLParser;
/**
* A list of date/time formats
* recognized by the parser.
*
* @property dates
* @type Array<String>
* @static
*/
CAAMLParser.dates = [
moment.ISO_8601
];
/**
* Conversion table for Profile Features.
*
* @property features
* @type Object
* @static
*/
CAAMLParser.features = {
temperature: {
nodes: 'tempProfile',
layer: 'Obs',
top: 'depth',
value: 'snowTemp',
legacy: {
meta: 'tempProfile > MetaData'
},
meta: 'tempProfile > tempMetaData',
metafields: {
comment: 'comment'
}
},
hardness: {
nodes: 'stratProfile',
layer: 'Layer',
top: 'depthTop',
value: 'Layer > hardness',
ignorenoval: false
},
critical: {
nodes: 'stratProfile',
layer: 'Layer',
top: 'depthTop',
value: 'criticalLayer',
ignorenoval: false
},
hardnessBottom: {
nodes: 'stratProfile',
layer: 'Layer',
top: 'depthTop',
value: 'hardnessBottom',
ignorenoval: false
},
wetness: {
nodes: 'stratProfile',
layer: 'Layer',
top: 'depthTop',
legacy: { // needed for CAAML5
value: 'lwc'
},
value: 'wetness',
ignorenoval: false
},
lwc: {
nodes: 'lwcProfile',
layer: 'Layer',
top: 'depthTop',
value: 'lwc',
ignorenoval: true,
proxy: 'wetness',
offset: 1,
legacy: {
meta: 'lwcProfile > MetaData'
},
meta: 'lwcProfile > lwcMetaData',
metafields: {
comment: 'comment',
method: 'methodOfMeas'
}
},
ssa: {
nodes: 'stratProfile',
layer: 'Layer',
top: 'depthTop',
value: 'Layer > specSurfArea',
ignorenoval: true
},
ssa2: {
nodes: 'specSurfAreaProfile',
layer: 'Layer',
ntuple: true,
top: 'depthTop',
value: 'specSurfArea',
ignorenoval: true,
proxy: 'ssa',
offset: 1,
legacy: {
meta: 'specSurfAreaProfile > MetaData'
},
meta: 'specSurfAreaProfile > specSurfAreaMetaData',
metafields: {
comment: 'comment',
method: 'methodOfMeas'
}
},
comments: {
nodes: 'stratProfile',
layer: 'Layer',
top: 'depthTop',
value: 'comment'
},
grainshape: {
nodes: 'stratProfile',
layer: 'Layer',
top: 'depthTop',
ignorenoval: false,
value: function (node) {
var primary = this.text('grainFormPrimary', node),
secondary = this.text('grainFormSecondary', node);
if (!primary) return null;
return { primary: primary, secondary: secondary };
}
},
grainsize: {
nodes: 'stratProfile',
layer: 'Layer',
top: 'depthTop',
ignorenoval: false,
value: function (node) {
var avg = this.text('grainSize avg', node), max = this.text('grainSize avgMax', node);
if (!avg && !max) return null;
return { avg: avg, max: max };
}
},
ramm: {
nodes: 'hardnessProfile[uomWeightHammer="kg"]',
layer: 'Layer',
top: 'depthTop',
legacy: {
meta: 'hardnessProfile > MetaData'
},
meta: 'hardnessProfile > hardnessMetaData',
metafields: {
comment: 'comment',
method: 'methodOfMeas'
},
value: function (node) {
return {
value: this.text('hardness', node),
depth: this.text('depthTop', node),
height: this.text('thickness', node),
weightHammer: this.text('weightHammer', node),
weightTube: this.text('weightTube', node),
nDrops: this.text('nDrops', node),
dropHeight: this.text('dropHeight', node)
};
}
},
smp: {
nodes: 'hardnessProfile:not([uomWeightHammer="kg"])',
ntuple: true,
legacy: {
meta: 'hardnessProfile > MetaData'
},
meta: 'hardnessProfile > hardnessMetaData',
metafields: {
comment: 'comment',
method: 'methodOfMeas'
}
},
ct: {
nodes: 'stbTests ComprTest',
layer: 'failedOn, noFailure',
legacy: {
nodes: 'stbTests',
layer: 'ComprTest',
value: function (node) {
return {
nofailure: this.$('noFailure', node) ? true : false,
value: this.text('Results testScore', node),
character: this.text('Results fractureCharacter', node),
comment: this.text('comment', node)
};
}
},
top: 'failedOn depthTop',
value: function (node) {
return {
nofailure: node.localName === 'noFailure' ? true : false,
value: this.text('Results testScore', node),
character: this.text('Results fractureCharacter', node),
comment: this.text('comment', node)
};
}
},
ect: {
nodes: 'stbTests ExtColumnTest',
layer: 'failedOn, noFailure',
legacy: {
nodes: 'stbTests',
layer: 'ExtColumnTest',
value: function (node) {
return {
nofailure: this.$('noFailure', node) ? true : false,
value: this.text('Results testScore', node),
comment: this.text('comment', node),
swiss: this.text('customData score1', node) === '' ? '' :
this.text('customData score1', node) + '/' + this.text('customData score2', node)
};
}
},
top: 'failedOn depthTop',
value: function (node) {
return {
nofailure: node.localName === 'noFailure' ? true : false,
value: this.text('Results testScore', node),
comment: this.text('comment', node),
swiss: this.text('customData score1', node) === '' ? '' :
this.text('customData score1', node) + '/' + this.text('customData score2', node)
};
}
},
rb: {
nodes: 'stbTests RBlockTest',
layer: 'failedOn, noFailure',
legacy: {
nodes: 'stbTests',
layer: 'RBlockTest',
value: function (node) {
return {
nofailure: this.$('noFailure', node) ? true : false,
value: this.text('Results testScore', node),
releasetype: this.text('Results releaseType', node),
character: this.text('Results fractureCharacter', node),
comment: this.text('comment', node)
};
}
},
top: 'failedOn depthTop',
value: function (node) {
return {
nofailure: node.localName === 'noFailure' ? true : false,
value: this.text('Results testScore', node),
releasetype: this.text('Results releaseType', node),
character: this.text('Results fractureCharacter', node),
comment: this.text('comment', node)
};
}
},
sf: {
nodes: 'stbTests ShearFrameTest',
layer: 'failedOn, noFailure',
legacy: {
nodes: 'stbTests',
layer: 'ShearFrameTest',
value: function (node) {
return {
nofailure: this.$('noFailure', node) ? true : false,
value: this.text('Results failureForce', node),
character: this.text('Results fractureCharacter', node),
comment: this.text('comment', node)
};
}
},
top: 'failedOn depthTop',
value: function (node) {
return {
nofailure: node.localName === 'noFailure' ? true : false,
value: this.text('Results failureForce', node),
character: this.text('Results fractureCharacter', node),
comment: this.text('comment', node)
};
}
},
saw: {
nodes: 'stbTests PropSawTest',
layer: 'failedOn, noFailure',
legacy: {
nodes: 'stbTests',
layer: 'PropSawTest'
},
top: 'failedOn depthTop',
value: function (node) {
return {
value: this.text('Results fracturePropagation', node),
cutlength: this.text('Results cutLength', node),
columnlength: this.text('Results columnLength', node),
comment: this.text('comment', node)
};
}
},
density: {
nodes: 'densityProfile',
layer: 'Layer',
top: 'depthTop',
value: 'density',
legacy: {
meta: 'densityProfile > MetaData'
},
meta: 'densityProfile > densityMetaData',
metafields: {
comment: 'comment',
method: 'methodOfMeas'
}
},
impurity: {
nodes: 'impurityProfile',
layer: 'Layer',
top: 'depthTop',
value: 'massFraction',
legacy: {
meta: 'impurityProfile > MetaData'
},
meta: 'impurityProfile > impurityMetaData',
metafields: {
comment: 'comment',
//fractiontype: this.$('massFraction', node) ? 'massFraction' : 'volumeFraction',
impuritytype: 'impurity',
method: 'methodOfMeas'
}
},
thickness: {
nodes: 'stratProfile',
layer: 'Layer',
top: 'depthTop',
value: 'thickness'
},
threads: {
nodes: 'thread-profile',
layer: 'layer',
top: 'top',
value: 'thread-color'
}
};
/**
* Main CAAML parser routine.
*
* @method parse_xml
* @private
*/
CAAMLParser.prototype.parse_xml = function () {
assert(this.document, 'No XML document to parse!');
this.version = this.parse_caaml_version() || 5;
var type, profile = new Profile(this.parse_date());
this.parse_gml_id(profile);
this.parse_name();
this.parse_position();
this.parse_general_data(profile);
this.parse_custom_data(profile);
for (type in CAAMLParser.features)
this.parse_feature(type, profile);
// Now parse SLF specific parts of the CAAML document
var slfparser = new CAAMLSLFParser(this, profile);
this.station.push(profile);
this.emit('profile', profile);
return this;
};
/**
* @method parse_custom_data
* @private
*/
CAAMLParser.prototype.parse_custom_data = function (profile) {
//store customData nodes and namespaces
profile.info.custom_meta =
this.document.querySelector('metaDataProperty MetaData customData');
profile.info.custom_snow =
this.document.querySelector('snowProfileResultsOf SnowProfileMeasurements > customData');
profile.info.custom_loc =
this.document.querySelector('locRef ObsPoint MetaData > customData');
};
/**
* @method parse_general_data
* @private
*/
CAAMLParser.prototype.parse_general_data = function (profile) {
var basenode = 'SnowProfileMeasurements ',
person = this.$('srcRef Person'), operation = this.$('srcRef Operation');
if (this.version !== 5) basenode = 'SnowProfileMeasurements weatherCond ';
profile.info.precipitation = this.text(basenode + '> precipTI');
profile.info.sky = this.text(basenode + '> skyCond');
profile.info.ta = this.text(basenode + '> airTempPres');
profile.info.wind = {
speed: this.text(basenode + '> windSpd'),
dir: this.text(basenode + 'windDir AspectPosition > position')
};
// Check if wind direction is actually an angle (i. e. number)
if (profile.info.wind.dir && typeof profile.info.wind.dir === 'number') {
profile.info.wind.angle = profile.info.wind.dir;
profile.info.wind.dir = niviz.Position.angle_to_dir(profile.info.wind.dir);
}
profile.info.roughness = this.text('surfFeatures Components > surfRoughness');
profile.info.comment = {};
profile.info.penetration = {};
if (this.version === 5) {
profile.info.observer = this.text('metaDataProperty MetaData srcRef Person > name');
profile.info.operation = this.text('metaDataProperty MetaData srcRef Person > operation');
profile.info.comment.metadata = this.text('MetaData > comment');
profile.info.comment.weather = this.text('SnowProfileMeasurements > comment');
profile.info.penetration.ram = this.text('SnowProfileMeasurements > penetrationRam');
profile.info.penetration.foot = this.text('SnowProfileMeasurements > penetrationFoot');
profile.info.penetration.ski = this.text('SnowProfileMeasurements > penetrationSki');
profile.info.hn24 = this.text('SnowProfileMeasurements hN24 Components > snowHeight');
profile.info.hs = this.text('hS Components > snowHeight');
} else {
profile.info.operation = this.text('srcRef Operation > name');
if (profile.info.operation)
profile.info.observer = this.text('srcRef contactPerson > name');
else
profile.info.observer = this.text('srcRef Person > name');
profile.info.comment.metadata = this.text('SnowProfile > metaData > comment');
profile.info.comment.weather = this.text('weatherCond metaData > comment');
profile.info.comment.surface = this.text('surfCond metaData > comment');
if (operation && operation.attributes && operation.attributes.getNamedItem('gml:id'))
profile.info.operation_gmlid = operation.attributes.getNamedItem('gml:id').value;
profile.info.penetration.ram = this.text('surfCond > penetrationRam');
profile.info.penetration.foot = this.text('surfCond > penetrationFoot');
profile.info.penetration.ski = this.text('surfCond > penetrationSki');
profile.info.hn24 = this.text('snowPackCond hN24 Components > height');
profile.info.comment.snowpack = this.text('snowPackCond metaData > comment');
profile.info.hs = this.text('snowPackCond hS height');
}
if (person && person.attributes && person.attributes.getNamedItem('gml:id'))
profile.info.observer_gmlid = person.attributes.getNamedItem('gml:id').value;
// The parsing of profile height and snow height is necessary because
// we need to recalculate top values according to depthTop
this.ph = this.text('SnowProfileMeasurements > profileDepth');
if (this.version === 5) {
this.hs = this.text('hS Components > snowHeight');
// HACK: CAAML5 files that sport a snow height, but profile height is bigger
// Solution: adapt snow height
if (this.hs && this.ph && this.ph > this.hs) {
this.hs = this.ph;
profile.info.hs = this.hs;
}
} else {
this.hs = this.text('SnowProfileMeasurements snowPackCond hS height');
}
if (!this.hs) { // the surface will be at 0
this.hs = 0;
profile.info.hs = 0;
}
};
/**
* @property parse_caaml_version
* @private
*/
CAAMLParser.prototype.parse_caaml_version = function () {
var root = this.document, version;
if (root && root.attributes && root.attributes.getNamedItem('xmlns:caaml'))
version = root.attributes.getNamedItem('xmlns:caaml').value;
if (version) version = parseInt(version.match(/\d+/)[0]);
return version;
};
/**
* @property parse_date
* @private
*/
CAAMLParser.prototype.parse_date = function () {
if (this.version === 5)
return moment.parseZone(
this.text('validTime timePosition'),
CAAMLParser.dates,
true
);
return moment.parseZone(
this.text('timeRef recordTime TimeInstant timePosition'),
CAAMLParser.dates,
true
);
};
/**
* @property parse_gml_id
* @private
*/
CAAMLParser.prototype.parse_gml_id = function (profile) {
var root = this.document, id = niviz.CAAML.defaults.profile_gmlid;
if (root && root.attributes && root.attributes.getNamedItem('gml:id'))
id = root.attributes.getNamedItem('gml:id').value;
profile.info.profile_gmlid = id;
return this;
};
/**
* @property parse_name
* @private
*/
CAAMLParser.prototype.parse_name = function () {
if (this.version === 5)
this.station.name = this.text('locRef ObsPoint > name');
else
this.station.name = this.text('locRef > name');
return this;
};
/**
* @property parse_position
* @private
*/
CAAMLParser.prototype.parse_position = function () {
var p = this.station.position, uom = 'm', uomattr, coord, base;
var basenode = 'locRef ObsPoint ';
if (this.version !== 5) basenode = 'locRef ';
base = this.$(basenode);
if (base && base.attributes && base.attributes.getNamedItem('gml:id'))
this.station.id = base.attributes.getNamedItem('gml:id').value;
uomattr = this.$(basenode + 'validElevation ElevationPosition');
if (uomattr && uomattr.attributes && uomattr.attributes.getNamedItem('uom'))
uom = uomattr.attributes.getNamedItem('uom').value;
p.aspect = this.text(basenode + 'validAspect position');
if (typeof p.aspect === 'number') p.azimuth = p.aspect;
p.altitude = this.text(basenode + 'validElevation position');
p.angle = this.text(basenode + 'validSlopeAngle position');
p.subtype = this.text(basenode + 'obsPointSubType');
p.uom = uom;
coord = this.text(basenode + 'pointLocation pos');
if (coord) {
coord = coord.split(/\s+/);
p.latitude = parseFloat(coord[1]);
p.longitude = parseFloat(coord[0]);
}
var x = this.text(basenode + 'customData location position > x');
var y = this.text(basenode + 'customData location position > y');
if (x && y) {
p.x = x;
p.y = y;
}
if (this.version === 5) {
p.description = this.text('locRef ObsPoint > description');
} else {
p.description = this.text('locRef metaData > comment');
}
return this;
};
/**
* @property add_feature
* @private
*/
CAAMLParser.prototype.add_feature = function (type, profile, q, nodes, extrai) {
var top, value, bottom, i, ii;
for (i = 0, ii = nodes.length; i < ii; ++i) {
top = this.convert(q.top, nodes[i]);
value = this.convert(q.value, nodes[i]);
bottom = this.convert('thickness', nodes[i]);
if (q.ignorenoval && (value === '' || value === null)) continue;
if (type === 'threads') { //SLF specific, does not adhere to topdown logic
profile.add(type, [[top, value]], extrai);
} else if (top !== '') {
top = (this.hs - top);
profile.add(type, [[top, value, round(top - bottom, 5)]], extrai);
} else { // Stability tests may not have a top value when no failure occured
profile.add(type, [[top, value]], extrai);
}
}
};
/**
* @property parse_meta_data
* @private
*/
CAAMLParser.prototype.parse_meta_data = function (type, profile, q, node, extrai) {
if (!q.meta) return;
extrai = extrai || 0;
node = node || this.document;
var fields = {}, key, metanode = node.querySelector(q.meta);
if (!metanode) return;
for (key in q.metafields)
fields[key] = this.text(this.$(q.metafields[key], metanode));
if (type === 'impurity')
fields.fractiontype = q.metafields.fractiontype;
if (profile[type] && profile[type].elements[extrai])
profile[type].elements[extrai].meta = extend(profile[type].elements[extrai].meta, fields);
};
/**
* @property parse_ntuple
* @private
*/
CAAMLParser.prototype.parse_ntuple = function (type, profile, q, node, extrai) {
var raw = '', array = [], data = [], top, value,
components = this.$$('MeasurementComponents', node);
if (!components || !components.length) return false;
raw = this.text(this.$('tupleList', node)).trim();
array = raw.split(/\s+/);
if (!array.length) return false;
array.forEach(function (layer) {
data = layer.split(',');
top = parseFloat(data[0]);
value = parseFloat(data[1]);
if (type === 'smp') {
value = { depth: top, value: value };
top = undefined;
} else {
top = (this.hs - top);
}
profile.add(type, [[top, value, top]], extrai);
}, this);
if (profile[type] && profile[type].elements[extrai])
profile[type].elements[extrai].info.pointprofile = true;
return true;
};
/**
* @property parse_feature
* @private
*/
CAAMLParser.prototype.parse_feature = function (type, profile) {
var qcopy = CAAMLParser.features[type], nodes, q = {};
Object.assign(q, qcopy); // Copying so it is easy to change legacy properties
if (this.version === 5 && q.legacy) // override with legacy parameters
Object.assign(q, q.legacy);
nodes = this.$$(q.nodes);
if (!nodes.length) return;
nodes.forEach(function (item, i) {
var n = this.$$(q.layer, nodes[i]), index = i + (q.offset || 0), isntuple = false;
setDynamicProperties(type, q, n);
if (q.ntuple)
isntuple = this.parse_ntuple(q.proxy || type, profile, q, nodes[i], index);
if (!isntuple)
this.add_feature(q.proxy || type, profile, q, n, index);
this.parse_meta_data(q.proxy || type, profile, q, item, index);
}, this);
};
CAAMLParser.prototype.convert = function (query, node) {
switch (typeof query) {
case 'function':
return query.call(this, node);
case 'string':
return this.text(this.$(query, node));
default:
throw new Error('invalid query: ' + query);
}
};
function setDynamicProperties (type, q, n) {
var jj = 0;
if (type === 'impurity') {
q.value = '';
q.metafields.fractiontype = '';
var children = n && n.length && n[0].children;
if (children.length) {
for (jj = 0; jj < children.length; ++jj) {
if (children[jj].nodeName === 'caaml:volumeFraction') {
q.value = 'volumeFraction';
q.metafields.fractiontype = 'volumeFraction';
break;
}
}
if (!q.metafields.fractiontype) {
q.value = 'massFraction';
q.metafields.fractiontype = 'massFraction';
}
}
};
}
}(niviz, moment));