API Docs for: 0.0.1
Show:

File: lib/features.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) {
  'use strict';

  // --- Module Dependencies ---
  var properties    = Object.defineProperties;
  var keys          = Object.keys;
  var extend        = niviz.util.extend;
  var Value         = niviz.Value;
  var isUndefOrNull = niviz.util.isUndefinedOrNull;

  /** @module niviz */

  /**
   * A feature of a snow Profile.
   *
   * @class Profile
   * @constructor
   */
  function Feature(type, p) {

    /**
     * Reference to the profile, this is necessary because some features
     * need to have access to dynamic properties such as snow height
     *
     * @property profile
     * @type Profile
     */
    this.profile = p;

    /**
     * @property type
     * @type String
     */
    this.type = type;

    /**
     * The features value stack. Values are assumed
     * to be stacked from the bottom to top.
     *
     * @property layers
     * @type Array
     */
    this.layers = [];

    /**
     * Max numeric value. Managed by `.push`.
     *
     * @property max
     * @type Number
     */
    this.max = Number.NEGATIVE_INFINITY;

    /**
     * Min numeric value. Managed by `.push`.
     *
     * @property min
     * @type Number
     */
    this.min = Number.POSITIVE_INFINITY;

    /**
     * Observations and metadata regarding this profile, such as
     * comments, type of profile, method of measurement
     *
     * @property info
     * @type {Object}
     */
    this.info = {
      pointprofile: false
    };

    if (this.definition.info)
      extend(this.info, this.definition.info);

    this.$indexzero = null; // index of first layer above 0
    this.$showsoil = true;  // show all soil layers
  }

  properties(Feature, {
    /**
     * @property type
     * @type Object
     * @static
     */
    type: {
      value: {}
    },

    /**
     * @property types
     * @type Array<String>
     * @static
     */
    types: {
      get: function () { return keys(this.type); }
    }
  });

  /**
   * Registers a feature definition. A definition should
   * contain a `name`, a `symbol` and, optionally, a
   * `Constructor` function to create new values.
   *
   * @method register
   * @chainable
   * @static
   *
   * @param {String} type
   * @param {Object} definition
   */
  Feature.register = function (type, definition) {
    if (typeof definition.Constructor !== 'function')
      definition.Constructor = Value;

    Feature.type[type] = definition;

    return this;
  };

  properties(Feature.prototype, {
    /**
     * @property definition
     * @type Object
     */
    definition: {
      get: function () { return Feature.type[this.type]; }
    },

    /**
     * @property name
     * @type String
     */
    name: {
      get: function () { return this.definition.name; }
    },

    /**
     * @property symbol
     * @type String
     */
    symbol: {
      get: function () { return this.definition.symbol; }
    },

    /**
     * @property symbol
     * @type String
     */
    unit: {
      get: function () { return this.definition.unit; }
    },

    /**
     * @property top
     * @type Number
     */
    top: {
      get: function () {
        return this.layers.length ? this.layers[this.layers.length - 1].top : 0;
      }
    },

    /**
     * @property bottom
     * @type Number
     */
    bottom: {
      get: function () {
        if (this.layers.length) {
          if (this.layers[0].bottom !== undefined)
            return this.layers[0].bottom;

          return this.layers[0].top;
        }

        return 0;
      }
    },

    /**
     * @property height
     * @type Number
     */
    height: {
      get: function () {
        if (this.type === 'smp' ||  this.type === 'ramm') //depthTop types
          return this.layers.length && this.layers[0].depth || 0;

        return this.top - this.bottom;
      }
    },

    /**
     * All relevant metadata in one object
     *
     * @property meta
     * @type Object
     */
    meta: {
      get: function () {
        var obj  = JSON.parse(JSON.stringify(this.info)); // deep copy
        return obj;
      },
      set: function (obj) {
        if (!obj) return;
        this.info = JSON.parse(JSON.stringify(obj)) || {};

        if (this.info.pointprofile === undefined)
          this.info.pointprofile = false;
      }
    }

  });

  /**
   * Delete a certain layer
   *
   * @method rmlayer
   * @chainable
   * @param {Number} i layer index
   */
  Feature.prototype.rmlayer = function (i) {
    if (i >= this.layers.length)
      throw new Error('index out of bounds');

    this.layers.splice(i, 1);

    return this;
  };

  /**
   * Edit a certain layer
   *
   * @method editlayer
   * @chainable
   * @param {Number} i layer index
   * @param {Object} values layer data
   */
  Feature.prototype.editlayer = function (i, values) {
    var current = this.layers[i] || null;

    if (current) {
      if (values.top !== undefined) current.top = values.top;
      if (values.bottom !== undefined) current.bottom = values.bottom;

      Object.keys(values).forEach(function (key) {
        if (key !== 'index' && key !== 'top' && key !== 'bottom')
          current[key] = values[key];
      });

      this.sort();
    } else {
      throw new Error('invalid layer selected for editing');
    }

    return this;
  };

  /**
   * @method layerheight
   * @return {Number} The sum of all layer heights
   */
  Feature.prototype.layerheight = function () {
    if (!this.layers.length) return 0;

    return this.layers.reduce(function (acc, cur) {
      return acc + cur.thickness;
    }, 0);
  };

  /**
   * Calculate average of the values of all layers, weighting according to layer thickness.
   *
   * @method average
   * @return {Number} The average value over all layers or undefined if not available
   */
  Feature.prototype.average = function () {
    var sum = 0, height = this.height;

    if (this.layers.length) {
      this.layers.forEach(function (current) {
        sum += (current.top - current.bottom) / height * current.value;
      });

      return sum;
    }

    return undefined;
  };

  /**
   * Retrieve value at certain depth/height of snowpack layer
   *
   * @method atheight
   * @param {Number} [height] depth or height of the desired layer
   * @param {Boolean} [fromtop] true if height is to be interpreted as depth
   * @return {Number} The value of the desired layer or null
   */
  Feature.prototype.atheight = function (height, fromtop) {
    var i, ii = this.layers && this.layers.length, layer;

    if (ii) {
      for (i = 0; i < ii; ++i) {
        layer = this.layers[i];

        if (fromtop) {
          var depthbottom = this.top - layer.bottom, depthtop = this.top - layer.top;

          if (depthbottom >= height && depthtop <= height)
            return layer.avg || layer.value || null;

        } else {

          if (layer.bottom <= height && layer.top >= height)
            return layer.avg || layer.value || null;
        }
      }
    }

    return null;
  };

  /**
   * Adds a new layer value.
   *
   * Note: This method assumes that layers are pushed in no particular order;
   * it used to assume that layers are pushed either from top to bottom,
   * or bottom to top!
   *
   * @method push
   * @chainable
   *
   * @param {Number,Object,Array} top or top/value/bottom object
   * @param {Object} [value]
   * @param {Number} [bottom]
   */
  Feature.prototype.push = function (top, value, bottom) {
    var i, ii;

    // --- Normalize Input Arguments
    if (arguments.length < 2 && typeof top === 'object' && !isUndefOrNull(top)) {

      if (Array.isArray(top)) {
        if (top.length === 3) bottom = top[2];
        value = top[1];
        top   = top[0];
      } else {
        bottom = top.bottom;
        value  = top.value;
        top    = top.top;
      }
    }

    value = new this.definition.Constructor(top, value, bottom, this.profile);
    var num = value.numeric;

    if (num > this.max) this.max = num;
    if (num < this.min) this.min = num;

    if (this.$indexzero === null && top > 0) this.$indexzero = this.layers.length;

    if (value.top >= this.top || value.top === undefined) { // undefined possible (RB, ECT)
      this.layers.push(value);
    } else if (value.top <= this.bottom) {
      this.layers.unshift(value);
    } else { // look for insertion position, HACK: implement binary insert
      var inserted = false;
      for (i = 0, ii = this.layers.length; i < ii; i++) {
        if (value.top <= this.layers[i].top) {
          this.layers.splice(i, 0, value);
          inserted = true;
          break;
        }
      }

      // layer has not been inserted (e. g. comparing undefined top values):
      if (!inserted) this.layers.push(value);
    }

    //this.layers[(value.top < this.top) ? 'unshift' : 'push'](value);

    return this;
  };

  /**
   * Sorts the layers according to top
   *
   * @method sort
   * @chainable
   *
   */
  Feature.prototype.sort = function () {
    this.layers.sort(function (a, b) {
      if (a.top === b.top && ((a.bottom || a.bottom === 0) && (b.bottom || b.bottom === 0)))
        return a.bottom - b.bottom;

      return a.top - b.top;
    });

    return this;
  };

  /**
   * Toggle whether to expose all layers 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
   */
  Feature.prototype.showsoil = function (enable) {
    var i, ii = this.layers.length;

    if (this.$showsoil && !enable) {
      this.$layers = this.layers; // buffer reference
      this.layers = [];

      if (this.$indexzero !== null)
        for (i = this.$indexzero; i < ii; ++i)
          this.layers.push(this.$layers[i]);
    } else if (!this.$showsoil && enable) {
      this.layers = this.$layers;
      delete this.$layers;
    }

    this.$showsoil = enable;
    return this;
  };

  /**
   * Adds a list of layer values.
   *
   * @method concat
   * @chainable
   *
   * @param {Array} values
   */
  Feature.prototype.concat = function (values) {
    for (var i = 0, ii = values.length; i < ii; ++i)
      this.push.call(this, values[i]);

    return this;
  };

  /**
   * Find layer closest to height passed in cm
   *
   * @method index
   *
   * @param {float} top in cm
   * @return {int} Index of closest layer
   */
  Feature.prototype.index = function (top) {
    var i, ii, diff, min, mini;

    if (top === undefined) return mini;

    for (i = 0, ii = this.layers.length; i < ii; ++i)  {
      diff = Math.abs(this.layers[i].top - top);

      if (diff <= min || min === undefined) {
        min = diff;
        mini = i;
      }
      if (mini !== i) break; // diff is becoming bigger
    }

    return mini;
  };

  Feature.prototype.toJSON = function () {
    return { meta: this.meta, layers: this.layers };
  };

  // --- Feature Definitions ---

  Feature
    .register('grainshape', {
      name: 'Grain shape', symbol: 'F', Constructor: Value.GrainShape
    })

    .register('grainsize', {
      name: 'Grain size', symbol: 'E', Constructor: Value.GrainSize, unit: 'mm'
    })

    .register('density', {
      name: 'Snow density', symbol: 'ρ', unit: 'kg/m\u00b3',
      info: { method: 'other' }
    })

    .register('gradient', {
      name: 'Temperature gradient', symbol: '\u2207T', unit: '°C/m'
    })

    .register('hardness', {
      name: 'Snow hardness', symbol: 'R', Constructor: Value.Hardness, unit: 'N'
    })

    .register('ramm', {
      name: 'Rammsonde', symbol: 'Rammsonde', Constructor: Value.Ramm, unit: 'N'
    })

    .register('wetness', {
      name: 'Liquid water content', symbol: '\u03B8', Constructor: Value.Wetness, unit: '%',
      info: { method: 'other' }
    })

    .register('ssa', {
      name: 'Specific surface area', symbol: 'SSA', unit: 'm\u00b2/kg',
      info: { method: 'other' }
    })

    .register('sk38', {
      name: 'Stability Class (Snowpack)', symbol: 'SK38',
      Constructor: Value.SK38
    })

    .register('ct', {
      name: 'Compression Test', symbol: 'CT',
      Constructor: Value.CT
    })

    .register('ect', {
      name: 'Extended Column Test', symbol: 'ECT', Constructor: Value.ECT
    })

    .register('rb', {
      name: 'Rutschblock', symbol: 'RB', Constructor: Value.Rutschblock
    })

    .register('sf', {
      name: 'Shear Frame Test', symbol: 'SF', Constructor: Value.ShearFrame
    })

    .register('saw', {
      name: 'Propagation Saw Test', symbol: 'Saw', Constructor: Value.Saw
    })

    .register('threads', {
      name: 'Threads', symbol: 'Threads'
    })

    .register('temperature', {
      name: 'Snow temperature', symbol: 'Ts', unit: '°C',
      info: { pointprofile: true }
    })

    .register('smp', {
      name: 'SnowMicroPen', symbol: 'SMP', unit: 'N', Constructor: Value.SMP,
      info: { pointprofile: true }
    })

    .register('impurity', {
      name: 'Impurity', symbol: 'IMP', unit: '%',
      info: { method: 'other', impuritytype: 'Isotopes', fractiontype: 'massFraction' }
    })

    .register('thickness', {
      name: 'Layer thickness', symbol: '\u2195', unit: 'cm'
    })

    .register('dendricity', {
      name: 'Dendricity', symbol: 'Dendricity'
    })

    .register('comments', {
      name: 'Comment', symbol: 'Comments'
    })

    .register('sphericity', {
      name: 'Sphericity', symbol: 'Sphericity'
    })

    .register('bondsize', {
      name: 'Bondsize', symbol: 'Bondsize'
    })

    .register('flags', {
      name: 'Flags', symbol: 'Flags'
    })

    .register('cn', {
      name: 'Coordination number', symbol: 'CN'
    })

    .register('critical', {
      name: 'Critical Layer', symbol: '-'
    })

    .register('hardnessBottom', {
      name: 'Snow hardness', symbol: 'R', Constructor: Value.Hardness, unit: 'N'
    });


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

}(niviz));