/*
* 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/>.
*/
(function (niviz) {
'use strict';
// --- Module Dependencies ---
var properties = Object.defineProperties;
var keys = Object.keys;
var extend = niviz.util.extend;
var Value = niviz.Value;
var isUndefOrNull = niviz.util.isUndefinedOrNull;
/** @module niviz */
/**
* A feature of a snow Profile.
*
* @class Profile
* @constructor
*/
function Feature(type, p) {
/**
* Reference to the profile, this is necessary because some features
* need to have access to dynamic properties such as snow height
*
* @property profile
* @type Profile
*/
this.profile = p;
/**
* @property type
* @type String
*/
this.type = type;
/**
* The features value stack. Values are assumed
* to be stacked from the bottom to top.
*
* @property layers
* @type Array
*/
this.layers = [];
/**
* Max numeric value. Managed by `.push`.
*
* @property max
* @type Number
*/
this.max = Number.NEGATIVE_INFINITY;
/**
* Min numeric value. Managed by `.push`.
*
* @property min
* @type Number
*/
this.min = Number.POSITIVE_INFINITY;
/**
* Observations and metadata regarding this profile, such as
* comments, type of profile, method of measurement
*
* @property info
* @type {Object}
*/
this.info = {
pointprofile: false
};
if (this.definition.info)
extend(this.info, this.definition.info);
this.$indexzero = null; // index of first layer above 0
this.$showsoil = true; // show all soil layers
}
properties(Feature, {
/**
* @property type
* @type Object
* @static
*/
type: {
value: {}
},
/**
* @property types
* @type Array<String>
* @static
*/
types: {
get: function () { return keys(this.type); }
}
});
/**
* Registers a feature definition. A definition should
* contain a `name`, a `symbol` and, optionally, a
* `Constructor` function to create new values.
*
* @method register
* @chainable
* @static
*
* @param {String} type
* @param {Object} definition
*/
Feature.register = function (type, definition) {
if (typeof definition.Constructor !== 'function')
definition.Constructor = Value;
Feature.type[type] = definition;
return this;
};
properties(Feature.prototype, {
/**
* @property definition
* @type Object
*/
definition: {
get: function () { return Feature.type[this.type]; }
},
/**
* @property name
* @type String
*/
name: {
get: function () { return this.definition.name; }
},
/**
* @property symbol
* @type String
*/
symbol: {
get: function () { return this.definition.symbol; }
},
/**
* @property symbol
* @type String
*/
unit: {
get: function () { return this.definition.unit; }
},
/**
* @property top
* @type Number
*/
top: {
get: function () {
return this.layers.length ? this.layers[this.layers.length - 1].top : 0;
}
},
/**
* @property bottom
* @type Number
*/
bottom: {
get: function () {
if (this.layers.length) {
if (this.layers[0].bottom !== undefined)
return this.layers[0].bottom;
return this.layers[0].top;
}
return 0;
}
},
/**
* @property height
* @type Number
*/
height: {
get: function () {
if (this.type === 'smp' || this.type === 'ramm') //depthTop types
return this.layers.length && this.layers[0].depth || 0;
return this.top - this.bottom;
}
},
/**
* All relevant metadata in one object
*
* @property meta
* @type Object
*/
meta: {
get: function () {
var obj = JSON.parse(JSON.stringify(this.info)); // deep copy
return obj;
},
set: function (obj) {
if (!obj) return;
this.info = JSON.parse(JSON.stringify(obj)) || {};
if (this.info.pointprofile === undefined)
this.info.pointprofile = false;
}
}
});
/**
* Delete a certain layer
*
* @method rmlayer
* @chainable
* @param {Number} i layer index
*/
Feature.prototype.rmlayer = function (i) {
if (i >= this.layers.length)
throw new Error('index out of bounds');
this.layers.splice(i, 1);
return this;
};
/**
* Edit a certain layer
*
* @method editlayer
* @chainable
* @param {Number} i layer index
* @param {Object} values layer data
*/
Feature.prototype.editlayer = function (i, values) {
var current = this.layers[i] || null;
if (current) {
if (values.top !== undefined) current.top = values.top;
if (values.bottom !== undefined) current.bottom = values.bottom;
Object.keys(values).forEach(function (key) {
if (key !== 'index' && key !== 'top' && key !== 'bottom')
current[key] = values[key];
});
this.sort();
} else {
throw new Error('invalid layer selected for editing');
}
return this;
};
/**
* @method layerheight
* @return {Number} The sum of all layer heights
*/
Feature.prototype.layerheight = function () {
if (!this.layers.length) return 0;
return this.layers.reduce(function (acc, cur) {
return acc + cur.thickness;
}, 0);
};
/**
* Calculate average of the values of all layers, weighting according to layer thickness.
*
* @method average
* @return {Number} The average value over all layers or undefined if not available
*/
Feature.prototype.average = function () {
var sum = 0, height = this.height;
if (this.layers.length) {
this.layers.forEach(function (current) {
sum += (current.top - current.bottom) / height * current.value;
});
return sum;
}
return undefined;
};
/**
* Retrieve value at certain depth/height of snowpack layer
*
* @method atheight
* @param {Number} [height] depth or height of the desired layer
* @param {Boolean} [fromtop] true if height is to be interpreted as depth
* @return {Number} The value of the desired layer or null
*/
Feature.prototype.atheight = function (height, fromtop) {
var i, ii = this.layers && this.layers.length, layer;
if (ii) {
for (i = 0; i < ii; ++i) {
layer = this.layers[i];
if (fromtop) {
var depthbottom = this.top - layer.bottom, depthtop = this.top - layer.top;
if (depthbottom >= height && depthtop <= height)
return layer.avg || layer.value || null;
} else {
if (layer.bottom <= height && layer.top >= height)
return layer.avg || layer.value || null;
}
}
}
return null;
};
/**
* Adds a new layer value.
*
* Note: This method assumes that layers are pushed in no particular order;
* it used to assume that layers are pushed either from top to bottom,
* or bottom to top!
*
* @method push
* @chainable
*
* @param {Number,Object,Array} top or top/value/bottom object
* @param {Object} [value]
* @param {Number} [bottom]
*/
Feature.prototype.push = function (top, value, bottom) {
var i, ii;
// --- Normalize Input Arguments
if (arguments.length < 2 && typeof top === 'object' && !isUndefOrNull(top)) {
if (Array.isArray(top)) {
if (top.length === 3) bottom = top[2];
value = top[1];
top = top[0];
} else {
bottom = top.bottom;
value = top.value;
top = top.top;
}
}
value = new this.definition.Constructor(top, value, bottom, this.profile);
var num = value.numeric;
if (num > this.max) this.max = num;
if (num < this.min) this.min = num;
if (this.$indexzero === null && top > 0) this.$indexzero = this.layers.length;
if (value.top >= this.top || value.top === undefined) { // undefined possible (RB, ECT)
this.layers.push(value);
} else if (value.top <= this.bottom) {
this.layers.unshift(value);
} else { // look for insertion position, HACK: implement binary insert
var inserted = false;
for (i = 0, ii = this.layers.length; i < ii; i++) {
if (value.top <= this.layers[i].top) {
this.layers.splice(i, 0, value);
inserted = true;
break;
}
}
// layer has not been inserted (e. g. comparing undefined top values):
if (!inserted) this.layers.push(value);
}
//this.layers[(value.top < this.top) ? 'unshift' : 'push'](value);
return this;
};
/**
* Sorts the layers according to top
*
* @method sort
* @chainable
*
*/
Feature.prototype.sort = function () {
this.layers.sort(function (a, b) {
if (a.top === b.top && ((a.bottom || a.bottom === 0) && (b.bottom || b.bottom === 0)))
return a.bottom - b.bottom;
return a.top - b.top;
});
return this;
};
/**
* Toggle whether to expose all layers or just the ones above the ground,
* i. e. with a top value > 0.
*
* @method showsoil
* @chainable
*
* @param {Boolean} enable true = expose all layers, false = hide soil layers
*/
Feature.prototype.showsoil = function (enable) {
var i, ii = this.layers.length;
if (this.$showsoil && !enable) {
this.$layers = this.layers; // buffer reference
this.layers = [];
if (this.$indexzero !== null)
for (i = this.$indexzero; i < ii; ++i)
this.layers.push(this.$layers[i]);
} else if (!this.$showsoil && enable) {
this.layers = this.$layers;
delete this.$layers;
}
this.$showsoil = enable;
return this;
};
/**
* Adds a list of layer values.
*
* @method concat
* @chainable
*
* @param {Array} values
*/
Feature.prototype.concat = function (values) {
for (var i = 0, ii = values.length; i < ii; ++i)
this.push.call(this, values[i]);
return this;
};
/**
* Find layer closest to height passed in cm
*
* @method index
*
* @param {float} top in cm
* @return {int} Index of closest layer
*/
Feature.prototype.index = function (top) {
var i, ii, diff, min, mini;
if (top === undefined) return mini;
for (i = 0, ii = this.layers.length; i < ii; ++i) {
diff = Math.abs(this.layers[i].top - top);
if (diff <= min || min === undefined) {
min = diff;
mini = i;
}
if (mini !== i) break; // diff is becoming bigger
}
return mini;
};
Feature.prototype.toJSON = function () {
return { meta: this.meta, layers: this.layers };
};
// --- Feature Definitions ---
Feature
.register('grainshape', {
name: 'Grain shape', symbol: 'F', Constructor: Value.GrainShape
})
.register('grainsize', {
name: 'Grain size', symbol: 'E', Constructor: Value.GrainSize, unit: 'mm'
})
.register('density', {
name: 'Snow density', symbol: 'ρ', unit: 'kg/m\u00b3',
info: { method: 'other' }
})
.register('gradient', {
name: 'Temperature gradient', symbol: '\u2207T', unit: '°C/m'
})
.register('hardness', {
name: 'Snow hardness', symbol: 'R', Constructor: Value.Hardness, unit: 'N'
})
.register('ramm', {
name: 'Rammsonde', symbol: 'Rammsonde', Constructor: Value.Ramm, unit: 'N'
})
.register('wetness', {
name: 'Liquid water content', symbol: '\u03B8', Constructor: Value.Wetness, unit: '%',
info: { method: 'other' }
})
.register('ssa', {
name: 'Specific surface area', symbol: 'SSA', unit: 'm\u00b2/kg',
info: { method: 'other' }
})
.register('sk38', {
name: 'Stability Class (Snowpack)', symbol: 'SK38',
Constructor: Value.SK38
})
.register('ct', {
name: 'Compression Test', symbol: 'CT',
Constructor: Value.CT
})
.register('ect', {
name: 'Extended Column Test', symbol: 'ECT', Constructor: Value.ECT
})
.register('rb', {
name: 'Rutschblock', symbol: 'RB', Constructor: Value.Rutschblock
})
.register('sf', {
name: 'Shear Frame Test', symbol: 'SF', Constructor: Value.ShearFrame
})
.register('saw', {
name: 'Propagation Saw Test', symbol: 'Saw', Constructor: Value.Saw
})
.register('threads', {
name: 'Threads', symbol: 'Threads'
})
.register('temperature', {
name: 'Snow temperature', symbol: 'Ts', unit: '°C',
info: { pointprofile: true }
})
.register('smp', {
name: 'SnowMicroPen', symbol: 'SMP', unit: 'N', Constructor: Value.SMP,
info: { pointprofile: true }
})
.register('impurity', {
name: 'Impurity', symbol: 'IMP', unit: '%',
info: { method: 'other', impuritytype: 'Isotopes', fractiontype: 'massFraction' }
})
.register('thickness', {
name: 'Layer thickness', symbol: '\u2195', unit: 'cm'
})
.register('dendricity', {
name: 'Dendricity', symbol: 'Dendricity'
})
.register('comments', {
name: 'Comment', symbol: 'Comments'
})
.register('sphericity', {
name: 'Sphericity', symbol: 'Sphericity'
})
.register('bondsize', {
name: 'Bondsize', symbol: 'Bondsize'
})
.register('flags', {
name: 'Flags', symbol: 'Flags'
})
.register('cn', {
name: 'Coordination number', symbol: 'CN'
})
.register('critical', {
name: 'Critical Layer', symbol: '-'
})
.register('hardnessBottom', {
name: 'Snow hardness', symbol: 'R', Constructor: Value.Hardness, unit: 'N'
});
// --- Module Exports ---
niviz.Feature = Feature;
}(niviz));