/*
* 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 space-infix-ops: [0, {"int32Hint": false}] */
/*eslint complexity: [2, 22]*/
(function (niviz, moment) {
'use strict';
// --- Module Dependencies ---
var Graph = niviz.Graph;
var Common = niviz.Common;
var Config = niviz.Config;
var Feature = niviz.Feature;
var round = niviz.util.round;
var t = niviz.Translate.gettext;
var extend = niviz.util.extend;
var nivizNode = niviz.util.nivizNode;
/** @module niviz */
/**
* Visualization of a singular snow profile in tabular form. This class is the parent
* of the SLFProfile, StructureProfile and SimpleProfile classes.
*
* @class TabularProfile
* @constructor
*
* @extends Graph
*/
function TabularProfile (station, canvas, properties) {
TabularProfile.uber.call(this, station, properties);
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.blanket = {};
this.elements = {};
this.columns = [];
this.properties = extend({}, TabularProfile.defaults.values);
// make sure that the children retain the right to call draw themselves
if (!(this instanceof niviz.SLFProfile) && !(this instanceof niviz.SimpleProfile)) {
this.draw(station.profiles[0]);
}
}
Graph.implement(TabularProfile, 'TabularProfile', 'profile');
TabularProfile.defaults = new Config('Tabular Graph', [
{ name: 'fontsize', type: 'number', default: 10 },
{ name: 'font_color', type: 'color', default: '#000000' },
{ name: 'grid_color', type: 'color', default: '#000000' }
]);
TabularProfile.defaults.load();
/**
* A list of parameters that will be layed out directly, i. e.
* at their real height and will not be placed among the stratigraphic
* parameters.
*
* @property nopos
* @type Array<String>
* @static
*/
TabularProfile.nopos = ['ct', 'ect', 'rb', 'sf', 'saw', 'threads', 'density'];
/**
* Overwrite current properties with the ones passed as parameter.
*
* @method setProperties
* @param {Object} properties
*/
TabularProfile.prototype.setProperties = function (properties) {
// Overwrite all defaults with the ones coming in
extend(this.properties, properties);
this.draw();
};
/**
* Configure basic properties of the TabularProfile such as font,
* table margins, height and width.
*
* @method config
* @protected
*/
TabularProfile.prototype.config = function () {
var p = this.properties, station = this.station,
oldheight = p.height, oldwidth = p.width;
p.height = this.canvas.height(); //get the container height into variable h
p.width = this.canvas.width();
if (!this.paper || p.height !== oldheight || p.width !== oldwidth) {
if (this.paper) this.paper.remove();
this.paper = Snap(this.canvasnode);
}
p.table = {};
p.table.max = Math.max(Math.floor((station.top + 40) / 40) * 40, 200);
p.table.min = 0;
p.table.range = p.table.max - p.table.min;
p.table.top = 4 * p.fontsize;
p.table.bottom = 1;
p.table.height = p.height - p.table.top - p.table.bottom;
p.table.dock = 0; // where the interconnector starts
p.table.origin = {
x: 150,
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
};
p.table.font = {
fontSize: p.fontsize + 'px',
fill: p.font_color,
textAnchor: 'middle',
fontFamily: 'Helvetica, Arial'
};
};
/**
* A closure that provides the property this.table().width
* to get the exact width of the table.
*
* @method table
* @protected
*/
TabularProfile.prototype.table = function () {
var self = this, p = this.properties;
var width = function () {
var sum = self.columns.reduce(function(a, b) {
if (self.profile[b.parameter]) return a + b.width;
return a;
}, 0);
return Math.ceil(sum * p.fontsize);
};
return {
width: width()
};
};
/**
* Set all possible columns of the table. These consist of the fixed
* columns for stratigraphic parameters and some configured by the user.
*
* @method loadcolumns
* @private
*/
TabularProfile.prototype.loadcolumns = function () {
var p = this.properties;
this.columns.length = 0; // clear
// The following columns are non-negotiable
this.add({width: 3, parameter: 'wetness'});
this.add({width: 5, parameter: 'grainshape'});
this.add({width: 6, parameter: 'grainsize'});
this.add({width: 3, parameter: 'hardness'});
p.other_parameters && p.other_parameters.forEach(function (e) {
if (e.name === 'thickness') this.add({ width: e.width, parameter: e.name });
}, this);
//this.add({width: 3, parameter: 'thickness'});
//this.add({width: 5, parameter: 'ramm'});
//this.add({width: 4, parameter: 'temperature'});
p.stability_parameters && p.stability_parameters.forEach(function (e) {
if (e.name === 'flags') {
this.add({ width: Common.defaults.display_yellow_flags === 'as flags' ? 7 : 4,
parameter: e.name });
}
}, this);
p.other_parameters && p.other_parameters.forEach(function (e) {
if (e.name !== 'thickness' && e.name !== 'comments')
this.add({ width: e.width, parameter: e.name });
if (e.name === 'comments') {
this.properties.show_layer_comments = true;
}
}, this);
p.stability_parameters && p.stability_parameters.forEach(function (e) {
if (e.name !== 'flags') this.add({ width: e.width, parameter: e.name });
}, this);
// this.add({width: 7, parameter: 'flags'});
// this.add({width: 4, parameter: 'ct'});
// this.add({width: 5, parameter: 'threads'});
};
/**
* Add a column to the table for a certain parameter.
*
* @method add
* @private
* @param {Object} object An object with a width and parameter property
* @param {Number} [position] Where to insert the column. Default: at the end.
*/
TabularProfile.prototype.add = function (object, position) {
if (position === undefined) position = this.columns.length;
if (position > -1 && position <= this.columns.length)
this.columns.splice(position, 0, object);
};
/**
* Remove a column from the table.
*
* @method remove
* @private
* @param {Number} column The column index to remove
*/
TabularProfile.prototype.remove = function (column) {
if (column > -1) this.columns.splice(column, 1);
};
/**
* Draw the TabularProfile and set up the mouse events.
*
* @method draw
* @param {Profile} profile
*/
TabularProfile.prototype.draw = function (profile) {
if (profile) this.profile = profile;
if (!this.profile) return;
this.loadcolumns();
this.config(); // Recalculate certain parameters
this.partitions = [];
this.arrange(this.partitions);
this.draw_columns(this.partitions);
};
/**
* Zoom in on a certain snow height range.
*
* @method zoom
* @protected
* @param {Object} range An object with a min, max and axis property
*/
TabularProfile.prototype.zoom = function (range) {
this.range = range;
this.config(); // Necessary to recalculate table properties
this.blanket = {};
this.draw_columns(this.layout(this.select()));
};
/**
* Once the zoomed primary partition has been subjected to layout
* adjust all other partitions to represent the initial relations.
*
* @method align
* @private
* @param {Array< Array<Object> >} partitions
* @return {Array< Array<Object> >} Partitions in zoomed range.
*/
TabularProfile.prototype.align = function (partitions) {
var i, j, k, primary = partitions[partitions.primeindex];
for (k = 1; k < primary.length; ++k) {
var members = primary[k].members,
rheight = Math.abs(primary[k].y - primary[k-1].y);
for (i = 0; i < members.length; ++i) {
var index = members[i].index;
var elements = members[i].elements;
var nopos = partitions[index].nopos;
for (j = 0; j < elements.length; ++j) {
if (nopos) {
partitions[index][elements[j]].y = this.y(partitions[index][elements[j]].height);
continue;
}
var ratio = Math.abs(partitions[index][elements[j]].y -
(primary[k].original || primary[k].y)) / primary[k].rheight;
if (primary[k].rheight === 0) {
partitions[index][elements[j]].y = primary[k].y;
continue;
}
partitions[index][elements[j]].y = primary[k].y + rheight * ratio;
}
}
}
return partitions;
};
/**
* Layout the primary partition in zoomed state.
*
* @method layout
* @private
* @param {Array< Array<Object> >} partitions
* @return {Array< Array<Object> >} Partitions in zoomed range.
*/
TabularProfile.prototype.layout = function (partitions) {
var primeindex = partitions.primeindex, i,
primary = partitions[primeindex], p = this.properties.table,
ypx, diff, myspace;
// Greedy strategy to the top, if possible
var space = p.height - partitions.height;
for (i = primary.length - 1; i >= 0; --i) {
//if (primary[i].layer === undefined) continue; // Ignore the fake top layer
ypx = this.y(primary[i].height);
diff = primary[i].y - ypx;
myspace = 0;
if (diff > 0) {
myspace = Math.min(space, diff);
if (myspace > 0) {
primary[i].original = primary[i].y;
primary[i].y -= myspace;
primary[i].shift = myspace;
}
}
space = myspace;
if (space === 0) break;
}
// Greedy strategy moving towards bottom
space = primary.length > 1 ? primary[1].y + primary[1].rheight : 0;
space = this.y(p.min) - space;
if (space < 0) space = 0;
for (i = 1; i < primary.length; ++i) {
ypx = this.y(primary[i].height);
diff = primary[i].y - ypx;
myspace = 0;
if (diff < 0) {
myspace = Math.min(space, Math.abs(diff));
if (myspace > 0) {
primary[i].original = primary[i].y;
primary[i].y += myspace;
primary[i].shift = (primary[i].shift ? primary[i].shift : 0) - myspace;
}
}
space = myspace;
if (space === 0) break;
}
primary[0].y = Math.min(this.y(primary[0].height), this.y(p.min));
if (primary.length > 1)
primary[0].y = Math.max(primary[1].y + primary[1].rheight, primary[0].y);
return this.align(partitions);
};
/**
* Select all layers in all partitions that are within p.table.min and p.table.max.
*
* @method select
* @private
* @return {Array< Array<Object> >} Partitions in zoomed range.
*/
TabularProfile.prototype.select = function () {
var ii, partitions = extend([], this.partitions),
primeindex = partitions.primeindex, i, kk, jj,
primary = partitions[primeindex], current = [],
p = this.properties.table, sumheight = 0;
for (i = 0; i < this.columns.length; ++i) {
current[i] = [{ height: this.partitions[i][0].height,
y: this.y(this.partitions[i][0].height), layer: 0 }];
}
for (ii = 1; ii < primary.length; ++ii) {
var lastheight = ii ? primary[ii-1].height : 0,
c = primary[ii].height;
if (lastheight < p.max && c > p.max) {
c = p.max;
} else if (primary[ii].height > p.max || primary[ii].height < p.min) continue;
current[primeindex].push({
height: primary[ii].height,
y: primary[ii].y,
layer: primary[ii].layer
});
var cc = current[primeindex].length - 1;
var h1 = primary[ii].height, h2 = Math.max(primary[ii-1].height, p.min);
// Push all other columns
var columns = [];
for (jj = primeindex + 1; jj < partitions.length; ++jj) {
var partition = partitions[jj];
if (partitions[jj].nopos) current[jj].nopos = true;
var rows = [];
for (kk = 1; kk < partition.length; ++kk) {
var height = partition[kk].height;
if (current[jj].nopos && (height < p.min || height > p.max)) continue;
if ((h1 === h2 && height === h1) || (height <= h1 && height > h2)) {
if ((height === h1) || height <= c) {
current[jj].push({
height: height,
y: partition[kk].y,
layer: partition[kk].layer
});
var x = current[jj][current[jj].length - 1];
if (x.height === h1) x.height = c;
rows.push(current[jj].length - 1);
}
}
}
columns.push({ index: jj, elements: rows });
}
current[primeindex][cc].members = columns;
current[primeindex][cc].rheight =
Math.abs(primary[ii].y - primary[ii-1].y);
sumheight += current[primeindex][cc].rheight;
if (lastheight < p.max && c >= p.max) {
current[primeindex][cc].height = c;
}
}
current.rows = primary.length;
current.height = sumheight;
current.primeindex = primeindex;
return current;
};
/**
* Initially (without zooming) arrange all the layers in the table by
* first allocating enough space for the primary (first) column to be
* rendered and then increasing the table size in case more space is needed
* for later columns.
*
* @method arrange
* @private
* @param {Array< Array<Object> >} partitions
*/
TabularProfile.prototype.arrange = function (partitions) {
var p = this.properties, height = p.fontsize * 1.5, i,
primeindex = 0, primary = false;
this.initialheight = p.table.height;
for (i = 0; i < this.columns.length; ++i) {
var column = this.columns[i], parameter = column.parameter,
data = this.profile[parameter];
partitions[i] = [{ height: p.table.min, y: this.y(p.table.min) }];
if (!data
|| (data.elements && !data.elements[0])
|| !data.layers.length) continue;
partitions[i] = [{ height: data.bottom, y: this.y(data.bottom) }];
var resizesize = 0;
if (!primary) {
//console.log('Primary partition: ' + parameter);
resizesize = this.partition(partitions[i], data, height);
//console.dir(partitions[i]);
primeindex = i;
primary = true;
} else if (TabularProfile.nopos.indexOf(data.type) > -1) {
this.directlayout(partitions[i], data);
partitions[i].nopos = true;
} else {
// all stratigraphic layers need to have the same tops and bottoms
partitions[i] = JSON.parse(JSON.stringify(partitions[primeindex]));
// Legacy code: keep in case non-stratigraphic layers need to be layed out
// partitions[i] = [];
// resizesize = this.position(partitions, i, primeindex, data, height);
// this.canvas.height(p.height + resizesize);
// this.config();
}
if (resizesize > 0 && i === primeindex) {
this.canvas.height(resizesize + p.table.bottom + p.table.top);
this.config();
partitions[i] = [{ height: p.table.min, y: this.y(p.table.min) }];
resizesize = this.partition(partitions[i], data, height);
if (resizesize) throw new Error('resizesize should be 0');
}
}
partitions.primeindex = primeindex;
};
/**
* For parameters that are not layed out relatively to the primary partition:
* directly place them at the actual height of the layer
*
* @method directlayout
* @private
* @param {Array<Object>} partition The partition (column) to be layed out
* @param {Feature} data
*/
TabularProfile.prototype.directlayout = function (partition, data) {
var i, ii = data.layers.length, ypx, p = this.properties.table, heights = [];
for (i = 0; i < ii; ++i) {
var current = data.layers[i].top;
if (current === undefined && (data.type === 'rb'
|| data.type === 'saw'
|| data.type === 'sf'
|| data.type === 'ect'
|| data.type === 'ct'))
current = this.profile.top;
if (current > p.max || current < p.min || heights.indexOf(current) !== -1) continue;
heights.push(current);
ypx = this.y(current);
partition.push({ y: ypx, height: current, layer: i });
}
};
/**
* Primary (first existant) column layout. If there is too little space to
* fit the column layers than a value >0 is returned. The canvas needs to
* be resized appropriately.
*
* @method partition
* @private
* @param {Array<Object>} partition The primary (column) to be layed out
* @param {Feature} data
* @param {Number} height The minimal height of a table row
* @return {Number} Number of pixels to enlarge the canvas
*/
TabularProfile.prototype.partition = function (partition, data, height) {
var ii, jj, reserve = 0, total = (data.layers.length + 2) * height,
p = this.properties.table, h;
reserve = this.y(p.min) - partition[0].y;
if (total > p.height) return total; // Need to enlarge canvas
for (ii = 0; ii < data.layers.length; ++ii) {
var space, current = data.layers[ii].top, ypx = this.y(current);
//console.dir('current: ' + current + ' ypx: ' + ypx);
if (current > p.max || current < p.min) continue;
// Calculate how much space would be left for the current layer:
// If this turns out to be negative then we need to rearrange the table
space = partition[partition.length-1].y - ypx - height;
partition.push({
y: ypx,
height: current,
layer: ii,
original: data.layers[ii].top
});
var now = partition.length - 1, old = now - 1;
if (space >= 0) { // enough space to place new layer at real position
reserve += space;
} else { // problem - not enough space, shifting down might help
var total_shift = Math.abs(space);
if (reserve >= total_shift) { // shift down possible
reserve -= total_shift;
for (jj = old; jj >= 0; --jj) {
var necessary_shift = Math.abs(partition[jj].y - partition[jj+1].y) - height;
if (necessary_shift >= 0) { // no shift necessary, we're done
break;
} else {
partition[jj].y -= necessary_shift;
}
}
} else { // shift up necessary
if (reserve > 0) { // Get rid of any extra space down below
for (jj = 0; jj < old; ++jj) {
partition[jj+1].y = partition[jj].y - height;
}
reserve = 0;
}
partition[now].y = partition[old].y - height;
}
}
}
// What if the layer does not reach the profile height?
// We'll add fake layer at the top of the profile, so the primary
// partition still remains the reference for other partitions
if (data.layers[ii-1].top < this.profile.top) {
h = this.y(this.profile.top);
if (partition.length && partition[partition.length - 1].y < h) {
var tmp = this.y(partition[partition.length - 1].height) - h;
h = partition[partition.length - 1].y - Math.min(height, tmp);
}
partition[partition.length] = { // Add a fake layer at the top
y: h,
height: this.profile.top,
layer: undefined // Check this to know whether it's fake
};
}
// What if the layer does not reach the profile bottom?
// We'll add fake layer at the bottom of the profile, so the primary
// partition still remains the reference for other partitions
if (partition[0].height > this.profile.bottom) {
h = this.y(this.profile.bottom);
partition.splice(0, 0, { // Add a fake layer at the bottom
y: h,
height: this.profile.bottom,
layer: undefined // Check this to know whether it's fake
});
}
return 0;
};
/**
* Position another column relatively to the primary partition.
*
* @method position
* @private
* @param {Array< Array<Object> >} partitions All partitions
* @param {Number} index Index of partition to be positioned
* @param {Number} primeindex Index of primary partition
* @param {Feature} data
* @param {Number} height The minimal height of a table row
* @return {Number} Number of pixels to enlarge the canvas
*/
TabularProfile.prototype.position = function (partitions, index, primeindex, data, height) {
var ii, jj, sum = 0, p = this.properties.table, heights = [],
partition = partitions[index], primary = partitions[primeindex];
for (ii = -1; ii < data.layers.length; ++ii) {
var current = ii >= 0 ? data.layers[ii].top : data.layers[0].bottom;
if (current > p.max || current < p.min || heights.indexOf(current) !== -1) continue;
heights.push(current);
for (jj=1; jj < primary.length; ++jj) {
if (primary[jj].height >= current) {
//console.dir(ii + ' into bin ' + jj + ', ' + current + ' <= ' + primary[jj].height);
break;
}
}
if (jj >= primary.length) continue; // Out of the scope of the table // HACK assert
// calculate position within bin
var relative = primary[jj].height - primary[jj-1].height, absolute;
if (relative !== 0) {
relative = (current - primary[jj-1].height) / relative;
absolute = primary[jj-1].y - (relative) * (primary[jj-1].y - primary[jj].y);
} else {
absolute = primary[jj].y;
}
//console.dir('Position: ' + absolute + ' - below bin '
// + jj + ' - ' + primary[jj].y + 'px');
var diff = ii>= 0? Math.abs(partition[partition.length - 1].y - absolute) : height;
partition.push({ y: absolute, height: current, layer: ii >=0 ? ii : undefined });
if (diff < height) {
var shift = (height - diff);
this.shiftdown(partitions, primeindex, jj, index, shift, height);
sum += shift;
}
}
return sum;
};
/**
* Shift down all partitions already layed out up until a certain
* partition and layer height.
*
* @method shiftdown
* @private
* @param {Array< Array<Object> >} partitions All partitions
* @param {Number} primeindex Index of primary partition
* @param {Number} primepos Only shift down below this height of the primary partition
* @param {Number} index Index up to which shifting may occur (i. e. currently positioned)
* @param {Number} shift Number of pixels to shift down
* @param {Number} height Minimal height of a row
*/
TabularProfile.prototype.shiftdown = function (partitions, primeindex,
primepos, index, shift, height) {
var primary = partitions[primeindex], partition = partitions[index], kk, ll;
// Use greedy algorithm to shift everything down:
// Table canvas will be resized appropriately
for (kk = primeindex; kk <= index; ++kk) {
var p = partitions[kk];
for (ll = 0; ll < p.length; ++ll) {
if (p[ll].height >= primary[primepos].height) break;
if (kk === index && ll === (partition.length - 1)) break;
p[ll].y += shift;
}
}
};
/**
* Connect table and graph through dotted lines and store reference in an object
* called this.blanket to later reference the individual lines.
*
* @method connect
* @private
* @param {Array<Object>} partition Primary partition, to be connected to graph
* @param {Number} offset Pixel position of the right edge of the graph
*/
TabularProfile.prototype.connect = function (partition, offset) {
var paper = this.paper, p = this.properties, ii, cnr = 0, profile = this.profile,
blanket = this.blanket, set = paper.g(), comments = profile.comments, cmax = 0;
if (p.show_layer_comments) {
cmax = this.profile.comments.layers.filter(function (l) { return l.value}).length;
}
blanket.length = 0;
for (ii = 1; ii < partition.length; ++ii) {
if (partition[ii].layer === undefined) continue; // Fake top or bottom layer
var ypx = Math.round(this.y(partition[ii].height));
var path = paper.g().add(paper.line(offset, Math.round(partition[ii].y), p.table.dock, ypx));
if (p.show_layer_lines) path.add(paper.line(p.table.dock, ypx, p.origin.x, ypx));
if (p.show_layer_comments && comments && comments.layers[ii - 1] && comments.layers[ii - 1].value) {
set.add(paper.text(offset - 1, Math.round(partition[ii].y) + p.fontsize, '(' + (cmax - cnr) + ')')
.attr(p.font)
.attr({
fontSize: p.fontsize - 2,
textAnchor: 'end'
}));
++cnr;
}
path.attr({
strokeDasharray: '1,1',
stroke: p.grid_color,
strokeWidth: 0.5
}).transform('t0.5,0.5');
blanket[partition[ii].height] = {
connector: path,
elements: [], // an array of horizontal table lines
area: { top: partition[ii].y, bottom: partition[ii-1].y }
};
set.add(path);
}
return set;
};
/**
* Draw a column header at the position specified by offset.
*
* @method header
* @private
* @param {Number} offset Pixel position
* @param {Object} options Holds the symbol and unit properties
* @param {Feature} data
*/
TabularProfile.prototype.header = function (offset, options, data) {
var paper = this.paper, set = paper.g(), p = this.properties.table,
center = offset + 4, flags = (Common.defaults.display_yellow_flags === 'as flags');
if (data.type === 'flags' && flags) { // requires a special header
set.add(this.flags(offset, data));
} else {
set.add(paper.text(center, p.labels.symbol, t(options.symbol || data.symbol || ''))
.attr(p.font).attr('text-anchor', 'start'));
if (data.type !== 'wetness' && data.type !== 'hardness')
set.add(paper.text(center, p.labels.unit, options.unit || '')
.attr(p.font).attr('text-anchor', 'start'));
}
return set;
};
/**
* Draw a column header for the yellow flags feature.
*
* @method flags
* @private
* @param {Number} offset Pixel position
* @param {Feature} data
*/
TabularProfile.prototype.flags = function (offset, data) {
var paper = this.paper, set = paper.g(), ii,
p = this.properties, center = p.table.labels.unit + p.fontsize,
labels = ['E', 'R', 'F', '\u0394E', '\u0394R', 'Depth'];
for (ii = 0; ii < labels.length; ++ii) {
set.add(paper.text(offset + (ii + 1) * p.fontsize, center, labels[ii])
.attr(p.table.font).transform('r270').attr('text-anchor', 'start'));
}
return set;
};
/**
* Draw all columns.
*
* @method draw_columns
* @private
* @param {Array< Array<Object> >} partitions All partitions
*/
TabularProfile.prototype.draw_columns = function (partitions) {
var jj, p = this.properties, offset = p.table.origin.x, paper = this.paper,
lineattr = {'stroke': p.grid_color, 'stroke-width': 0.5},
set = paper.g(), bwidth;
if (this.elements['table']) this.elements['table'].remove();
for (jj = 0; jj < this.columns.length; ++jj) {
var column = this.columns[jj], width = column.width,
parameter = column.parameter, data = this.profile[parameter];
var layer_path = [], partition = partitions[jj], ii = partition.length, i, nopos;
if (!data
|| (data.elements && (!data.elements[0] || !data.layers.length))
|| (!data.layers.length)) continue;
if (jj === partitions.primeindex)
set.add(this.connect(partition, offset));
var options = Feature.type[data.type] || undefined;
set.add(this.header(offset, options || {}, data));
// Vertical separators
layer_path = paper.line(offset, p.table.labels.symbol - p.fontsize,
offset, p.table.height + p.table.top);
if (!nopos && TabularProfile.nopos.indexOf(data.type) > -1) {
nopos = true;
set.add(layer_path.attr({stroke: p.grid_color, strokeWidth: 2}));
bwidth = offset - p.table.origin.x;
} else {
set.add(layer_path.attr(lineattr).transform('t0.5,0.5'));
}
for (i = 1; i < ii; ++i) {
var layer = partition[i].layer, row = paper.g();
if (layer === undefined) continue; // Fake top or bottom layer
var y = Math.round(partition[i].y), depth = partition[i].height,
lasty = Math.round(partition[i-1].y), height;
if (this.range) lasty = Math.min(lasty, Math.round(this.y(this.range.min)));
height = lasty - y;
if ((data.type !== 'comments' || data.layers[layer].value) && y >= p.table.top)
row.add(paper.line(offset, y, offset + width * p.fontsize, y));
if (data.type === 'density') {
// draw the bottom of the density layer in certain cases
var ty = Math.round(this.y(data.layers[layer].bottom));
height = ty - y;
if ((ty <= p.table.origin.y && ty >= p.table.top) && (i === 1 || lasty !== ty))
row.add(paper.line(offset, ty, offset + width * p.fontsize, ty));
}
row.attr(lineattr).transform('t0.5,0.5');
set.add(row);
//visualize the values
set.add(this.toString(data.layers[layer], data.type, offset, y, width, height, layer));
if (depth in this.blanket) { //add elements to this row
this.blanket[depth].elements.push(row);
}
}
offset += width * p.fontsize;
}
p.table.width = offset - p.table.origin.x;
p.table.bwidth = bwidth || p.table.width;
// Draw the outer frame of the table
layer_path = paper.g();
layer_path.add(paper.line(p.table.origin.x, p.table.origin.y, offset, p.table.origin.y));
layer_path.add(paper.line(p.table.origin.x, p.table.top, offset, p.table.top));
layer_path.add(paper.line(offset, p.table.labels.symbol - p.fontsize / 2,
offset, p.table.origin.y));
// Draw bottom line of profile
var cp = partitions[partitions.primeindex], l = 0;
if (cp.length > 1 && cp[1].layer === undefined) l = 1;
if (isNaN(cp[l].y) || !isFinite(cp[l].y)) return;
layer_path.add(paper.line(p.table.origin.x,
Math.round(cp[l].y), p.table.origin.x + (bwidth || 0), Math.round(cp[l].y)));
set.add(layer_path.attr(lineattr).transform('t0.5,0.5'));
if (p.table.width > p.fontsize * 10)
this.slope(p.table.origin.x, p.table.origin.y + p.fontsize, set);
if (this.mat) this.mat.remove();
this.mat = paper
.rect(p.table.origin.x, p.table.top, offset - p.table.origin.x, p.table.height)
.attr({ 'stroke-width': 0, 'fill-opacity': 0.0, 'fill': '#F0F' });
this.mouseon();
set.add(this.mat);
this.elements['table'] = set;
};
/**
* Draw the slope angle at the bottom of the table and display exposition.
*
* @method slope
* @private
* @param {Number} x x-coordinate
* @param {Number} y y-coordinate
* @param {Object} svg group container to append to
*/
TabularProfile.prototype.slope = function (x, y, set) {
var p = this.properties, paper = this.paper, path = paper.g(),
position = this.station.position, length = 42,
lineattr = {'stroke': p.grid_color, 'stroke-width': 2},
font = { fontSize: '18px', fontFamily: 'Helvetica, Arial' };
if (position && position.angle !== undefined && position.angle !== '') {
if (position.angle === 0) {
path.add(paper.line(x + length, y, x, y));
} else if (position.angle > 0) {
length = position.angle < 55 ? 42 : 30;
var rad = position.angle * Math.PI / 180, pos = {
x: x + Math.abs(Math.cos(rad) * length),
y: y + Math.abs(Math.sin(rad) * length)
};
path.add(paper.line(x, pos.y, pos.x, pos.y));
path.add(paper.line(pos.x, pos.y, x, y));
}
set.add(path.attr(lineattr));
var text = '';
if (this.station.position.direction !== '')
text += t(this.station.position.direction, 'directions');
if (position.angle !== '')
text += (text ? ' / ' : '') + position.angle + '°';
set.add(paper.text(x + length + 18 * .5, y + 16, text).attr(font));
}
};
/**
* Draw the text of the current layer and column, depending on the type of
* paramater different renderings are required.
*
* @method toString
* @private
* @param {Value} value The current value
* @param {String} type The parameter name
* @param {Number} offset Pixel offset of the current table column
* @param {Number} y Pixel y-coordinate of the layer top
* @param {Number} width Column width
* @param {Number} height Row height
* @param {Number} layer Layer index (for cross-reference)
* @return {Object} svg group container
*/
TabularProfile.prototype.toString = function (value, type, offset, y, width, height, layer) {
var content, string = '', p = this.properties, paper = this.paper;
var txt = p.table.font,
center = {
x: offset + width / 2 * p.fontsize,
y: y + p.fontsize - 1,
c: y + height / 2 + p.fontsize / 2 - 1,
above: y - 2
};
if (center.c > p.table.origin.y) center.c = p.table.origin.y - p.fontsize / 2;
if (center.c < (p.table.top + p.fontsize)) center.c = p.table.top + p.fontsize;
if (type === 'grainshape') {
content = paper.text(center.x, center.c, value.code).attr(
{ fontSize: (p.fontsize + 3) + 'px', fill: p.font_color,
textAnchor: 'middle', fontFamily: 'snowsymbolsiacs' });
} else if (type === 'grainsize') {
if (value.avg && value.avg !== value.max) {
string += round(value.avg, 2);
if (value.max) string += ' - ';
}
if (value.max) string += round(value.max, 2);
content = paper.text(center.x, center.c, string).attr(txt);
} else if (type === 'density') {
if (value.value || value.value === 0)
string += Math.round(value.value);
content = paper.text(center.x, center.c, string).attr(txt);
} else if (type === 'ct') {
content = this.cttxt(paper, p, center, txt, value);
} else if (type === 'saw' || type === 'sf') {
content = this.stbtxt(paper, p, center, txt, value);
} else if (type === 'ect') {
content = this.ecttxt(paper, p, center, txt, value);
} else if (type === 'rb') {
content = this.rbtxt(paper, p, center, txt, value);
} else if (type === 'flags') {
content = this.flagstxt(paper, p, center, txt, value, offset);
} else if (type === 'wetness') {
var val = (value.index === 1) ? '' : value.index;
if (Common.defaults.LWC_format !== '1 - 5') {
val = value.code;
}
content = paper.text(center.x, center.c, val).attr(txt);
} else if (type === 'threads') {
content = paper.text(center.x, center.y, t(value.value)).attr(txt);
} else if (type === 'comments') {
content = this.commentstxt(paper, p, center, txt, value, offset, width, height, layer);
} else {
if (value.value && /[a-z]/i.test(value.value)) {
string += t(value.value);
} else if (value.value || value.value === 0) {
string += round(value.value, 2);
}
content = paper.text(center.x, center.c, string).attr(txt);
}
return content;
};
/**
* Render the cell text for the comments type.
*
* @method commentstxt
* @private
* @param {Object} paper SnapSVG paper
* @param {Object} p Properties object
* @param {Object} center Pixel coordinates used for positioning
* @param {Object} txt Font object
* @param {Value} layer The layer object,
* @param {Value} offsetx The x-coord at the left of the column
* @param {Value} width Max-width number of fontsizes
* @param {Value} height Height of the cell in pixel
* @param {Number} lindex Layer index (for cross referencing)
* @return {Object} svg group container
*/
TabularProfile.prototype.commentstxt = function (paper, p, center, txt, layer,
offsetx, width, height, lindex) {
var critical = this.profile.critical && this.profile.critical.layers.length
&& this.profile.critical.layers[lindex]
&& this.profile.critical.layers[lindex].text || '';
if (critical) critical = 'Critical layer (' + critical + '); ';
var content = paper.g(), string = critical + (layer.value || ''),
textbox = paper.text(0, -100, ''),
words = string.split(/[ ]/) || [], i, vh = 1, last = '', result = '',
lines = Math.max(Math.floor(height / (p.fontsize * 1.2)), 5);
width = width * p.fontsize - 10;
for (i = 0; i < words.length; ++i) {
last = last + words[i] + ' ';
textbox.attr('text', last);
if (textbox.getBBox().width > width) {
++vh;
if (vh >= lines) {
result += '…';
--vh;
break;
}
result += '\n' + words[i] + ' ';
last = '';
} else {
result += words[i] + ' ';
}
}
textbox.remove(); // only used for layouting
content.add(paper.multitext(offsetx + 5, center.y, result)
.attr(txt)
.attr({ 'text-anchor': 'start' }));
return content;
};
/**
* Render the cell text for the flags parameter. Either asterisks or flags.
*
* @method flagstxt
* @private
* @param {Object} paper SnapSVG paper
* @param {Object} p Properties object
* @param {Object} center Pixel coordinates used for positioning
* @param {Object} txt Font object
* @param {Value} layer The flags value object
* @param {Number} offset The column offset
* @return {Object} svg group container
*/
TabularProfile.prototype.flagstxt = function (paper, p, center, txt, layer, offset) {
var content = paper.g(), string = '', ii,
labels = ['gs', 'h', 'gt', 'dgs', 'dh', 'depth'],
display_flags = (Common.defaults.display_yellow_flags === 'as flags');
for (ii = 0; ii < labels.length; ++ii) {
if (!layer || !layer.value[labels[ii]]) continue;
if (display_flags) {
content.add(
paper.text(p.fontsize * ii + offset + p.fontsize + 1, center.y + p.fontsize * 0.75, '*')
.attr(txt)
.attr({'font-size': p.fontsize * 1.5,
'fill': layer.value.sum > 4 ? '#DD0000' : '#000'})
);
} else {
string += '*';
}
}
if (!display_flags && layer) {
content.add(
paper.text(center.x, center.y + p.fontsize * 0.75, string).attr(txt)
.attr({'fill': layer.value.sum > 4 ? '#DD0000' : '#000',
'font-size': p.fontsize * 1.5})
);
}
return content;
};
/**
* Render the cell text for the CT parameter.
*
* @method cttxt
* @private
* @param {Object} paper SnapSVG paper
* @param {Object} p Properties object
* @param {Object} center Pixel coordinates used for positioning
* @param {Object} txt Font object
* @param {Value} layer The flags value object
* @return {Object} svg group container
*/
TabularProfile.prototype.cttxt = function (paper, p, center, txt, layer) {
var content = paper.g(), string = layer.text;
content.add(paper.multitext(center.x, center.y, string).attr(txt));
return content;
};
/**
* Generic rendering for stability types.
*
* @method stbtxt
* @private
* @param {Object} paper SnapSVG paper
* @param {Object} p Properties object
* @param {Object} center Pixel coordinates used for positioning
* @param {Object} txt Font object
* @param {Value} layer The flags value object
* @return {Object} svg group container
*/
TabularProfile.prototype.stbtxt = function (paper, p, center, txt, layer) {
var content = paper.g(), string = layer.text;
if (layer.character) string += '\n' + layer.character;
content.add(paper.multitext(center.x, center.y, string).attr(txt));
return content;
};
/**
* Render the cell text for the ECT parameter.
*
* @method ecttxt
* @private
* @param {Object} paper SnapSVG paper
* @param {Object} p Properties object
* @param {Object} center Pixel coordinates used for positioning
* @param {Object} txt Font object
* @param {Value} layer The flags value object
* @return {Object} svg group container
*/
TabularProfile.prototype.ecttxt = function (paper, p, center, txt, layer) {
var content = paper.g(), style = {}, text;
if (layer.character === 'P')
style = {
'fill': layer.value <= 30 ? '#d00' : p.font_color,
'font-weight': layer.value <= 21 ? 'bold' : 'normal'
};
text = layer.text;
if (Common.defaults.ECT_format === 'Swiss') text = layer.swisscode || layer.text;
content.add(paper.text(center.x, center.y, text).attr(txt).attr(style));
return content;
};
/**
* Render the cell text for the Rutschblock parameter.
*
* @method rbtxt
* @private
* @param {Object} paper SnapSVG paper
* @param {Object} p Properties object
* @param {Object} center Pixel coordinates used for positioning
* @param {Object} txt Font object
* @param {Value} layer The flags value object
* @return {Object} svg group container
*/
TabularProfile.prototype.rbtxt = function (paper, p, center, txt, layer) {
var content = paper.g(), string = '', style = {}, y = center.y;
if (layer.releasetype === 'WB') style = { 'fill': '#d00' };
if (layer.value <= 4 && layer.value > 0)
style = {
'fill': '#d00',
'font-weight': layer.releasetype === 'WB' ? 'bold' : 'normal'
};
content.add(paper.text(center.x, center.above, layer.text).attr(txt).attr(style));
if (layer.releasetype) string += t(layer.releasetype) + '\n';
if (layer.character) string += t(layer.character);
if (layer.top) {
var text = paper.multitext(center.x, y, string);
text.attr(txt).attr('font-size', p.fontsize - 2);
content.add(text);
}
return content;
};
/**
* This method enables the mousemove event on the area spanning
* the table.
*
* @method mouseon
* @private
*/
TabularProfile.prototype.mouseon = function () {
var self = this, canvas = this.canvas, last, offset, current;
this.mat.mousemove(function (e) {
clearTimeout(self.timer);
self.timer = setTimeout( function () {
offset = e.pageY - canvas.offset().top;
self.highlight(offset);
if (self.bargraph) {
current = self.bargraph.index.call(self.bargraph, self.index(offset));
last = current ?
self.bargraph.highlight.call(self.bargraph, last, current) :
self.bargraph.unhighlight.call(self.bargraph, last);
}
}, 5);
});
this.mat.dblclick(function () {
if (current && self.dblclickhandler) self.dblclickhandler(current);
});
this.mat.mouseout(function () {
clearTimeout(self.timer);
setTimeout( function () {
self.unhighlight();
if (self.bargraph) self.bargraph.unhighlight.call(self.bargraph, last);
}, 30);
});
};
/**
* This method returns the reference to the blanket object that spans the
* given y-coordinate - or undefined if no table row matches the y-coordinate.
*
* @method index
* @private
* @param {Number} y y-coordinate as pixel value
* @return {Number} key of this.blanket object to reference actual object
*/
TabularProfile.prototype.index = function (y) {
var blanket = this.blanket;
for (var key in blanket) {
if (blanket.hasOwnProperty(key) && blanket[key].area) {
var area = blanket[key].area,
upper = area.top, lower = area.bottom;
if (y >= upper && y < lower) return key;
}
}
return undefined;
};
/**
* Remove any highlights and the snow height label, if there is one.
* @method unhighlight
* @private
*/
TabularProfile.prototype.unhighlight = function () {
if (!this.activekey || !this.blanket) return;
var blanket = this.blanket[this.activekey];
blanket.elements.forEach(function (e) { e.attr({'stroke-width': 0.5}); });
blanket.connector.attr(
{
strokeDasharray: '1,1',
strokeWidth: 0.5
});
if (blanket.rect) blanket.rect.remove();
this.activekey = undefined;
if (this.hs) this.hs();
};
/**
* Highlight the table row that spans the passed y-coordinate pixel.
* Draw snow height label (this.hs()), if applicable.
*
* @method highlight
* @private
* @param {Number} y y-coordinate as pixel value
*/
TabularProfile.prototype.highlight = function (y) {
var p = this.properties, blanket = this.blanket, key = this.index(y);
if ((!key && this.activekey) ||
(key && this.activekey && key !== this.activekey)) {
this.unhighlight();
}
if (key && key !== this.activekey) {
var area = blanket[key].area;
var upper = area.top, lower = area.bottom;
blanket[key].elements.forEach(function (e) { e.attr({'stroke-width': 2}); });
blanket[key].connector.attr(
{
'stroke-dasharray': '',
'stroke-width': 2
});
blanket[key].rect = this.paper
.rect(p.table.origin.x + .5, upper + 1, p.table.bwidth, lower - upper - .5)
.attr({fill: 'blue', fillOpacity: 0.1, stroke: 'none', pointerEvents: 'none'});
this.activekey = key;
if (this.hs) this.hs(key);
}
};
/**
* Get the pixel value for a given snow height.
*
* @method y
* @private
* @param {Number} y Snow height
* @return {Number} Pixel value
*/
TabularProfile.prototype.y = function (y) {
// if (this.range)
// console.dir('y(' + y + ') requested, range: ' + this.range.axis.min + ' / ' + this.range.axis.max);
if (this.range) return this.range.axis.pixel(y);
var p = this.properties.table;
return Math.round(p.height + p.top - p.height / p.range * (y - p.min));
};
// --- Helpers ---
// --- Module Exports ---
niviz.TabularProfile = TabularProfile;
}(niviz, moment));