/*
* 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));