API Docs for: 0.0.1
Show:

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

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

  /** @module niviz */

  /**
   * Visualization of a singular snow profile SLF style.
   *
   * @class SLFProfile
   * @constructor
   *
   * @extends TabularProfile
   */
  function SLFProfile (station, canvas, properties) {
    SLFProfile.uber.call(this, station, canvas, properties);
    this.properties = extend(this.properties, SLFProfile.defaults.values);
    extend(this.properties, properties);

    // make sure that the children retain the right to call draw themselves
    if (!(this instanceof niviz.StructureProfile)) {
      this.draw(station.current || station.profiles[0]);
    }
  }

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

  SLFProfile.defaults = new Config('SLF Profile', [
    { name: 'fontsize',       type: 'number', default: 12,        required: true },
    { name: 'font_color',     type: 'color',  default: '#000000', required: true },
    { name: 'grid_color',     type: 'color',  default: '#000000', required: true },
    { name: 'ramm_color',     type: 'color',  default: '#0000FF', required: true },
    { name: 'hardness_color', type: 'color',  default: '#707070', required: true },
    Common.head,
    {
      name: 'max_snowheight', type: 'number', default: '',
      hint: 'max_snowheight_hint'
    },
    {
      name: 'min_temperature', type: 'number', default: '',
      hint: 'max_snowheight_hint'
    },
    Common.stability_parameters,
    Common.other_parameters,
    Common.show_arrows,
    { name: 'show_comment', type: 'boolean', default: true },
    { name: 'show_layer_lines', type: 'boolean', default: false },
    { name: 'print_optimized', type: 'boolean', default: false }
  ]);

  SLFProfile.defaults.load();

  SLFProfile.newton = 1000;

  /**
   * Deregister events
   * @method destroy
   */
  SLFProfile.prototype.destroy = function () {};

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

  /**
   * Configure basic properties of the SLFProfile and the parent class TabularProfile
   * such as font, table margins, height and width.
   *
   * @method config
   * @private
   */
  SLFProfile.prototype.config = function () {
    var p = this.properties, station = this.station,
      oldheight = p.height, oldwidth = p.width;

    p.height = this.canvas.height();
    p.width  = this.canvas.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);
      }
    }

    this.paper.clear();

    var max = Math.max(Number(SLFProfile.defaults.max_snowheight), 0);
    if (isNaN(max) || max === 0) {
      if (station.top === 0) {
        max = 20;
        if (this.profile.info.ta === 0 || this.profile.info.ta) max += 30;
      } else {
        max = Math.max(Math.floor((station.top + 90) / 50) * 50, 200);
      }
    }
    p.margin =  p.fontsize * 2;

    p.table = {};
    p.table.max    = max;
    p.table.min    = station.bottom ? Math.ceil((station.bottom - 20) / 20) * 20 : 0;
    p.table.range  = p.table.max - p.table.min;

    p.table.dock   = 550; // where the interconnector starts

    p.table.origin = {
      x: p.table.dock + p.fontsize * 4
    };

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

    p.font  = p.table.font;

    this.canvas.width(Math.round(p.table.origin.x + this.table().width + 1));
    p.width  = this.canvas.width();

    p.table.top    = 4 * p.fontsize +
      header.height(p, this.paper, this.profile, p.margin, p.width - p.margin - 1);

    p.table.origin.y = p.table.top;
    p.table.labels = {
      symbol: p.table.top - p.fontsize * 2.5,
      unit: p.table.top - 7 - p.fontsize / 2
    };

    if (p.print_optimized)
      this.canvas.height(this.canvas.width() * Math.sqrt(2));
    //p.height = Math.round(this.canvas.width() * Math.sqrt(2));

    p.height = this.canvas.height();
    this.paper.attr({ width: p.width + 'px', height: p.height + 'px' });

    p.table.bottom = 4 * p.fontsize;
    p.table.height = p.height - p.table.top - p.table.bottom;

    p.table.origin.y = p.table.top + p.table.height;

    p.origin = {
      x: p.table.dock - 100,
      y: p.table.top + p.table.height
    };

    p.left  = { 'text-anchor': 'start' };
    p.right = { 'text-anchor': 'end' };
  };

  /**
   * Configure the axes for hs, hardness, temperature and the gradient parameters.
   *
   * @method setup
   * @private
   */
  SLFProfile.prototype.setup = function () {
    var p = this.properties, profile = this.profile,
      xright = p.origin.x, xleft = p.margin + 10;

    this.cartesian = new Cartesian();

    var cartesian = this.cartesian;

    cartesian.addy('hs', p.table.min, p.table.max, p.origin.y, p.table.top);
    cartesian.addx('hardness', 0, SLFProfile.newton, xright, xleft);

    if (profile.ramm) cartesian.addx('ramm', 0, SLFProfile.newton, xright, xleft);
    if (profile.temperature) {
      var min = Number(SLFProfile.defaults.min_temperature);
      if (isNaN(min) || min >= 0) {
        min = Math.ceil(this.profile.temperature.min / 10 - 1) * 10;
        min = Math.min(min, -20);
      }
      cartesian.addx('temperature', min, 0, xleft, xright);

      if (profile.gradient) {
        var max = Math.max(Math.abs(profile.gradient.min), Math.abs(profile.gradient.max));
        max = Math.floor( max / 50 + 1) * 50;
        cartesian.addx('gradient', -max, max, xleft, xright); //symmetrical
      }
    } else {
      cartesian.addx('temperature', -20, 0, xleft, xright);
    }

    p.cliprect = [p.margin, p.table.top, p.origin.x - p.margin, p.origin.y - p.table.top];
  };

  /**
   * Draw the comment box on the graph.
   * @method comment
   * @private
   */
  SLFProfile.prototype.comment = function () {
    var p = this.properties, paper = this.paper, profile = this.profile;

    if (!profile.info.slf || !profile.info.slf.txt) return;

    var tmp = '', set = paper.g(), box = paper.text(0, 0, '').attr(p.font).attr(p.left);

    tmp = this.print('default', '', box, tmp);
    tmp = this.print('loc', t('Location', 'comment') + ': ', box, tmp);
    tmp = this.print('profile', t('Profile', 'comment') + ': ', box, tmp);
    tmp = this.print('rb', t('Rutschblock', 'comment') + ': ', box, tmp);
    tmp = this.print('ss', t('Snow surface', 'comment') + ': ', box, tmp);
    tmp = this.print('drift', t('Snowdrift', 'comment') + ': ', box, tmp);
    tmp = this.print('signals', t('Alarm signals', 'comment') + ': ', box, tmp);
    tmp = this.print('avalanches', t('Avalanches', 'comment') + ': ', box, tmp);
    tmp = this.print('danger', t('Danger level', 'comment') + ': ', box, tmp);
    tmp = this.print('remarks', t('Remarks', 'comment') + ':', box, tmp);
    tmp = this.print('confidential', t('Confidential remarks', 'comment') + ': ', box, tmp);

    tmp = this.testprint(box, tmp);
    box.remove(); // remove the dummy textbox

    if (tmp.length !== 0) {
      box = paper.multitext(p.margin + 10, p.table.top, tmp).attr(p.font).attr(p.left);
      box.transform('t0,' + (p.table.top - box.getBBox().y + 3 * p.fontsize));
      var width = box.getBBox().width + 10, height = box.getBBox().height + 10;
      set.add(paper.rect(box.getBBox().x - 5, box.getBBox().y - 5, width, height).attr({
        'fill': '#FFF',
        'stroke': '#000'
      }).transform('t0.5,0.5'));

      box.node.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
      set.add(box);
      this.elements['comment'] = set;
    }
  };

  /**
   * Check if the passed stability test is ordered and if so return
   * the ordered sequence.
   *
   * @method testorder
   * @private
   * @param {Feature} test CT or ECT
   * @return {Array<Number>} The sequence of tests
   */
  SLFProfile.prototype.testorder = function (test) {
    var i, ii, order = [], ordered = true, sequence = [];

    for (i = test.layers.length - 1; i >= 0; --i) {
      if (test.layers[i].order === undefined) {
        ordered = false;
      } else {
        if (sequence.indexOf(test.layers[i].order) === -1) {
          for (ii = 0; ii < sequence.length; ++ii) {
            if (test.layers[i].order < sequence[ii]) break;
          }
          sequence.splice(ii, 0, test.layers[i].order);
        }
      }
    }

    if (ordered) {
      for (ii = 0; ii < sequence.length; ++ii) {
        for (i = test.layers.length - 1; i >= 0; --i)
          if (test.layers[i].order === sequence[ii]) order.push(i);
      }
    } else {
      for (i = test.layers.length - 1; i >= 0; --i) order.push(i);
    }

    return order;
  };

  /**
   * Print the stability test ECT and CT (comment section).
   *
   * @method testprint
   * @private
   * @param {Object} box svg text element
   * @param {String} current The current comment string
   * @return {String} The altered comment string
   */
  SLFProfile.prototype.testprint = function (box, current) {
    var tests = ['ct', 'ect'], i, flag = false, first = true;

    for (i = 0; i < tests.length; ++i) {
      var test = this.profile[tests[i]], ii;

      if (test && test.layers && test.layers.length) {
        var order = -1, sequence = this.testorder(test);

        for (ii = 0; ii < sequence.length; ++ii) {
          var layer = test.layers[sequence[ii]], h = layer.top ? round(layer.top, 2) : '';

          if (ii === 0 && !flag) {
            current = this.format(box, current, '\n');
            current = this.format(box, current, '\n');
            current = this.format(box, current,
                                  t('Stability tests') + ':\n"""""""""""""""""""""\n');
            flag = true;
          }

          if (layer.order !== undefined && layer.order !== '' && order !== layer.order) {
            order = layer.order;
            if (!first) current = this.format(box, current, '\n');
            current += tests[i].toUpperCase() + ' #' + (layer.order + 1) + ':\n';
            first = false;
          }

          if (test.type === 'ct') {
            current = this.format(box, current,
                      layer.value + '@' + h + 'cm ' + (layer.comment || '') + '\n');
          } else if (test.type === 'ect') {
            var height = layer.top ? '@' + h + 'cm\n' : ' \n', text = layer.text;
            if (Common.defaults.ECT_format === 'Swiss') text = layer.swiss || layer.text;
            current = this.format(box, current, text + height);
          }
        }
      }
    }

    return current;
  };

  /**
   * Print the stability test ECT and CT (comment section).
   *
   * @method print
   * @private
   * @param {String} param Name of string within the txt object
   * @param {String} lbl The label for the current text block
   * @param {Object} box svg text element
   * @param {String} current The current comment string
   * @return {String} The altered comment string
   */
  SLFProfile.prototype.print = function (param, lbl, box, current) {
    var text = this.profile.info.slf.txt[param];
    if (!text) return current;

    if (current.length) current += '\n \n';
    if (lbl) current = current + lbl + '\n';
    current = this.format(box, current, text);
    box.attr('text', current);

    return current;
  };

  /**
   * Actually add the addendum string by making sure it doesn't transgress the
   * boundaries of the textbox.
   *
   * @method format
   * @private
   * @param {Object} textbox svg text element
   * @param {String} current The current comment string
   * @param {String} addendum Text string to be added
   * @return {String} The altered comment string
   */
  SLFProfile.prototype.format = function (textbox, current, addendum) {
    addendum += '';
    var words = addendum.split(/[ ]/) || [], ii, last = current.lastIndexOf('\n'), index;
    if (last) last = current.substring(last);
    else last = '';

    for (ii = 0; ii < words.length; ++ii) {
      last = last + (ii === 0 ? '' : ' ') + words[ii];
      textbox.attr('text', last);

      if (textbox.getBBox().width > 250) {
        current += '\n' + words[ii];
        last = '';
      } else {
        if (words[ii] === '\n') {
          current += '\u200C\n';
          last = '';
        } else if (words[ii] === '') {
          current = current;
        } else {
          current += (ii === 0 ? '' : ' ') + words[ii];

          index = words[ii].lastIndexOf('\n');
          if (index > -1) last = words[ii].substring(index);
        }
      }
    }

    return current;
  };

  /**
   * Draw the coordinate system
   * @method coordsys
   * @private
   */
  SLFProfile.prototype.coordsys = function () {
    var cartesian = this.cartesian, p = this.properties, paper = this.paper,
      ii, set = paper.g(), profile = this.profile;

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

    var path = paper.g(), c1 = cartesian.pixel('hardness', 'hs', 0, p.table.min),
      c2 = cartesian.pixel('hardness', 'hs', SLFProfile.newton, p.table.max),
      legend = paper.g(), hardness = paper.g(), increment = 10, x, y;

    for (ii = p.table.min; ii <= p.table.max; ii += increment) {
      y = Math.round(cartesian.py('hs', ii));
      if (ii > p.table.min && ii < p.table.max)
        path.add(paper.line(p.table.dock - 5, y, p.margin + .5, y));

      set.add(paper.text(p.table.dock + 5, y, ii).attr(p.font).attr(p.left));
      legend.add(paper.line(p.table.dock + .5, Math.round(y), p.table.dock - 5, Math.round(y)));
    }

    increment = 100;
    for (ii = 0; ii <= SLFProfile.newton; ii += increment) {
      x = Math.round(cartesian.px('hardness', ii));

      if (ii && ii <= SLFProfile.newton)
        path.add(paper.line(x, c2.y + .5, x, c1.y - .5));

      legend.add(paper.line(x, p.origin.y + .5, x, p.origin.y + 5));
      legend.add(paper.line(x, p.table.top - .5, x, p.table.top - 5));

      set.add(paper.text(x, p.table.top - 10, ii + '').attr(p.font));
    }

    set.add(path.attr({ stroke: '#bdbdbd' }).transform('t0.5,0.5'));
    set.add(legend.attr({ stroke: '#000' }).transform('t0.5,0.5'));

    path = ['M' + c1.x, c1.y, 'V' + c2.y, 'H' + p.margin, 'V' + c1.y, 'H' + p.table.dock];
    path.push(['V' + c2.y, 'H' + c1.x]);

    set.add(paper.path(path.join(',')).attr({
      fill: 'none',
      stroke: '#000'
    }).transform('t0.5,0.5'));

    for (ii = 0; ii < 5; ++ii) {
      x = Math.round(cartesian.px('hardness', hhardness[ii].value));
      hardness.add(paper.line(x, p.table.top + .5, x, p.table.top + 5));
      set.add(paper.text(x, p.table.top + 5 + p.fontsize, hhardness[ii].text).attr(p.font));
    }

    set.add(hardness.attr({ stroke: '#000' }).transform('t0.5,0.5'));

    set.add(paper.text(p.margin / 2, p.table.top + p.table.height / 2,
                        t('Snow height') + ' [cm]').attr(p.font).transform('r270'));

    set.add(paper.text(p.margin + (p.origin.x - p.margin) / 2,
                        p.table.labels.symbol, t('Ram resistance') + ' [N]')
             .attr(p.font).attr({fill: 'blue'}));

    if (profile.hs === 0 || profile.height < profile.hs) { // green bar
      y = cartesian.py('hs', profile.bottom);
      set.add(paper.rect(p.margin - 10, y, p.origin.x - p.margin + 20, 20).
               attr({stroke: 'none', fill: '#060', opacity: 0.4}));
    }

    this.draw_lower_legend('temperature');
    this.elements['coordsys'] = set;
  };

  /**
   * Draw legend for the curve (temperature/gradient) at the bottom of the graph.
   *
   * @method draw_lower_legend
   * @private
   * @param {String} parameter The profile parameter (e. g. temperature, gradient)
   */
  SLFProfile.prototype.draw_lower_legend = function (parameter) {
    var cartesian = this.cartesian, p = this.properties, paper = this.paper,
      ii, set = paper.g(), profile = this.profile, increment, self = this;

    if (!profile[parameter]) return;
    if (this.elements['bottom']) this.elements['bottom'].remove();

    increment = 100;
    for (ii = 0; ii <= SLFProfile.newton; ii += increment) {
      var x = cartesian.px('hardness', ii);

      set.add(paper.text(x, p.origin.y + 5 + p.fontsize,
               round(cartesian.coordx(parameter, x), 2) + '').attr(p.font));
    }

    set.add(paper.text(p.margin + (p.origin.x - p.margin) / 2,
      p.origin.y + 5 + 2.5 * p.fontsize,
      t(profile[parameter].name) + ' [' + profile[parameter].unit + ']')
      .attr(p.font).attr({fill: 'red', cursor: 'pointer'}));

    set.click(function (){
      self.toggle();
    });

    this.elements['bottom'] = set;
  };

  /**
   * Toggle between gradient and temperature curve.
   * @method toggle
   * @private
   */
  SLFProfile.prototype.toggle = function () {
    if (this.showgrad === undefined) this.showgrad = false;
    this.showgrad = !this.showgrad;

    var parameter = this.showgrad ? 'gradient' : 'temperature';

    this.draw_lower_legend(parameter);
    this.draw_curve(parameter);
  };

  /**
   * Draw a parameter of the profile as a curve.
   *
   * @method draw_curve
   * @private
   * @param {String} parameter The profile parameter (e. g. temperature, gradient)
   */
  SLFProfile.prototype.draw_curve = function (parameter) {
    var profile = this.profile, data = profile[parameter], old, path = [],
      paper = this.paper, ii, cartesian = this.cartesian, set = paper.g(),
      p = this.properties;

    if (this.elements['curve']) this.elements['curve'].remove();
    if (!data || !data.layers) return;

    this.temperatures = [];
    for (ii = 0; ii < data.layers.length; ++ii) {
      var current = data.layers[ii];
      var c = cartesian.pixel(parameter, 'hs', current.value, current.top);

      set.add(paper.circle(c.x, c.y, 2).attr({
        stroke: 'red', fill: 'red', strokeWidth: 1
      }));

      this.temperatures.push({'c': c, 't': current.value});

      if (ii > 0) path.push(old.x, old.y, c.x, c.y);

      old = c;
    }

    if (profile.info.ta !== undefined && profile.info.ta !== '' && parameter === 'temperature') {
      var d = 4;
      c = cartesian.pixel('temperature', 'hs', profile.info.ta, profile.top + 40);

      var ta = paper.line(old.x, old.y, c.x, c.y);
      set.add(ta.attr({ stroke: 'red', strokeDasharray: '4,4' }));

      var cross = paper.g();
      cross.add(paper.line(c.x, c.y - d, c.x, c.y + d));
      cross.add(paper.line(c.x - d, c.y, c.x + d, c.y));
      set.add(cross.attr({ stroke: 'red' }));

      this.temperatures.push({'c': c, 't': profile.ta});
      if (profile.info.ta > 0) this.temperatures.hover = { tr: c, bl: old, ta: profile.ta };
    }

    set.add(paper.polyline(path.join(',')).attr({ stroke: 'red', fill: 'none' }));

    set.attr({
      clip: paper.rect(p.cliprect[0], p.cliprect[1], p.cliprect[2], p.cliprect[3])
    });
    this.elements['curve'] = set;
  };

  /**
   * Draw a parameter of the profile as a staircase graph.
   *
   * @method draw_block
   * @private
   * @param {String} parameter The profile parameter (e. g. ramm)
   * @param {String} color
   * @param {String} name The name of the value of the parameter to be drawn (e. g. value)
   */
  SLFProfile.prototype.draw_block = function (parameter, color, name) {
    var cartesian = this.cartesian, paper = this.paper, p = this.properties,
      profile = this.profile, data = profile[parameter], ii;

    if (this.elements[parameter]) this.elements[parameter].remove();
    if (!data || !data.layers) return;

    var initial = data.layers &&
      data.layers[0].bottom !== undefined ? data.layers[0].bottom : profile.bottom;

    var old = cartesian.pixel(parameter, 'hs', 0, initial), path = [old.x, old.y], c;

    for (ii = 0; ii < data.layers.length; ++ii) {
      var current = data.layers[ii];
      c = cartesian.pixel(parameter, 'hs', current[name], current.top);
      if (data.layers[ii].bottom !== undefined &&
          data.layers[ii].bottom === data.layers[ii].top) continue;

      path.push(c.x, old.y, c.x, c.y);

      old = c;
    }
    old = cartesian.pixel(parameter, 'hs', 0, initial);
    path.push(old.x, c.y, old.x, old.y);

    this.elements[parameter] = paper.polygon(path).attr({
      'stroke': color,
      'stroke-width': 2,
      'fill': paper.hatching(color, (parameter === 'ramm' ? 0 : 90)),
      'fill-opacity': 0.7,
      clip: paper.rect(p.cliprect[0], p.cliprect[1], p.cliprect[2], p.cliprect[3])
    });
  };

  /**
   * Display the snow height label.
   *
   * @method hs
   * @protected
   * @param {Number} snowheight
   */
  SLFProfile.prototype.hs = function (snowheight) {
    var element = this.elements['hs'], text = snowheight ? round(snowheight, 3) + ' cm' : '';

    if (element) {
      if (!snowheight) text = '';
      element.attr('text', text);
    } else {
      var p = this.properties, center = (p.table.dock - p.origin.x) / 2 + p.origin.x,
        label = this.paper.text(center, p.table.labels.symbol, text).attr(p.font);

      this.elements['hs'] = label;
    }
  };

  /**
   * Method to be called when mousemove event is detected. It calls the method
   * SLFProfile:show in turn and deals with the comment box.
   *
   * @method mousemove
   * @private
   * @return {Boolean} on Turn mouse events on
   */
  SLFProfile.prototype.mousemove = function (on) {
    var self = this, paper = this.paper, cartesian = this.cartesian,
      p = this.properties;

    var c1 = cartesian.pixel('hardness', 'hs', 0, p.table.min);
    var c2 = cartesian.pixel('hardness', 'hs', SLFProfile.newton, p.table.max);

    if (this.overlay) this.overlay.remove();
    if (on) {
      this.overlay = paper.rect(c2.x - 5, c2.y - 5, c1.x - c2.x + 15, c1.y - c2.y + 20)
        .attr({ 'opacity': 0.00, 'fill': '#000' })
        .click(function () {
          if (!self.hiddencomment && self.elements['comment']) {
            self.elements['comment']
              .animate({ opacity : 0 }, 500, function () { this.attr('display', 'none'); });
            self.hiddencomment = true;
          } else if (self.hiddencomment && self.elements['comment']) {
            self.elements['comment'].attr('display', '').animate({ opacity : 1 }, 500);
            self.hiddencomment = false;
          }
        });

      this.overlay.mousemove(function (e) {
        clearTimeout(this.overlaytimer);
        this.overlaytimer = setTimeout( function () {
          var parentOffset = self.canvas.offset();
          var y = e.pageY - parentOffset.top;
          var x = e.pageX - parentOffset.left;
          self.show(x, y);
        }, 10);
      });
    }
  };

  /**
   * Build the popup for the temperature / gradient curve.
   *
   * @method buildpopup
   * @private
   * @param {Object} paper SnapSVG paper object
   * @param {Object} p Properties object
   * @param {String} text Popup text
   * @param {Number} x Popup position x-coordinate
   * @param {Number} y Popup position y-coordinate
   */
  SLFProfile.prototype.buildpopup = function (paper, p, text, x, y) {
    if (this.popup) this.popup.remove();
    this.popup = paper.g();
    var label = this.paper.text(0, 0, text).attr(p.font).attr(p.left);

    var side = (x > p.margin + 50) ? 'left' : 'right';
    this.popup.add(this.paper.popup(x, y, label, side, 10).attr({
      fill: '#fff', stroke: '#666', 'stroke-width': 1, 'fill-opacity': .7
    }));

    this.popup.add(label);
  };

  /**
   * Draw the popup for the temperature or gradient curve (whichever is present)
   * if mouse cursor is close to or on data point of the curve.
   *
   * @method show
   * @private
   * @param {Number} x Mouse x-coordinate
   * @param {Number} y Mouse y-coordinate
   */
  SLFProfile.prototype.show = function (x, y) {
    var p = this.properties, paper = this.paper, ii, success = false, text,
      i = this.temperatures ? this.temperatures.length : 0,
      unit = this.showgrad ? '°C/m' : '°C';

    for (ii = 0; ii < i; ++ii) {
      var c = this.temperatures[ii].c;

      if (y < c.y + 8 && y >= c.y - 8 &&
          x > c.x - 8 && x < c.x + 8) {

        text = round(this.temperatures[ii].t, 2) + ' ' + unit;
        this.buildpopup(paper, p, text, c.x, c.y);
        success = true;
        break;
      }
    }

    if (this.temperatures && this.temperatures.hover) {
      var bl = this.temperatures.hover.bl, tr = this.temperatures.hover.tr,
        ta = this.temperatures.hover.ta;

      if (y < bl.y && y > tr.y && x > bl.x && x < p.origin.x) {
        text = t('Air temp.') + ': ' + round(ta, 2) + ' ' + unit;
        this.buildpopup(paper, p, text, p.origin.x, tr.y);
        success = true;
      }
    }

    if (!success && this.popup) this.popup.remove();
  };

  /**
   * Draw the arrows for the parameters passed.
   *
   * @method arrows
   * @private
   * @param {Array<Object>} params The parameters to be drawn
   * @param {String} name Name for the svg group container, that will hold the arrows
   */
  SLFProfile.prototype.arrows = function (params, name) {
    if (this.elements[name]) this.elements[name].remove();

    var p = this.properties, left = p.origin.x, right = p.table.dock,
      set = this.paper.g(), i, ii = params.length, el,
      c = this.cartesian.py.bind(this.cartesian, 'hs');

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

    this.elements[name] = set;
  };

  /**
   * Delete the hs, datesmall and layerlines elements.
   * @method clear
   * @private
   */
  SLFProfile.prototype.clear = function () {
    var garbage = ['hs', 'datesmall', 'layerlines'], ii;

    for (ii = 0; ii < garbage.length; ++ii) {
      var tmp = garbage[ii];

      if (this.elements[tmp]) {
        this.elements[tmp].remove();
        delete this.elements[tmp];
      }
    }
  };

  /**
   * Draw the SLFProfile and set up the mouse events.
   * @method draw
   */
  SLFProfile.prototype.draw = function (profile) {
    var p = this.properties;
    if (p.miniature) return;

    if (profile) {
      this.clear();
      this.canvas.height(p.print_optimized ? 1400 : 950);
      this.profile = profile;
    }

    this.config(); // configure table

    SLFProfile.uber.prototype.draw.call(this, profile);
    this.setup(); // configure graph

    this.coordsys();

    header.draw(this, p.margin, p.width - p.margin - 1);

    this.draw_block('hardness', p.hardness_color, 'newton');
    this.draw_block('ramm', p.ramm_color, 'value');
    this.draw_curve('temperature');
    if (p.show_comment) this.comment();
    this.arrows(SLFProfile.defaults.show_arrows, 'arrows');

    this.mousemove(true);
  };

  // --- Helpers ---

  var hhardness = function () {
    return [
      {value: (new Value.Hardness(0, 1)).newton, text: 'F'},
      {value: (new Value.Hardness(0, 2)).newton, text: '4F'},
      {value: (new Value.Hardness(0, 3)).newton, text: '1F'},
      {value: (new Value.Hardness(0, 4)).newton, text: 'P'},
      {value: (new Value.Hardness(0, 5)).newton, text: 'K'}
    ];
  }();

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

}(niviz, moment));