API Docs for: 0.0.1
Show:

File: lib/parsers/smet.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 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));