/*
* 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, 15]*/
(function (niviz, moment) {
'use strict';
// --- Module Dependencies ---
var properties = Object.defineProperties;
var FeatureSet = niviz.FeatureSet;
var Feature = niviz.Feature;
var assert = niviz.util.assert;
var pick = niviz.util.pick;
/** @module niviz */
/**
* A Snow Profile.
*
* @class Profile
* @constructor
*/
function Profile(date) {
/**
* @property date
* @type {Moment}
*/
this.date = date || moment();
/**
* The height at the top of the topmost layer in the profile,
* measured from the ground.
*
* @property top
* @type {Number}
*/
this.top = 0;
/**
* The height at the bottom of the lowest layer in the profile,
* measured from the ground. Not all values have a bottom value,
* thus initialization with undefined.
*
* @property bottom
* @type {Number}
*/
this.bottom = undefined;
/**
* Observations and metadata regarding this profile, such as
* comments, sky conditions, air temperature, etc.
*
* @property info
* @type {Object}
*/
this.info = {
profile_gmlid: niviz.CAAML.defaults.profile_gmlid,
observer_gmlid: niviz.CAAML.defaults.observer_gmlid,
operation_gmlid: niviz.CAAML.defaults.operation_gmlid
};
/**
* A list of all avaialable features for this profile.
* The list is updated automatically when adding
* features using the `.add` method.
*
* @property features
* @type Array<String>
*/
this.features = [];
}
Profile.flaggs = ['FC', 'FCxr', 'SH', 'DH'];
/**
* All features that make up the stratigraphic features
*
* @property stratigraphic
* @type {Array.<string>}
*/
Profile.stratigraphic = ['wetness', 'hardness', 'grainshape', 'grainsize', 'comments'];
/**
* Calculate the yellow flags within a layer, given the hand hardness,
* grainsize and grainshape values.
*
* @static
* @method layerflags
* @param {Hardness}
* @param {Grainsize}
* @param {Grainshape}
* @return {Object} An object containing the layer flags and the grainsize average
*/
Profile.layerflags = function (hardness, grainsize, grainshape) {
var gs = 0, gt = 0, h = 0, gsavg = grainsize.avg || 0;
if (grainsize.max) gsavg = (gsavg + grainsize.max) / 2;
if (grainshape.value.primary === 'IF') gsavg = 2; // assume 2mm for ice layers
if (gsavg >= 1.25) gs++;
if (hardness.value < 2) h++;
if (Profile.flaggs.indexOf(grainshape.value.primary) > -1) gt++;
if (grainshape.value.primary === 'MFcr'
&& Profile.flaggs.indexOf(grainshape.value.secondary) > -1) gt++;
return { gsavg: gsavg, gs: gs, gt: gt, h: h, sum: gs + gt + h };
};
Profile.windspeed = [
{ symbol: 'C', text: 'Calm', min: 0, max: 0 },
{ symbol: 'L', text: 'Light', min: 0, max: 7.5 },
{ symbol: 'M', text: 'Moderate', min: 7.5, max: 11.5 },
{ symbol: 'S', text: 'Strong', min: 11.5, max: 17 },
{ symbol: 'X', text: 'Extreme', min: 17, max: 100 }
];
Profile.convert_windspeed = function (data) {
if (data === 0) return Profile.windspeed[0];
else if (data > 0 && data <= 7.5) return Profile.windspeed[1];
else if (data > 7.5 && data <= 11.5) return Profile.windspeed[2];
else if (data > 11.5 && data <= 17) return Profile.windspeed[3];
else if (data > 17) return Profile.windspeed[4];
};
properties(Profile.prototype, {
/**
* The height of the snow profile.
*
* @property height
* @type {Number}
*/
height: {
get: function () { return this.top - this.bottom; }
},
/**
* Snow height is the total snow height or
* 0 if snow height is unknown
*
* @property hs
* @type {Number}
*/
hs: {
get: function hs$get () {
if (this.top && ((Number.isFinite && Number.isFinite(this.top)) || isFinite(this.top)))
return this.top;
return 0;
}
},
/**
* Snow water equivalent calculated by:
* SWE [mm] = Snow Depth [m] * Density [kg / m³]
*
* @property swe
* @type {Number}
*/
swe: {
get: function swe$get () {
if (!this.density || !this.density.elements[0]) return null;
var avg = this.density.elements[0].average();
if (avg) return this.density.elements[0].height / 100 * avg;
return null;
}
},
/**
* Accessor for the stability features:
* CT, ECT, RB, SF and Saw.
*
* @property stability
* @type {Array.<Value>}
*/
stability: {
get: function () {
var tests = [];
if (this.ct) tests.push(this.ct);
if (this.ect) tests.push(this.ect);
if (this.rb) tests.push(this.rb);
if (this.sf) tests.push(this.sf);
if (this.saw) tests.push(this.saw);
if (this.flags) tests.push(this.flags);
return tests;
}
},
/**
* Accessor to the yellow flags feature, which is calculated
* on the fly
*
* @property flags
* @type {Feature}
*/
flags: {
get: function () {
if (!this.grainshape || !this.hardness || !this.grainsize
|| this.grainshape.layers.length !== this.hardness.layers.length
|| this.grainsize.layers.length !== this.hardness.layers.length)
return undefined;
if (!Feature.type['flags'])
Feature.register('flags', { name: 'flags', symbol: 'N' });
var feature = new Feature('flags', this), grainshape = this.grainshape.layers,
grainsize = this.grainsize.layers, hardness = this.hardness.layers,
depth = this.top - 100;
for (var ii = 0; ii < this.grainshape.layers.length - 1; ++ii) {
var val = { gs: 0, gt: 0, h: 0, dgs: 0, dh: 0, depth: 0 }, fail = ii, other = ii + 1;
// Layer properties
var c = Profile.layerflags(hardness[ii], grainsize[ii], grainshape[ii]), gsavg = 0,
n = Profile.layerflags(hardness[other], grainsize[other], grainshape[other]);
if (n.sum > c.sum) {
val.gs = n.gs; val.gt = n.gt; val.h = n.h; val.sum = n.sum;
gsavg = n.gsavg;
other = [fail, fail = other][0];
} else {
val.gs = c.gs; val.gt = c.gt; val.h = c.h; val.sum = c.sum;
gsavg = c.gsavg;
}
//Compare properties of the two layers adjacent to the interface
var ogsavg = grainsize[other].avg || 0;
if (grainsize[other].max) ogsavg = (ogsavg + grainsize[other].max) / 2;
// assume 2mm for ice layers
if (grainshape[other].value.primary === 'IF') ogsavg = 2;
if (Math.abs(gsavg - ogsavg) >= 0.75) val.dgs++;
if (Math.abs(hardness[fail].value - hardness[other].value) >= 2) val.dh++;
if (grainshape[ii].top > depth) val.depth++;
val.sum = val.sum + val.dgs + val.dh + val.depth;
feature.concat([[
this.grainshape.layers[ii].top,
val,
this.grainshape.layers[ii].bottom
]]);
}
return feature;
}
},
/**
* Accessor to the temperature gradient feature,
* which is calculated on the fly
*
* @property gradient
* @type {Feature}
*/
gradient: {
get: function () {
if (!this.temperature) return undefined;
var featureset = new FeatureSet('gradient', this);
var feature = new Feature('gradient', this), i, height, grad,
ii = this.temperature.layers.length - 1,
layers = this.temperature.layers;
for (i = 0; i < ii; ++i) {
height = (layers[i + 1].top - layers[i].top) / 100; // in m
grad = (layers[i + 1].value - layers[i].value) / height;
feature.concat([[
(layers[i + 1].top + layers[i].top) / 2,
grad
]]);
}
featureset.elements[0] = feature;
return featureset;
}
},
/**
* Collect all relevant metadata in one object
*
* @property meta
* @type Object
*/
meta: {
get: function () {
var obj = JSON.parse(JSON.stringify(this.info)); // deep copy
obj.date = moment(this.date);
['custom_meta', 'custom_loc', 'custom_snow'].forEach(function (p) {
delete obj[p];
});
return obj;
},
set: function (obj) {
if (!obj) return;
this.info = JSON.parse(JSON.stringify(obj)) || {};
delete this.info.date;
if (!obj.date || !moment(obj.date).isValid()) throw new Error('Invalid date');
this.date = moment(obj.date);
}
}
});
/**
* Add a feature without layers
*
* @method addfeature
* @chainable
*
* @param {String} type feature type
*/
Profile.prototype.addfeature = function (type, extrai) {
assert(!!Feature.type[type], 'Invalid feature type: ' + type);
assert(extrai || extrai === 0, 'Invalid feature type: ' + type);
if (!this[type]) this[type] = new FeatureSet(type, this);
var feature = new Feature(type, this);
if (!this[type].length) this.features.push(type);
this[type].elements[extrai] = feature;
return this;
};
/**
* Add feature values to this profile.
*
* @method add
* @chainable
*
* @param {String} type feature type
* @param {Array} layers values for each layer
*/
Profile.prototype.add = function (type, layers, extrai) {
assert(!!Feature.type[type], 'Invalid feature type: ' + type);
assert(Array.isArray(layers), 'Invalid layers array: ' + layers);
var feature;
if (!this[type]) this[type] = new FeatureSet(type, this);
feature = this[type].elements[extrai || 0] || new Feature(type, this);
feature.concat(layers);
if (feature.top > this.top) this.top = feature.top;
if (this.bottom === undefined || feature.bottom < this.bottom)
this.bottom = feature.bottom;
if (!this[type].length) this.features.push(type);
this[type].elements[extrai || 0] = feature;
return this.adjust();
};
/**
* Adjust top and bottom value by looping through all features
*
* @method adjust
* @chainable
*/
Profile.prototype.adjust = function () {
var self = this;
this.top = 0;
this.bottom = undefined;
if (this.features.length) {
this.top = Math.max.apply(Math, this.features.map( function(f) {
if (f === 'ramm' || f === 'smp') return 0; // depthTop types
var max = self[f] && self[f].top || 0;
return max;
}));
if (this.info.hs || this.info.hs === 0) this.top = this.info.hs;
var features = this.features.filter(function (f) {
return (self[f] && (self[f].bottom || self[f].bottom === 0));
});
this.bottom = Math.min.apply(Math, features.map( function(f) {
var min = self[f] && self[f].bottom || 0;
return min;
}));
}
if (this.info.hs || this.info.hs === 0) this.top = this.info.hs;
return this;
};
/**
* Delete certain Feature from a present FeatureSet
*
* @method rmfeature
* @chainable
*
* @param {String} type feature type
* @param {Number} ielement feature index
*/
Profile.prototype.rmfeature = function (type, ielement) {
if (ielement !== 0 && !ielement)
throw new Error('rmfeature requires a second parameter, was: ' + ielement);
if (!this[type] || !this[type].length > ielement)
throw new Error('Index out of bounds in rmfeature');
this[type].elements.splice(ielement, 1);
if (!this[type].length) {
delete this[type];
this.features.splice(this.features.indexOf(type), 1);
}
return this.adjust();
};
/**
* Add a layer to one of the features present
*
* @method addlayer
* @chainable
*
* @param {String} type feature type
* @param {Object} values layer data
*/
Profile.prototype.addlayer = function (type, values, ielement) {
var val = val = values.value;
if (!val && val !== 0) val = values[type];
if (type === '_stratigraphic') {
Profile.stratigraphic.forEach(function (p) {
this.addlayer(p, values);
}, this);
} else {
this.add(type, [[values.top, val, values.bottom]], ielement);
}
return this;
};
/**
* Delete a layer of one of the features present
*
* @method rmlayer
* @chainable
*
* @param {String} type feature type
* @param {Number} i layer index
*/
Profile.prototype.rmlayer = function (type, i, ielement) {
ielement = ielement || 0;
if (type === '_stratigraphic') {
Profile.stratigraphic.forEach(function (p) {
this.rmlayer(p, i);
}, this);
} else {
this[type].elements[ielement].rmlayer(i);
if (!this[type].elements[ielement].layers.length) {
this[type].elements.splice(ielement, 1);
}
if (!this[type].length) {
this.features.splice(this.features.indexOf(type), 1);
delete this[type];
}
}
return this.adjust();
};
/**
* Edit a layer of one of the features present
*
* @method editlayer
* @chainable
*
* @param {String} type feature type
* @param {Number} i layer index
* @param {Object} values layer data
*/
Profile.prototype.editlayer = function (type, i, values, ielement) {
ielement = ielement || 0;
if (type === '_stratigraphic') {
Profile.stratigraphic.forEach(function (p) {
this.editlayer(p, i, values);
}, this);
} else {
if (this[type] && this[type].elements[ielement]) {
this[type].elements[ielement].editlayer(i, values);
} else {
throw new Error('type ' + type + ' does not exist in profile');
}
}
return this.adjust();
};
/**
* Toggle whether to expose all layers of all features present
* 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
*/
Profile.prototype.showsoil = function (enable) {
this.features.forEach(function (name) {
var featureset = this[name];
featureset.elements.length && featureset.elements.forEach(function (feature) {
feature.showsoil(enable);
});
}, this);
return this.adjust();
};
/**
* Whether or not the profile has the given `feature`.
*
* @method has
* @param {String} feature
*
* @returns {Boolean}
*/
Profile.prototype.has = function (feature) {
return !!(Feature.type[feature] && this[feature]);
};
Profile.prototype.toJSON = function () {
return pick(this, [ 'date', 'info' ].concat(this.features));
};
// --- Module Exports ---
niviz.Profile = Profile;
}(niviz, moment));