API Docs for: 0.0.1
Show:

File: lib/profile.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/>.
 */

/*eslint complexity: [2, 15]*/
(function (niviz, moment) {
  'use strict';

  // --- Module Dependencies ---
  var properties = Object.defineProperties;
  var FeatureSet = niviz.FeatureSet;
  var Feature    = niviz.Feature;

  var assert     = niviz.util.assert;
  var pick       = niviz.util.pick;

  /** @module niviz */

  /**
   * A Snow Profile.
   *
   * @class Profile
   * @constructor
   */
  function Profile(date) {
    /**
     * @property date
     * @type {Moment}
     */
    this.date = date || moment();

    /**
     * The height at the top of the topmost layer in the profile,
     * measured from the ground.
     *
     * @property top
     * @type {Number}
     */
    this.top = 0;

    /**
     * The height at the bottom of the lowest layer in the profile,
     * measured from the ground. Not all values have a bottom value,
     * thus initialization with undefined.
     *
     * @property bottom
     * @type {Number}
     */
    this.bottom = undefined;

    /**
     * Observations and metadata regarding this profile, such as
     * comments, sky conditions, air temperature, etc.
     *
     * @property info
     * @type {Object}
     */
    this.info = {
      profile_gmlid: niviz.CAAML.defaults.profile_gmlid,
      observer_gmlid: niviz.CAAML.defaults.observer_gmlid,
      operation_gmlid: niviz.CAAML.defaults.operation_gmlid
    };

    /**
     * A list of all avaialable features for this profile.
     * The list is updated automatically when adding
     * features using the `.add` method.
     *
     * @property features
     * @type Array<String>
     */
    this.features = [];
  }

  Profile.flaggs = ['FC', 'FCxr', 'SH', 'DH'];

  /**
   * All features that make up the stratigraphic features
   *
   * @property stratigraphic
   * @type {Array.<string>}
   */
  Profile.stratigraphic = ['wetness', 'hardness', 'grainshape', 'grainsize', 'comments'];

  /**
   * Calculate the yellow flags within a layer, given the hand hardness,
   * grainsize and grainshape values.
   *
   * @static
   * @method layerflags
   * @param {Hardness}
   * @param {Grainsize}
   * @param {Grainshape}
   * @return {Object} An object containing the layer flags and the grainsize average
   */
  Profile.layerflags = function (hardness, grainsize, grainshape) {
    var gs = 0, gt = 0, h = 0, gsavg = grainsize.avg || 0;

    if (grainsize.max) gsavg = (gsavg + grainsize.max) / 2;
    if (grainshape.value.primary === 'IF') gsavg = 2; // assume 2mm for ice layers
    if (gsavg >= 1.25) gs++;

    if (hardness.value < 2) h++;
    if (Profile.flaggs.indexOf(grainshape.value.primary) > -1) gt++;
    if (grainshape.value.primary === 'MFcr'
        && Profile.flaggs.indexOf(grainshape.value.secondary) > -1) gt++;

    return { gsavg: gsavg, gs: gs, gt: gt, h: h, sum: gs + gt + h };
  };

  Profile.windspeed = [
    { symbol: 'C', text: 'Calm', min: 0, max: 0 },
    { symbol: 'L', text: 'Light', min: 0, max: 7.5 },
    { symbol: 'M', text: 'Moderate', min: 7.5, max: 11.5 },
    { symbol: 'S', text: 'Strong', min: 11.5, max: 17 },
    { symbol: 'X', text: 'Extreme', min: 17, max: 100 }
  ];

  Profile.convert_windspeed = function (data) {
    if (data === 0) return Profile.windspeed[0];
    else if (data > 0 && data <= 7.5) return Profile.windspeed[1];
    else if (data > 7.5 && data <= 11.5) return Profile.windspeed[2];
    else if (data > 11.5 && data <= 17) return Profile.windspeed[3];
    else if (data > 17) return Profile.windspeed[4];
  };

  properties(Profile.prototype, {
    /**
     * The height of the snow profile.
     *
     * @property height
     * @type {Number}
     */
    height: {
      get: function () { return this.top - this.bottom; }
    },

    /**
     * Snow height is the total snow height or
     * 0 if snow height is unknown
     *
     * @property hs
     * @type {Number}
     */
    hs: {
      get: function hs$get () {
        if (this.top && ((Number.isFinite && Number.isFinite(this.top)) || isFinite(this.top)))
          return this.top;

        return 0;
      }
    },

    /**
     * Snow water equivalent calculated by:
     * SWE [mm] = Snow Depth [m] * Density [kg / m³]
     *
     * @property swe
     * @type {Number}
     */
    swe: {
      get: function swe$get () {
        if (!this.density || !this.density.elements[0]) return null;

        var avg = this.density.elements[0].average();

        if (avg) return this.density.elements[0].height / 100 * avg;

        return null;
      }
    },

    /**
     * Accessor for the stability features:
     * CT, ECT, RB, SF and Saw.
     *
     * @property stability
     * @type {Array.<Value>}
     */
    stability: {
      get: function () {
        var tests = [];

        if (this.ct) tests.push(this.ct);
        if (this.ect) tests.push(this.ect);
        if (this.rb) tests.push(this.rb);
        if (this.sf) tests.push(this.sf);
        if (this.saw) tests.push(this.saw);
        if (this.flags) tests.push(this.flags);

        return tests;
      }
    },

    /**
     * Accessor to the yellow flags feature, which is calculated
     * on the fly
     *
     * @property flags
     * @type {Feature}
     */
    flags: {
      get: function () {
        if (!this.grainshape || !this.hardness || !this.grainsize
            || this.grainshape.layers.length !== this.hardness.layers.length
            || this.grainsize.layers.length !== this.hardness.layers.length)
          return undefined;

        if (!Feature.type['flags'])
          Feature.register('flags', { name: 'flags', symbol: 'N' });

        var feature = new Feature('flags', this), grainshape = this.grainshape.layers,
          grainsize = this.grainsize.layers, hardness = this.hardness.layers,
          depth = this.top - 100;

        for (var ii = 0; ii < this.grainshape.layers.length - 1; ++ii) {
          var val = { gs: 0, gt: 0, h: 0, dgs: 0, dh: 0, depth: 0 }, fail = ii, other = ii + 1;

          // Layer properties
          var c = Profile.layerflags(hardness[ii], grainsize[ii], grainshape[ii]), gsavg = 0,
              n = Profile.layerflags(hardness[other], grainsize[other], grainshape[other]);

          if (n.sum > c.sum) {
            val.gs = n.gs; val.gt = n.gt; val.h  = n.h; val.sum = n.sum;
            gsavg = n.gsavg;
            other = [fail, fail = other][0];
          } else {
            val.gs = c.gs; val.gt = c.gt; val.h  = c.h; val.sum = c.sum;
            gsavg = c.gsavg;
          }

          //Compare properties of the two layers adjacent to the interface
          var ogsavg = grainsize[other].avg || 0;
          if (grainsize[other].max) ogsavg = (ogsavg + grainsize[other].max) / 2;

          // assume 2mm for ice layers
          if (grainshape[other].value.primary === 'IF') ogsavg = 2;

          if (Math.abs(gsavg - ogsavg) >= 0.75) val.dgs++;
          if (Math.abs(hardness[fail].value - hardness[other].value) >= 2) val.dh++;
          if (grainshape[ii].top > depth) val.depth++;

          val.sum = val.sum + val.dgs + val.dh + val.depth;

          feature.concat([[
            this.grainshape.layers[ii].top,
            val,
            this.grainshape.layers[ii].bottom
          ]]);
        }

        return feature;
      }
    },

    /**
     * Accessor to the temperature gradient feature,
     * which is calculated on the fly
     *
     * @property gradient
     * @type {Feature}
     */
    gradient: {
      get: function () {
        if (!this.temperature) return undefined;

        var featureset = new FeatureSet('gradient', this);

        var feature = new Feature('gradient', this), i, height, grad,
          ii = this.temperature.layers.length - 1,
          layers = this.temperature.layers;

        for (i = 0; i < ii; ++i) {
          height = (layers[i + 1].top - layers[i].top) / 100; // in m
          grad   = (layers[i + 1].value - layers[i].value) / height;

          feature.concat([[
            (layers[i + 1].top + layers[i].top) / 2,
            grad
          ]]);
        }

        featureset.elements[0] = feature;

        return featureset;
      }
    },

    /**
     * Collect all relevant metadata in one object
     *
     * @property meta
     * @type Object
     */
    meta: {
      get: function () {
        var obj  = JSON.parse(JSON.stringify(this.info)); // deep copy
        obj.date = moment(this.date);

        ['custom_meta', 'custom_loc', 'custom_snow'].forEach(function (p) {
          delete obj[p];
        });

        return obj;
      },
      set: function (obj) {
        if (!obj) return;

        this.info = JSON.parse(JSON.stringify(obj)) || {};
        delete this.info.date;

        if (!obj.date || !moment(obj.date).isValid()) throw new Error('Invalid date');
        this.date = moment(obj.date);
      }
    }
  });

  /**
   * Add a feature without layers
   *
   * @method addfeature
   * @chainable
   *
   * @param {String} type feature type
   */
  Profile.prototype.addfeature = function (type, extrai) {
    assert(!!Feature.type[type], 'Invalid feature type: ' + type);
    assert(extrai || extrai === 0, 'Invalid feature type: ' + type);

    if (!this[type]) this[type] = new FeatureSet(type, this);
    var feature = new Feature(type, this);

    if (!this[type].length) this.features.push(type);

    this[type].elements[extrai] = feature;

    return this;
  };

  /**
   * Add feature values to this profile.
   *
   * @method add
   * @chainable
   *
   * @param {String} type feature type
   * @param {Array} layers values for each layer
   */
  Profile.prototype.add = function (type, layers, extrai) {
    assert(!!Feature.type[type], 'Invalid feature type: ' + type);
    assert(Array.isArray(layers), 'Invalid layers array: ' + layers);

    var feature;
    if (!this[type]) this[type] = new FeatureSet(type, this);
    feature = this[type].elements[extrai || 0] || new Feature(type, this);

    feature.concat(layers);

    if (feature.top > this.top) this.top = feature.top;

    if (this.bottom === undefined || feature.bottom < this.bottom)
      this.bottom = feature.bottom;

    if (!this[type].length) this.features.push(type);

    this[type].elements[extrai || 0] = feature;

    return this.adjust();
  };

  /**
   * Adjust top and bottom value by looping through all features
   *
   * @method adjust
   * @chainable
   */
  Profile.prototype.adjust = function () {
    var self = this;

    this.top = 0;
    this.bottom = undefined;

    if (this.features.length) {
      this.top = Math.max.apply(Math, this.features.map( function(f) {
        if (f === 'ramm' || f === 'smp') return 0; // depthTop types

        var max = self[f] && self[f].top || 0;
        return max;
      }));

      if (this.info.hs || this.info.hs === 0) this.top = this.info.hs;

      var features = this.features.filter(function (f) {
        return (self[f] && (self[f].bottom || self[f].bottom === 0));
      });

      this.bottom = Math.min.apply(Math, features.map( function(f) {
        var min = self[f] && self[f].bottom || 0;
        return min;
      }));
    }

    if (this.info.hs || this.info.hs === 0) this.top = this.info.hs;

    return this;
  };

  /**
   * Delete certain Feature from a present FeatureSet
   *
   * @method rmfeature
   * @chainable
   *
   * @param {String} type feature type
   * @param {Number} ielement feature index
   */
  Profile.prototype.rmfeature = function (type, ielement) {
    if (ielement !== 0 && !ielement)
      throw new Error('rmfeature requires a second parameter, was: ' + ielement);

    if (!this[type] || !this[type].length > ielement)
      throw new Error('Index out of bounds in rmfeature');

    this[type].elements.splice(ielement, 1);
    if (!this[type].length) {
      delete this[type];
      this.features.splice(this.features.indexOf(type), 1);
    }

    return this.adjust();
  };

  /**
   * Add a layer to one of the features present
   *
   * @method addlayer
   * @chainable
   *
   * @param {String} type feature type
   * @param {Object} values layer data
   */
  Profile.prototype.addlayer = function (type, values, ielement) {
    var val = val = values.value;
    if (!val && val !== 0) val = values[type];

    if (type === '_stratigraphic') {
      Profile.stratigraphic.forEach(function (p) {
        this.addlayer(p, values);
      }, this);
    } else {
      this.add(type, [[values.top,  val, values.bottom]], ielement);
    }

    return this;
  };

  /**
   * Delete a layer of one of the features present
   *
   * @method rmlayer
   * @chainable
   *
   * @param {String} type feature type
   * @param {Number} i layer index
   */
  Profile.prototype.rmlayer = function (type, i, ielement) {
    ielement = ielement || 0;

    if (type === '_stratigraphic') {
      Profile.stratigraphic.forEach(function (p) {
        this.rmlayer(p, i);
      }, this);
    } else {
      this[type].elements[ielement].rmlayer(i);

      if (!this[type].elements[ielement].layers.length) {
        this[type].elements.splice(ielement, 1);
      }

      if (!this[type].length) {
        this.features.splice(this.features.indexOf(type), 1);
        delete this[type];
      }
    }

    return this.adjust();
  };

  /**
   * Edit a layer of one of the features present
   *
   * @method editlayer
   * @chainable
   *
   * @param {String} type feature type
   * @param {Number} i layer index
   * @param {Object} values layer data
   */
  Profile.prototype.editlayer = function (type, i, values, ielement) {
    ielement = ielement || 0;

    if (type === '_stratigraphic') {
      Profile.stratigraphic.forEach(function (p) {
        this.editlayer(p, i, values);
      }, this);

    } else {
      if (this[type] && this[type].elements[ielement]) {
        this[type].elements[ielement].editlayer(i, values);
      } else {
        throw new Error('type ' + type + ' does not exist in profile');
      }
    }

    return this.adjust();
  };

  /**
   * Toggle whether to expose all layers of all features present
   * or just the ones above the ground, i. e. with a top value > 0.
   *
   * @method showsoil
   * @chainable
   *
   * @param {Boolean} enable true = expose all layers, false = hide soil layers
   */
  Profile.prototype.showsoil = function (enable) {

    this.features.forEach(function (name) {
      var featureset = this[name];
      featureset.elements.length && featureset.elements.forEach(function (feature) {
        feature.showsoil(enable);
      });
    }, this);

    return this.adjust();
  };

  /**
   * Whether or not the profile has the given `feature`.
   *
   * @method has
   * @param {String} feature
   *
   * @returns {Boolean}
   */
  Profile.prototype.has = function (feature) {
    return !!(Feature.type[feature] && this[feature]);
  };

  Profile.prototype.toJSON = function () {
    return pick(this, [ 'date', 'info' ].concat(this.features));
  };

  // --- Module Exports ---
  niviz.Profile = Profile;

}(niviz, moment));