API Docs for: 0.0.1
Show:

File: lib/graphs/mobile.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
 *
 * 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 Axis         = niviz.Axis;
  var Grid         = niviz.Grid;
  var t            = niviz.Translate.gettext;
  var round        = niviz.util.round;
  var hsgrid       = niviz.Grid.hsgrid;
  var extend       = niviz.util.extend;
  var nivizNode    = niviz.util.nivizNode;
  var format       = niviz.Visuals.format;
  var formatSimple = niviz.Visuals.formatSimple;

  /** @module niviz */
  var lbl = ['F', '4F', '1F', 'P', 'K', 'I'],
    boxcolor = '#cccbea',
    criticalcolor = '#F00',
    parameters = {
      'lwc': {
        color: '#3f9397',
        label: 'LWC'
      },
      'density': {
        color: '#6dc06a',
        label: 'Density\n[kg/m\u00b3]'
      },
      'grainsize': {
        color: '#cc7b30',
        label: 'Grain\nsize [mm]'
      },
      'grainshape': {
        color: '#000',
        label: 'Grain\nform'
      },
      'stability': {
        color: '#000',
        label: 'Stability\nTests'
      }
    };

  /**
   * Visualization of a singular snow profile optimized for mobile viewing.
   *
   * @class MobileProfile
   * @constructor
   * @extends Graph
   *
   * @param {Station} station A niViz station object
   * @param {Object} canvas A svg element that will be used as SnapSVG paper
   * @param {Object} properties
   */
  function MobileProfile (station, canvas, properties) {
    MobileProfile.uber.call(this);

    if (canvas.jquery) {
      this.canvasnode = canvas[0];
      this.canvas = canvas;
    } else {
      this.canvasnode = canvas;
      this.canvas = nivizNode(canvas);
    }

    this.canvas.empty();

    this.station = station;
    console.dir(this.station);

    this.elements = {};

    this.properties = extend({}, MobileProfile.defaults.values);
    extend(this.properties, properties);

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

    if (this.station && this.station.profiles.length) this.draw(station.profiles[0]);
  }

  Graph.implement(MobileProfile, 'MobileProfile', 'profile');

  MobileProfile.defaults = new Config('Mobile Profile', [
    { name: 'fontsize',   type: 'number', default: 14, required: true },
    { name: 'margin_top', type: 'number', default: 0 }
  ]);

  MobileProfile.defaults.load();

  /**
   * Remove a parameter from the barparams Config MobileProfile.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)
   */
  MobileProfile.remove = function (name) {
    var i, ii = MobileProfile.defaults.barparams.length - 1, current;

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

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

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

  /**
   * Configure basic properties of the MobileProfile such as font,
   * height and width.
   *
   * @method config
   * @private
   */
  MobileProfile.prototype.config = function () {
    var p = this.properties, station = this.station, canvas = this.canvas,
        oldheight = p.height, oldwidth = p.width, profile = this.profile;

    canvas.width(400);
    canvas.height(600 + p.margin_top);

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

    p.gheight = p.height - 7 * p.fontsize;

    if (profile && profile.hardness) {
      var nr = profile.hardness.layers.length;
      canvas.height(Math.max(nr * p.fontsize * 2 + 7 * p.fontsize + p.margin_top, 600));
      p.height = Math.round(canvas.height());
      p.gheight = p.height - 7 * p.fontsize;
    }

    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.hsgrid = hsgrid(station, null, null, Common.defaults.autoscale);
    p.top = 3 * p.fontsize + p.margin_top;
    p.bottom = p.height - 4 * p.fontsize;
    p.left = p.fontsize;
    p.right = p.width - 4 * p.fontsize;
    p.tempgrid = [];

    if (!this.profile.info.hs) {
      var tmpaxis = new Axis(this.hsgrid.min, this.hsgrid.max, p.bottom, p.top);

      if (p.bottom - tmpaxis.pixel(this.profile.bottom) < 30) {
        this.hsgrid = hsgrid({ top: station.top, bottom: station.bottom - 20 },
          null, null, Common.defaults.autoscale);
      }
    }

    this.cartesian = new Cartesian();

    this.cartesian.addy('depth',
      Math.min(Math.abs(this.hsgrid.min), Math.abs(this.hsgrid.max)),
      Math.max(Math.abs(this.hsgrid.min), Math.abs(this.hsgrid.max)), p.top, p.bottom);

    this.cartesian.addy('hs', this.profile.bottom, this.profile.top,
      this.cartesian.py('depth',
        Math.max(Math.abs(this.profile.top), Math.abs(this.profile.bottom))), p.top);

    this.cartesian.addx('hardness', 0, 6, p.right, p.left);
    this.cartesian.addx('density', 0, 1050, this.cartesian.px('hardness', 3), p.left);

    if (profile.temperature && (profile.temperature.min || profile.temperature.min === 0)) {
      var min = Math.min(Math.floor(profile.temperature.min / 10) * 10, -10);
      p.tempgrid = Grid.smartLegend(min, 0);
      this.cartesian.addx('temperature', p.tempgrid[0], 0, p.left, p.right);
    }

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

    if (this.elements.text) this.elements.text.remove();
    this.elements.text = this.paper.g();

    // console.dir(this.cartesian);
    // console.dir(this.hsgrid);
    // console.dir(this.paper);
  };

  /**
   * Draw the top and bottom grid of the abscissa (user configured)
   * including annotating texts.
   *
   * @method grid
   * @private
   */
  MobileProfile.prototype.grid = function () {
    var p = this.properties, cartesian = this.cartesian, paper = this.paper,
      i, set = paper.g(), path = paper.g(), x, y, bottom;

    this.remove('grid');

    // Hand hardness labels
    for (i = 0; i < 7; ++i) {
      x = round(cartesian.px('hardness', i), 1);
      y = (!i || i === 6) ? p.top - 5 : p.bottom;
      bottom = p.bottom + 5;

      if (!this.profile.info.hs && i === 0) bottom = cartesian.py('hs', this.profile.bottom);

      path.add(paper.line(x, y, x, bottom));
      i && set.add(paper.text(x, p.bottom + 8 + p.fontsize, lbl[i - 1]).attr(p.font));

      if (i === 5) { // Stability tests label
        set.add(paper.multitext(x, p.bottom + 10 + 2 * p.fontsize, parameters.stability.label)
          .attr(p.font).attr({ fontSize: p.fontsize - 3 + 'px' }));
      } else if (i === 3) { // Stability tests label
        set.add(paper.multitext(x, p.bottom + 10 + 2 * p.fontsize, parameters.lwc.label)
          .attr(p.font).attr({ fontSize: p.fontsize - 3 + 'px', fill: parameters.lwc.color }));
      } else if (i === 4) { // Density label
        set.add(paper.multitext(x, p.bottom + 10 + 2 * p.fontsize, parameters.density.label)
          .attr(p.font)
          .attr({ fontSize: p.fontsize - 3 + 'px', fill: parameters.density.color }));
      } else if (i === 2) { // Grainsize label
        set.add(paper.multitext(x, p.bottom + 10 + 2 * p.fontsize, parameters.grainsize.label)
          .attr(p.font)
          .attr({ fontSize: p.fontsize - 3 + 'px', fill: parameters.grainsize.color }));
      } else if (i === 1) { // Grainform label
        set.add(paper.multitext(x, p.bottom + 10 + 2 * p.fontsize, parameters.grainshape.label)
          .attr(p.font)
          .attr({ fontSize: p.fontsize - 3 + 'px', fill: parameters.grainshape.color }));
      }
    }

    // HS labels and legend
    for (i = 0; i < this.hsgrid.divisions; ++i) {
      x = (!i || i === this.hsgrid.divisions - 1) ? p.left : p.right;
      y = round(cartesian.py('depth', Math.abs(this.hsgrid.heights[i])), 1);

      path.add(paper.line(x, y, p.right + 5, y));

      if (i === 0 && !this.profile.info.hs) continue;
      set.add(paper.text(p.right + 8, y + (p.fontsize / 3),
        round(Math.abs(this.hsgrid.heights[i]), 2) + '')
        .attr(p.font).attr({textAnchor: 'start'}));
    }

    set.add(paper.text(p.width - 5, p.top + (p.bottom - p.top) / 2, t('Depth') + ' [cm]')
      .attr(p.font).transform('r270,' + (p.width - 5) + ',' + (p.top + (p.bottom - p.top) / 2))
      .attr({ opacity: 0.9 }));

    // Temperature (snow) labels and legend
    for (i = 0; i < p.tempgrid.length; ++i) {
      x = round(cartesian.px('temperature', p.tempgrid[i]), 1);
      path.add(paper.line(x, p.top, x, p.top - 5));
      set.add(paper.text(x, p.top - p.fontsize / 2 - 5, round(p.tempgrid[i], 4) + '')
        .attr(p.font));
    }

    set.add(paper.text(p.left + (p.right - p.left) / 2, p.top - 5 - 1.8 * p.fontsize,
      t('Temperature') + ' [°C]')
      .attr(p.font).attr({ opacity: 0.9 }));

    this.path(false, path, set);

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

  /**
   * Check if the object with identifier name exists in this.elements and
   * try to remove it from the paper and then delete the object itself.
   *
   * @method remove
   * @private
   * @param {String} name Object identifier
   */
  MobileProfile.prototype.remove = function (name, ctx) {
    ctx = ctx || this.elements;
    if (ctx[name] && ctx[name].remove) ctx[name].remove();
    delete ctx[name];
  };

  /**
   * Draw grid paths, either on the grid (light and dotted) or as solid lines.
   *
   * @method path
   * @private
   * @param {Boolean} grid true if dotted, false if solid
   * @param {Array<String>} path The path to draw
   * @param {Object} svg group container to append to
   */
   MobileProfile.prototype.path = function (grid, path, set) {
    var p = this.properties;

    if (grid) {
      set.add(path.attr({
        'stroke-dasharray': '1,1',
        'stroke': '#bdbdbd',
        'opacity': 0.9,
        fill: 'none'
      }).transform('t0.5,0.5'));
    } else {
      set.add(path.attr({
        'stroke': p.grid_color || '#000',
        'strokeWidth': 1,
        //'opacity': 0.9,
        fill: 'none'
      }));
    }
  };

  /**
   * Draw the snow temperature curve
   *
   * @method temperature
   * @private
   */
  MobileProfile.prototype.temperature = function () {
    var profile = this.profile, data = profile.temperature, old, path = [],
      paper = this.paper, i, cartesian = this.cartesian, set = paper.g(), circles = paper.g();

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

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

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

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

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

      old = c;
    }

    set.add(paper.polyline(path.join(','))
      .attr({ stroke: '#f00600', fill: 'none', strokeWidth: 3 }));
    set.add(circles);

    this.elements['temperature'] = set;
    this.elements['temperature'].after(this.elements['text']);
  };

  /**
   * Draw the snow density curve
   *
   * @method density
   * @private
   */
  MobileProfile.prototype.density = function () {
    var profile = this.profile, data = profile.density, old, path = [],
      paper = this.paper, i, cartesian = this.cartesian, set = paper.g(),
      p = this.properties, text = paper.g();

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

    this.densities = [];
    for (i = 0; i < data.layers.length; ++i) {
      var current = data.layers[i];
      var c = cartesian.pixel('density', 'hs', current.value, current.top);
      var bottom = cartesian.py('hs', current.bottom);

      if (i > 0) path.push(c.x, old.y, c.x, c.y);
      else path.push(c.x, bottom, c.x, c.y);

      text.add(paper.text(c.x - 5, bottom - (bottom - c.y) / 2 + p.fontsize / 3,
        Math.round(current.value))
        .attr(p.font)
        .attr({fill: parameters.density.color, textAnchor: 'end', fontSize: p.fontsize - 3}));

      old = c;
    }

    set.add(paper.polyline(path.join(','))
      .attr({ stroke: parameters.density.color, fill: 'none', strokeWidth: 3 }));

    this.elements['text'].add(text);
    this.elements['density'] = set;
    this.elements['density'].after(this.elements['text']);
  };

  /**
   * Note snow stability tests in the graph
   *
   * @method stability
   * @private
   */
  MobileProfile.prototype.stability = function () {
    var profile = this.profile, paper = this.paper, cartesian = this.cartesian,
      set = paper.g(), p = this.properties, text = paper.g(), stabilites = ['ct', 'ect'];

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

    stabilites.forEach(function (test) {
      profile[test] && profile[test].layers.forEach(function (layer) {
        //console.dir(layer);
        var y = cartesian.py('hs', layer.top);
        var ltext = layer.text;

        if (test === 'ct' && layer.character) {
           ltext += ' ' + layer.character || '';
        }

        text.add(paper.text(p.left + 10, y - 2, ltext));

        set.add(paper.line(p.left + 10, y, p.right, y).attr({
          'stroke-dasharray': '3,3',
          'stroke': '#000',
          'opacity': 0.3,
          fill: 'none'
        }).transform('t0.5,0.5'));
      });
    });

    text.attr(p.font)
      .attr({ fontSize: p.fontsize - 3, fontWeight: 'bold', textAnchor: 'start' });

    this.elements['text'].add(text);
    this.elements['stability'] = set;
    this.elements['stability'].after(this.elements['text']);
  };

  /**
   * Display the layer comments as footnotes
   *
   * @method comments
   * @private
   */
  MobileProfile.prototype.comments = function () {
    var paper = this.paper, p = this.properties, text = paper.g(), references = paper.g(),
      layers = this.partitions.filter(function (l) { return l.comment; }), y, self = this,
      fontsize = p.fontsize - 3, formatting = { fontSize: fontsize, textAnchor: 'start' };

    if (!layers.length) return;

    var box = paper.text(0, 0, '').attr(p.font).attr(formatting), sum = 0;
    var ignoreLibs = !!window.ignoreLibs;

    layers.slice().reverse().forEach(function (layer, i) {
      var currenttxt = format(box, '', layer.comment, 300);
      if (ignoreLibs) currenttxt = formatSimple(layer.comment);

      var mbox = paper.multitext(0, 0, currenttxt).attr(p.font).attr(formatting);

      // Increase the height of the SVG canvas depending on the height of mbox
      var lines = (currenttxt.match(/\n/g) || []).length + 1, h = lines * fontsize * 1.2;
      if (!ignoreLibs) h = mbox.getBBox().height;

      p.height = p.height + h + 0.3 * p.fontsize;
      self.canvas.height(p.height);

      var cy = p.bottom + 5 * p.fontsize + sum + i * 0.2 * p.fontsize;

      // Numbering of comments
      y = layer.ctop + p.fontsize - 4;
      references.add(paper.text(p.right - 5, y, '(' + (i + 1) + ')'));

      text.add(paper.text(p.left, cy, '(' + (i + 1) + ') ')
        .attr(p.font).attr(formatting).attr({ textAnchor: 'middle' }));
      mbox.transform('t' + (p.left + 12) + ',' + cy);

      text.add(mbox);
      sum += h;
    });

    box.remove();

    references.attr(p.font).attr({ fontSize: p.fontsize - 3, textAnchor: 'end' });

    this.elements['text'].add(text);
    this.elements['text'].add(references);
  };

  /**
   * Arrange the layers for best display
   *
   * @method arrange
   * @private
   */
  MobileProfile.prototype.arrange = function () {
    var p = this.properties, profile = this.profile, hardness = profile.hardness,
      cartesian = this.cartesian, xbottom, object,
      layers = hardness.layers, nlayers = layers.length, minheight = p.fontsize * 2, i;

    this.partitions = [];
    var space = 0, oldy = cartesian.py('hs', profile.top);
    for (i = nlayers - 1; i >= 0; --i) { // starting from the surface
      var ctop = Math.round(cartesian.py('hs', layers[i].top));
      var cbtm = Math.round(cartesian.py('hs', layers[i].bottom));
      xbottom = null;

      if (profile.hardnessBottom && profile.hardnessBottom.layers[i].value) {
        xbottom = Math.round(cartesian.px('hardness', profile.hardnessBottom.layers[i].value));
      }

      var x = Math.round(cartesian.px('hardness', layers[i].value || 0));
      var gs = '';
      if (profile.grainsize.layers[i].value) {
        gs += profile.grainsize.layers[i].value.avg || '';
        if (profile.grainsize.layers[i].value.max
            && profile.grainsize.layers[i].value.max !== profile.grainsize.layers[i].value.avg)
          gs += ' - ' + profile.grainsize.layers[i].value.max;
      }

      object = {
        x: x, xbottom: xbottom || x, gs: gs,
        code: profile.grainshape.layers[i].code || '',
        fill:  profile.grainshape.layers[i].color || '#FFF',
        lwc: profile.wetness.layers[i].code || '',
        critical: profile.critical && profile.critical.layers[i].value || null,
        comment: profile.comments.layers[i].value || null
      };

      var diff_to_last = cbtm - oldy;

      if (diff_to_last < minheight) { // just append
        object.top =  oldy;
        object.bottom = oldy + minheight;
        object.ctop = ctop;
        object.cbtm = cbtm;

        oldy = oldy + minheight;
      } else {
        space += (diff_to_last - minheight);

        object.top =  Math.max(oldy, ctop);
        object.bottom = cbtm;
        object.ctop = ctop;
        object.cbtm = cbtm;

        oldy = cbtm;
      }

      this.partitions.unshift(object);
    }

    this.shiftup(nlayers, minheight, space);
    //console.dir(this.partitions);
  };

  /**
   * Shift the stratigraphic layers upwards as much as possible
   *
   * @method shiftup
   * @private
   */
  MobileProfile.prototype.shiftup = function (nlayers, minheight, space) {
    var tmp = null, moveup, i;
    for (i = 0; i < nlayers; ++i) {
      tmp = this.partitions[i];
      var h = Math.round(tmp.bottom - tmp.top - minheight);
      if (i < nlayers - 1) {
        h = Math.round(tmp.bottom - this.partitions[i + 1].bottom - minheight);
      }

      if (tmp.bottom > tmp.cbtm || (i && tmp.bottom > this.partitions[i - 1].top)) {
        moveup = tmp.bottom - tmp.cbtm;
        if (i) moveup = Math.max(moveup, tmp.bottom - this.partitions[i - 1].top);
        tmp.bottom -= Math.min(space, moveup);
      }

      if (h > 0) space -= h;
      if (space <= 0) break;
    }

    // Second pass: check if all layers are high enough
    for (i = 0; i < nlayers - 1; ++i) {
      tmp = this.partitions[i];

      if (tmp.top > tmp.ctop) {
        moveup = Math.min(tmp.top - tmp.ctop, tmp.top - this.partitions[i + 1].bottom);
        tmp.top -= moveup;
      }

      if (tmp.bottom - tmp.top < minheight) {
        tmp.top = tmp.bottom - minheight;
        if (this.partitions[i + 1].bottom > tmp.top) this.partitions[i + 1].bottom = tmp.top;
      }
    }
  };

  /**
   * Display the hardness profile (including wetness, grain size and grain shape)
   *
   * @method stratigraphy
   * @private
   */
  MobileProfile.prototype.stratigraphy = function () {
    var p = this.properties, cartesian = this.cartesian, partitions = this.partitions,
      paper = this.paper, set = paper.g(), path = [], text = paper.g(), i, poly;

    var xF = Math.round(cartesian.px('hardness', 1)),
      middle = Math.round(xF + (p.right - xF) / 2),
      x4F = Math.round(cartesian.px('hardness', 2)),
      x1F = Math.round(cartesian.px('hardness', 3));
      //xP = Math.round(cartesian.px('hardness', 4));
      //xK = Math.round(cartesian.px('hardness', 5));

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

    for (i = 0; i < partitions.length; ++i) {
      var c = partitions[i];
      path.length = 0;

      path.push(c.x, c.top, xF, c.top, middle, c.ctop, p.right, c.ctop, p.right, c.cbtm,
        middle, c.cbtm, xF, c.bottom, c.xbottom, c.bottom, c.x, c.top);

      poly = paper.polyline(path).attr({ fill: boxcolor });
      set.add(poly);

      if (c.critical) this.markCriticalLayer(c, xF, middle, paper, p, set, poly);

      // Add grainshape column
      text.add(paper.text(xF - 5, c.top + (c.bottom - c.top) / 2 + p.fontsize / 3, c.code)
        .attr({
          fontSize: (p.fontsize + 3) + 'px', fill: '#000', fontWeight: 'bold',
          textAnchor: 'end', fontFamily: 'snowsymbolsiacs'
        }));

      // Add grain size column
      text.add(paper.text(x4F - 2, c.top + (c.bottom - c.top) / 2 + p.fontsize / 3, c.gs)
        .attr(p.font).attr({
          fill: parameters.grainsize.color, textAnchor: 'end',
          fontSize: p.fontsize - 3, fontWeight: 'bold'}));

      // Add wetness column
      text.add(paper.text(x1F - 3, c.top + (c.bottom - c.top) / 2 + p.fontsize / 3, c.lwc)
        .attr(p.font).attr({fill: parameters.lwc.color, textAnchor: 'end',
          fontSize: p.fontsize - 3, fontWeight: 'bold'}));
    }

    set.attr({ strokeWidth: 1, stroke: '#000', opacity: 1 });

    this.elements['text'].add(text);
    this.elements['stratigraphy'] = set;
    this.elements['stratigraphy'].after(this.elements['text']);
  };

  /**
   * Mark critical layer
   *
   * @method markCriticalLayer
   */
  MobileProfile.prototype.markCriticalLayer = function (c, xF, middle, paper, p, set, poly) {
    var gradient, gradpath;

    if (c.critical === 'top') {
      var gradbtm = Math.round((c.x - c.xbottom) * (2 / 3) + c.xbottom),
        ctop = Math.round(c.ctop + (c.cbtm - c.ctop) / 3),
        top = Math.round(c.top + (c.bottom - c.top) / 3);

      gradpath = [c.x, c.top, xF, c.top, middle, c.ctop, p.right, c.ctop,
        p.right, ctop, middle, ctop, xF, top, gradbtm, top, c.x, c.top];

      gradient = paper.polyline(gradpath).attr({ fill: criticalcolor, strokeWidth: 0 });
      set.add(gradient);

    } else if (c.critical === 'bottom'){
      var gradtop = Math.round((c.x - c.xbottom) * (1 / 3) + c.xbottom),
        cbtm = Math.round(c.cbtm - (c.cbtm - c.ctop) / 3),
        btm = Math.round(c.bottom - (c.bottom - c.top) / 3);

      gradpath = [ gradtop, btm, xF, btm, middle, cbtm, p.right, cbtm, p.right, c.cbtm,
        middle, c.cbtm, xF, c.bottom, c.xbottom, c.bottom, gradtop, btm ];

      gradient = paper.polyline(gradpath).attr({ fill: criticalcolor, strokeWidth: 0 });
      set.add(gradient);

    } else {
      poly.attr({ fill: criticalcolor });
    }
  };

  /**
   * Mark soil surface
   *
   * @method markSoil
   */
  MobileProfile.prototype.markSoil = function () {
    var profile = this.profile, paper = this.paper, cartesian = this.cartesian,
      set = paper.g(), p = this.properties;

    this.remove('soil');

    // Mark soil if possible
    if (profile.info.hs) {
      var y = round(cartesian.py('depth', Math.abs(this.profile.info.hs), 1));
      var left = round(((p.right + cartesian.px('hardness', 1)) / 2), 1);
      var lineattr = { fill: '#FFF', stroke: 'sienna', strokeWidth: 2 };
      var space = Math.round((p.right - left - 2) / 4);


      set.add(paper.polyline(left + space, y + 8, left, y, p.right + 5, y).attr(lineattr));
      for (var i = 1; i < 4; ++i) {
        set.add(paper.line(left + space * i, y, left + space * (i + 1), y + 8).attr(lineattr));
      }

      set.add(paper.rect(p.right + 5, y - p.fontsize * 0.7, p.fontsize * 2.7, p.fontsize * 1.4)
        .attr(lineattr));

      set.add(paper.text(p.right + 8, y + p.fontsize / 3, 'GND').attr(p.font)
        .attr({fill: 'sienna', textAnchor: 'start'}));
    }

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

  /**
   * Mark that profile is not dug to ground
   *
   * @method zigzag
   */
  MobileProfile.prototype.zigzag = function () {
    var profile = this.profile, paper = this.paper, cartesian = this.cartesian,
      set = paper.g(), p = this.properties;

    this.remove('zigzag');

    // Mark zigzag
    if (!profile.info.hs) {
      var y = round(cartesian.py('depth', Math.abs(this.profile.bottom), 1));
      var lineattr = { fill: '#FFF', stroke: 'black', strokeWidth: 1 };

      set.add(paper.polyline(p.right, p.bottom + 5, p.right, p.bottom - 10,
        p.right + 15, p.bottom - 15, p.right - 15, p.bottom - 20,
        p.right, p.bottom - 25, p.right, y).attr(lineattr));
    }

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


  /**
   * Draw the MobileProfile.
   *
   * @method draw
   * @param {Profile} profile The niViz profile to render
   */
  MobileProfile.prototype.draw = function (profile) {

    if (profile) this.profile = profile;

    this.config();
    this.arrange();

    try { // fail more graciously when plotting the grid
      this.grid();
    } catch (e) {
      console.error(e);
    }

    this.stratigraphy();
    this.density();
    this.stability();
    this.temperature();
    this.comments();
    this.markSoil();
    this.zigzag();
  };

  // --- Helpers ---

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

}(niviz, moment));