API Docs for: 0.0.1
Show:

File: lib/parsers/caaml.js

/*
 * 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));