API Docs for: 0.0.1
Show:

File: lib/graphs/simple.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, 18]*/
(function (niviz, moment) {
  'use strict';

  // --- Module Dependencies ---
  var Graph     = niviz.Graph;
  var Common    = niviz.Common;
  var Config    = niviz.Config;
  var Cartesian = niviz.Cartesian;
  var BarGraph  = niviz.BarGraph;
  var Value     = niviz.Value;
  var Axis      = niviz.Axis;
  var header    = niviz.Header;
  var t         = niviz.Translate.gettext;
  var arrows    = niviz.Visuals.Arrows;
  var hsgrid    = niviz.Grid.hsgrid;
  var extend    = niviz.util.extend;

  /** @module niviz */
  var lbl = ['F', '4F', '1F', 'P', 'K', 'I'];

  /**
   * Visualization of a singular snow profile in a simple way with arrows
   * indicating failed stability tests. A miniature version exists which
   * may be used in combination with a timeline graph.
   *
   * @class SimpleProfile
   * @constructor
   * @extends TabularProfile
   *
   * @param {Station} station A niViz station object
   * @param {Object} canvas A svg element that will be used as SnapSVG paper
   * @param {Object} properties
   */
  function SimpleProfile (station, canvas, properties) {
    SimpleProfile.uber.call(this, station, canvas, properties);

    extend(this.properties, SimpleProfile.defaults.values);
    extend(this.properties, properties);

    this.features = [];

    this.counter = undefined;
    this.subcounter = 0;
    this.draw(station.current || station.profiles[station.profiles.length - 1]);

    var self = this;
    station.emitter.on('profile', function (object, reconfigure) {
      self.pchange = !!reconfigure;
      self.draw(object, true);
    });

    station.emitter.on('parameter', function (object) {
      self.graph(object);
    });
  }

  Graph.implement(SimpleProfile, 'SimpleProfile', 'profile', 'TabularProfile');

  SimpleProfile.defaults = new Config('Simple Profile', [
    { name: 'fontsize',   type: 'number', default: 12, required: true },
    Common.head,
    Common.stability_parameters,
    Common.other_parameters,
    Common.show_arrows,
    { name: 'use_hhindex_as_primary_axis', type: 'boolean', default: false },
    { name: 'hide_hardness_profile', type: 'boolean', default: false },
    { name: 'monochrome_hardness_profile', type: 'boolean', default: false },
    { name: 'show_additional_parameters', type: 'boolean', default: true },
    { name: 'barparams', type: 'barparams', values: ['line', 'stairs'],
      default: [
        {name: 'temperature', style: 'line', color: 'red', left: -20, right: 0, log: false},
        {name: 'density', style: 'stairs', left: 1000, right: 0, color: 'purple', log: false},
        {name: 'ramm', style: 'stairs', left: 1000, right: 0, color: 'darkblue', log: false},
        {name: 'grainsize', style: 'line', left: 5, right: 0, color: 'maroon', log: false},
        {name: 'wetness', style: 'line', left: 20, right: 0, color: 'steelblue', log: false}
      ]
    }
  ]);

  SimpleProfile.defaults.load();

  /**
   * Remove a parameter from the barparams Config SimpleProfile.defaults. This method is
   * invoked by the settings modal when a user removes a parameter from the additional
   * parameters section.
   *
   * @method remove
   * @static
   * @param {String} name Paramter name (e. g. ramm, density)
   */
  SimpleProfile.remove = function (name) {
    var i, ii = SimpleProfile.defaults.barparams.length - 1, current;

    for (i = ii; i >= 0; --i) {
      current = SimpleProfile.defaults.barparams[i].name;
      if (current === name || current === '') SimpleProfile.defaults.barparams.splice(i, 1);
    }
  };

  /**
   * Deregister events
   * @method destroy
   */
  SimpleProfile.prototype.destroy = function () {
    this.station.emitter.off('profile');
    this.station.emitter.off('parameter');
  };

  /**
   * Overwrite current properties with the ones passed as parameter.
   *
   * @method setProperties
   * @param {Object} properties
   */
  SimpleProfile.prototype.setProperties = function (properties) {
    this.pchange = true; // Force bargraph reconfiguration
    extend(this.properties, SimpleProfile.defaults.values);
    extend(this.properties, properties);
    this.draw(this.profile);
  };

  /**
   * Populate the features array, which will hold all features that are
   * present in both the barparams array and the station's features array
   *
   * @method matchfeatures
   * @private
   */
  SimpleProfile.prototype.matchfeatures = function () {
    var p = this.properties, features = this.station.features, parameters = p.barparams, i;
    this.features.length = 0;

    parameters.forEach(function (parameter, i) {
      if (features.indexOf(parameter.name) > -1) {
        this.features.push({ name: parameter.name, index: i });
      }
    }, this);
  };

  /**
   * Set the variable this.counter to a parameter present in the profile by
   * trying to preserve the last selected parameter (e. g. grainsize) which
   * is buffered in the this.lastcounter variable.
   *
   * @method setcounter
   * @private
   */
  SimpleProfile.prototype.setcounter = function () {
    var p = this.properties, parameters = p.barparams, i;

    if (this.counter !== undefined) {
      this.lastcounter = this.counter;
    } else if (this.counter === undefined) {
      this.counter = this.lastcounter;
      p.show_additional_parameters = SimpleProfile.defaults.values.show_additional_parameters;
    }
    // Check if at least one parameter is available
    if (SimpleProfile.defaults.values.show_additional_parameters && !this.features.length) {
      p.show_additional_parameters = false;
    } else {
      if (this.counter === undefined) this.counter = 0;
    }
  };

  /**
   * Configure all properties and axes dependent on canvas size and whether
   * the graph shall be used in miniature or normal mode.
   *
   * @method configure
   * @private
   */
  SimpleProfile.prototype.configure = function () {
    var p = this.properties, hsmin, hsmax;

    if (this.hsgrid) {
      hsmin = this.hsgrid.min;
      hsmax = this.hsgrid.max;
    }

    p.height = this.canvas.height();
    p.width  = this.canvas.width();

    this.setcounter();
    this.left  = 0;

    if (p.miniature) {
      this.miniconfig();
      this.right = p.width - 4 * p.fontsize;
      this.cart = 100;
    } else if (p.table) {
      this.top = p.table.top;
      this.bottom = p.height - p.table.bottom;
      this.height = p.height - p.table.top - p.table.bottom;
      this.right = p.table.dock;
      this.cart = Math.max(p.table.dock / 4, 100);
    }

    if (!p.show_arrows.length) this.cart = 0;

    if (p.table && p.table.font)
      p.font = p.table.font;
    else p.font = {
      fontSize: p.fontsize + 'px',
      textAnchor: 'middle',
      fontFamily: 'LatoWeb, Helvetica, Arial',
      fill: p.font_color
    };

    if (!this.cartesian) this.cartesian = new Cartesian();

    if (p.inverted) {
      this.cartesian.addy('hs', this.hsgrid.min, this.hsgrid.max, this.top, this.bottom);
    } else {
      this.cartesian.addy('hs', this.hsgrid.min, this.hsgrid.max, this.bottom, this.top);
    }

    if (p.use_hhindex_as_primary_axis) {
      this.cartesian.addx('hardness', 0, 6,
                          this.right - this.cart, this.left + 2 * p.fontsize);
    } else {
      this.cartesian.addx('hardness', 0, p.hide_hardness_profile ? 1000 : 1050,
                          this.right - this.cart,
                          this.left + 2 * p.fontsize);
    }

    if (hsmin !== this.hsgrid.min || hsmax !== this.hsgrid.max) this.pchange = true;

    if (!this.bargraph || this.pchange) {
      this.pchange = false;
      this.setbarcfg();

      if (!this.bargraph) this.bargraph = new BarGraph(this.paper, this.barcfg);
      else this.bargraph.reconfigure(this.barcfg);
    }
  };

  /**
   * Configure bargraph properties.
   * @method setbarcfg
   * @private
   */
  SimpleProfile.prototype.setbarcfg = function () {
    var p = this.properties, callbacks = [], vf = valuefunc, primary = {
      lbl: t('Hand hardness index') + ' [N]', pos: 'top',
      min: 0, max: p.hide_hardness_profile ? 1000 : 1050, inc: 250, axis: 'hardness'
    };

    callbacks.push({ f: this.arrows, param: [p.show_arrows, 'arrows'] });
    //callbacks.push({ f: this.redarrow, param: ['critical', 'redarrow'] });
    if (!p.hide_hardness_profile) callbacks.push({ f: this.hhlabels, param: ['', ''] });
    callbacks.push({ f: this.adapt(), param: ['', ''] });
    callbacks.push({ f: this.printDate, param: [p.miniature, 'datelabel'] });

    if (p.use_hhindex_as_primary_axis) {
      vf = valuefunc_hh;
      primary = {
        lbl: t('Hand hardness index') + '', pos: 'top',
        min: 0, max: 6, inc: 1, axis: 'hardness'
      };
    }

    this.barcfg = {
      //inverted: true,
      font: p.font,
      fontsize: p.fontsize,
      grid_color: p.grid_color,
      monochrome: SimpleProfile.defaults.monochrome_hardness_profile || false,
      cartesian: this.cartesian,
      use_hhindex_as_primary_axis: !!p.use_hhindex_as_primary_axis,
      valuefunc: vf,
      primary: primary,
      hs : this.hsgrid,
      bars: {x: 'hardness', y: 'hs'},
      showdate: (p.miniature || p.head === 'none') ? false : false,
      top: p.table ? p.table.top : this.top,
      left: this.left,
      right: this.right,
      cart: this.cart, //px
      height: p.table ? p.height - p.table.top - p.table.bottom : this.height,
      //width: this.right - this.left + 6 * p.fontsize,
      hslabel: p.miniature ? 'right' : 'left',
      canvas: this.canvas,
      callbacks: callbacks,
      emitter: this.station.emitter,
      nobars: p.hide_hardness_profile,
      hardnessBottom: true
    };
  };

  /**
   * Configure parameters for miniature mode.
   * @method miniconfig
   * @private
   */
  SimpleProfile.prototype.miniconfig = function () {
    var p = this.properties;

    p.grid_color = '#000';//'#707070';
    p.fontsize = 14;

    this.top = 3 * p.fontsize;
    this.bottom = p.height - 3 * p.fontsize;
    this.height = p.height - 6 * p.fontsize;
    this.hsgrid = hsgrid(this.station, null, null, Common.defaults.autoscale);

    if (!this.paper) this.paper = Snap(this.canvasnode);
  };

  /**
   * In case the bargraph is zoomed or resized, the table and curves need
   * to be adapted to the area visible. The adapt method returns a callback
   * function that in turn calls all necessary functions of SimpleProfile and
   * TabularProfile.
   *
   * @method adapt
   * @private
   * @return {Function} A function to be called a
   */
  SimpleProfile.prototype.adapt = function () {
    var self = this;
    if (this.properties.miniature) return function () {
      self.graph();
    };

    return function () {
      self.graph();
      self.zoom.call(self, this.range);
    };
  };

  /**
   * In case the bargraph is in miniature mode print the date of the currently
   * profile in the top right corner.
   *
   * @method printDate
   * @private
   */
  SimpleProfile.prototype.printDate = function (miniature, name) {
    if (this.elements[name]) this.elements[name].remove();

    if (!miniature) return;

    var p = this.properties, left = this.properties.origin.x, right = this.properties.xright,
      set = this.paper.g(), x = left + (right - left) / 2, y =  p.origin.y + 7 + 1.8 * p.fontsize,
      text = this.profile && this.profile.date.format("MMM DD 'YY, HH:mm") || '';

    set.add(this.paper.text(left, y, text).attr(p.font).attr({textAnchor: 'start'}));

    this.elements[name] = set;
  };

  /**
   * Draws arrows for one or more parameters specified by calling the
   * Visuals:Arrows method.
   *
   * @method arrows
   * @private
   * @param {Array<String>} params All parameter that require arrow drawing
   * @param {String} name Name of the svg group to bundle the arrows
   */
  SimpleProfile.prototype.arrows = function (params, name) {
    if (this.elements[name]) this.elements[name].remove();

    var left = this.properties.origin.x, right = this.properties.xright, el,
      set = this.paper.g(), range = this.range, i = 0, ii = params.length,
      c = this.range.axis.pixel.bind(this.range.axis);

    for (; i < ii; ++i) {
      el = this.profile[params[i].name];
      if (el && el.layers && el.layers.length) {
        set.add(arrows(this, el, left, right, range.min, range.max, c));
        break;
      }
    }

    this.elements[name] = set;
  };

  SimpleProfile.prototype.redarrow = function (feature, type) {
    feature = this.profile[feature];
    if (this.elements[type]) this.elements[type].remove();

    if (!feature) return;

    var ii = 0, paper = this.paper, set = paper.g(), x = this.coord(0, 0).x, y;

    for ( ; ii < feature.layers.length; ++ii) {
      if (!feature.layers[ii] || !feature.layers[ii].value) continue;
      y = this.coord(0, feature.layers[ii].top - feature.layers[ii].thickness / 2).y;

      set.add(paper.line(x + 5, y, x + 100, y));
      set.add(paper.polygon(x + 3, y, x + 10, y - 5, x + 10, y + 5, x + 3, y));
    }

    set.attr({
      'fill' : '#D00',
      'stroke': '#D00',
      'arrow-end': 'block-wide-long',
      'stroke-width': 3
    });

    this.elements[type] = set;
  };

  /**
   * Draws the hand hardness index labels below the top abscissa.
   * @method hhlabels
   * @private
   */
  SimpleProfile.prototype.hhlabels = function () {
    if (this.elements['labels']) return;

    var ii, p = this.properties, top = p.top, paper = this.paper, set = paper.g(),
        lblpos = top + p.fontsize + 5, axis = paper.g();

    if (this.elements['labels']) this.elements['labels'].remove();

    for (ii = 0; ii < lbl.length - 1; ++ii) {
      var current = this.coord((new Value.Hardness(0, ii + 1)).newton, 0);
      axis.add(paper.line(Math.round(current.x), top, Math.round(current.x), top + 5));
      set.add(paper.text(current.x, lblpos, lbl[ii]).attr(p.font).attr({ opacity: 0.9 }));
    }

    set.add(axis.attr({ 'stroke': p.grid_color }).transform('t0.5,0.5'));
    this.elements['labels'] = set;
  };

  var valuefunc = function (value) {
    return value.newton;
  };

  var valuefunc_hh = function (value) {
    return value;
  };

  /**
   * Configure all properties related to the TabularProfile parent class.
   * This method overrides TabularProfile:config and is called by
   * TabularProfile:draw, TabularProfile:zoom and TabularProfile:arrange.
   *
   * @method config
   * @private
   */
  SimpleProfile.prototype.config = function () {
    var p = this.properties, station = this.station,
        oldheight = p.height, oldwidth = p.width, twidth;

    twidth = this.table().width;

    p.height = Math.round(this.canvas.height()); //container height
    p.width  = Math.round(this.canvas.width());  //container width

    if (!this.paper || p.height !== oldheight || p.width !== oldwidth) {
      if (this.paper) {
        this.paper.attr({ width: p.width + 'px', height: p.height + 'px' });
      } else {
        this.paper = Snap(this.canvasnode);
      }
    }

    p.table = {};
    this.hsgrid = hsgrid(station, null, null, Common.defaults.autoscale);
    p.table.max    = this.range ? this.range.max : this.hsgrid.max;
    p.table.min    = this.range ? this.range.min : this.hsgrid.min;
    p.table.range  = p.table.max - p.table.min;

    p.table.font = {
      fontSize: p.fontsize + 'px',
      fill: p.font_color,
      textAnchor: 'middle',
      fontFamily: 'Helvetica, Arial'
    };
    p.font = p.table.font;

    p.table.top    = 4 * p.fontsize + header.height(p, this.paper, this.profile,
                     2 * p.fontsize, p.width - 2 * p.fontsize - 1);
    p.table.bottom = (p.head === 'full') ? 4 * p.fontsize : 5 * p.fontsize;
    p.table.height = p.height - p.table.top - p.table.bottom;
    p.table.dock   = p.width - twidth - 100; // where the interconnector starts

    p.table.origin = {
      x: p.width - twidth - 1,
      y: p.table.top + p.table.height
    };

    p.table.labels = {
      symbol: p.table.top - 2 * p.fontsize - 5,
      unit: p.table.top - p.fontsize / 2 - 5
    };

  };

  /**
   * @method dblclickhandler
   * @param {Object} current
   * @private
   */
  SimpleProfile.prototype.dblclickhandler = function (current) {
    this.station.emitter.emit('layer', current);
  };

  /**
   * Set this.counter to the next valid additional parameter that a curve
   * or stair case graph shall be drawn for.
   * @method next
   * @private
   */
  SimpleProfile.prototype.next = function () {
    var cc = 0, parameters = this.properties.barparams;

    if (this.counter === undefined) return;

    var data = this.profile[this.features[this.counter].name];

    if (data && data.length > 1) {
      this.subcounter++;
      if (data.elements[this.subcounter]) return;
    }

    this.subcounter = 0;
    this.counter = (this.counter + 1) % this.features.length;
  };

  /**
   * If a parameter is active in the timeline graph, check whether
   * this parameter also exists as additional parameter in the
   * SimpleProfile.
   * Note: grainshape is mapped to temperature
   *
   * @method setParameter
   * @private
   * @param {String} param Name of parameter active
   */
  SimpleProfile.prototype.setParameter = function (param) {
    var cc = 0, name;
    if (!this.features.length) return;

    if (param === 'grainshape') param = 'temperature';

    do {
      name = this.features[cc].name;
      if (name === param) {
        this.counter = cc;
        this.subcounter = 0;
        break;
      }
      ++cc;
    } while (cc < this.features.length);
  };

  /**
   * Callback to graph the currently selected additional parameter. Calculates
   * the constraints and calls this.bargraph.curve.
   *
   * @method graph
   * @private
   * @param {String} param Name of parameter active
   */
  SimpleProfile.prototype.graph = function (parameter) {
    var self = this, p = this.properties, c, b = this.bargraph, data = null, meta = null;

    if (parameter) this.setParameter(parameter);
    if (!this.features[this.counter]) return;

    c = p.barparams[this.features[this.counter].index];
    meta = niviz.Feature.type[this.features[this.counter].name];

    if (!this.profile[c.name] || !this.profile[c.name].elements[this.subcounter]) {
      this.bargraph.clearcurve();
      data = {
        type: this.features[this.counter].name,
        name: meta.name,
        unit: meta.unit
      };
    } else {
      data = this.profile[c.name].elements[this.subcounter];
    }

    var max = c.right !== '' ?
        parseFloat(c.right) : Math.floor((data.max) / 10 + 1) * 10;
    var min = c.left !== '' ?
        parseFloat(c.left) : Math.ceil(data.min / 10 - 1) * 10;

    var primary = b.properties.primary,
        steps = Math.round(Math.abs(primary.max - primary.min) / primary.inc),
        left = b.properties.cartesian.px(primary.axis, steps * primary.inc + primary.min);

    var axis = new Axis(min, max, left, this.right - this.cart, c.log);

    var callback = function () {
      self.next();
      self.graph();
    };

    this.bargraph.curve(data, axis, c.color, callback, c.style === 'stairs', this.subcounter);
  };

  /**
   * Draw the SimpleProfile by drawing the bargraph.
   *
   * @method draw
   * @param {Profile} profile The niViz profile to render
   */
  SimpleProfile.prototype.draw = function (profile, renderaspng) {
    var p = this.properties;

    if (profile) this.profile = profile;
    this.matchfeatures();

    if (!p.miniature) { // draw TabularGraph AND the SimpleProfile
      SimpleProfile.uber.prototype.draw.call(this, this.profile);
    }

    this.configure();

    if (p.head !== 'none' && !p.miniature) {
      if (this.elements.header) this.elements.header.remove();
      this.elements.header =
        header.draw(this, 2 * p.fontsize,
                    p.table.origin.x + this.table().width - 2 * p.fontsize);
    }
    this.bargraph.draw(this.profile, renderaspng);
    this.graph();

    // This is a workaround, because the range may actually change during bargraph
    // drawing and this.y() in the TabularGraph depends on this.range:
    if (!p.miniature) { // draw TabularGraph AND the SimpleProfile
      SimpleProfile.uber.prototype.draw.call(this, this.profile);
    }
  };

  // --- Helpers ---

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

}(niviz, moment));