/*
* 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 Common = niviz.Common;
var round = niviz.util.round;
var log10 = niviz.util.log10;
/** @module niviz */
/**
* The static Grid class encompasses a few static methods to split up an axis
* into intervals of points in such a way that number aesthetics are preserved.
*
* @class Grid
* @static
*/
function Grid () {}
/**
* This function returns the number of points that should devide an axis
* of length 'pixels'.
*
* @static
* @method points
* @param {Number} pixels
* @return {Number}
*/
Grid.points = function (pixels) {
if (pixels < 200) return 5;
else return 10;
};
/**
* Split up the snow height uniformly into intervals.
*
* @method hsgrid
* @static
* @param {Station} station
* @param {Number} [itop] Pixel value of the top
* @param {Number} [ibottom] Pixel value of the bottom
* @param {Boolean} [autoscale] Whether to use autoscale (default: no)
* @return {Object} Object with max, min, divisions, height and heights properties
*/
Grid.hsgrid = function (station, itop, ibottom, autoscale) {
var top = station.top, bottom = station.bottom,
increment, heights = [], i, points = 0;
if (!autoscale) {
if (itop !== null && ibottom !== null) {
top = itop;
bottom = ibottom;
} else if (top === 0) { // for profiles without snow height
bottom = bottom ? Math.ceil((bottom - 20) / 20) * 20 : 0;
} else {
top = Math.max(Math.floor((top + 50) / 50) * 50, 200);
bottom = bottom ? Math.ceil((bottom - 50) / 50) * 50 : 0;
}
if (Common.defaults.max_snowheight) top = Common.defaults.max_snowheight;
} else if (top === 0) { // for profiles without snow height
bottom = bottom ? Math.ceil((bottom - 20) / 20) * 20 : 0;
}
var heights = Grid.smartLegend(bottom, top);
return {
max: heights[heights.length - 1],
min: heights[0],
divisions: heights.length,
height: heights[heights.length - 1] - heights[0],
heights: heights
};
};
/**
* Split up a given snow height interval uniformly into intervals.
*
* @method hsgrid
* @static
* @param {Number} [bottom] Bottom height
* @param {Number} [top] Top height
* @return {Object} Object with max, min, divisions, height and heights properties
*/
Grid.hsfixed = function (bottom, top) {
var increment, heights = [], i, points = 5;
increment = Math.round((top - bottom) / points);
for (i = 0; i < points; ++i)
heights.push(Math.round(bottom + i * increment));
heights.push(Math.round(top));
return {
max: top,
min: bottom,
divisions: points + 1,
height: top - bottom,
heights: heights
};
};
/**
* This function returns the min and max to be used when drawing the axis
* with values ranging from min to max, a pixel span of length pixels is to be
* utilized.
*
* @static
* @method points
* @param {Number} min Min value that needs to be on axis
* @param {Number} max Max value that needs to be on axis
* @param {Number} pixels Length of axis in pixels
* @return {Object} Object with min and max property
*/
Grid.minmax = function (min, max, pixels, points, mag) {
if (min === undefined || max === undefined) return { min: 0, max: 1 }; //HACK
var height = Math.max(max - min, 0.1), mag = mag || Math.floor(log10(Math.abs(height))),
pts = points || Grid.points(pixels), newmax, newmin;
newmax = Math.floor((max + Math.pow(10, mag)) / Math.pow(10, mag)) * Math.pow(10, mag);
if (min >= 0)
newmin = Math.ceil((min - Math.pow(10, mag)) / Math.pow(10, mag)) * Math.pow(10, mag);
else
newmin = Math.floor((min - Math.pow(10, mag)) / Math.pow(10, mag)) * Math.pow(10, mag);
if (max >= 0 && min >= 0) {
newmin = Math.max(0, newmin);
} else if (max <= 0 && min <= 0) {
newmax = Math.min(0, newmax);
} else { // make sure zero is displayed
var inc = (newmax - newmin) / pts, top = round(newmax / inc, 2),
bottom = round(newmin / inc, 2);
if (parseInt(top) !== top || (parseInt(bottom) !== bottom)) {
newmax = Math.round(top) * inc;
newmin = Math.round(bottom) * inc;
if (newmax < max || newmin > min) {
var log = Math.floor(log10(inc));
inc = Math.floor((inc + Math.pow(10, log)) / Math.pow(10, log)) * Math.pow(10, log);
newmax = Math.round(top) * inc;
newmin = Math.round(bottom) * inc;
}
}
}
return { min: newmin, max: newmax };
};
/**
* This function returns the steps to be used when drawing the axis
* with values ranging from minimum to maximum. Optionally a desired
* number of points can be handed over as third parameter.
*
* NOTE: This function has been copied from trunk/meteoio/Graphics.cc
* of the MeteoIO project: see https://models.slf.ch/p/meteoio
*
* @static
* @method smartLegend
* @param {Number} min Min value that needs to be on axis
* @param {Number} max Max value that needs to be on axis
* @param {Number} nb_labels Number of labels desired
* @return Array<Number> The points that include min max and guarantee a nice display
*/
Grid.smartLegend = function (minimum, maximum, nb_labels) {
var range = maximum - minimum, min_norm = minimum, max_norm = maximum,
decade_mult = null, step_norm, nb_labels_norm, height = 2000;
nb_labels = nb_labels || 7;
if (range > 0.) {
var decade = Math.floor(log10(range));
decade_mult = Math.pow(10., decade);
min_norm = Math.floor(minimum / decade_mult * 10.) / 10.;
max_norm = Math.ceil(maximum / decade_mult * 10.) / 10.; //between 0 & 10
var range_norm = max_norm - min_norm; //between 0 & 10
var step = range_norm / nb_labels; //between 0 & 10 / number of labels -> between 0 & 1
// for precision issues, it is necessary to keep step_norm
// as an interger -> we will always use step_norm/10
if (step <= 0.1) step_norm = 1;
else if (step <= 0.2) step_norm=2;
else if (step <= 0.5) step_norm=5;
else step_norm = 10;
if (max_norm >= 0) {
min_norm = Math.floor(min_norm / step_norm * 10) * step_norm / 10.;
} else {
min_norm = Math.ceil(min_norm / step_norm * 10) * step_norm / 10.;
}
if (max_norm >= 0) {
max_norm = Math.ceil(max_norm / step_norm * 10) * step_norm / 10.;
} else {
max_norm = Math.floor(max_norm / step_norm * 10) * step_norm / 10.;
}
//because min_norm might have been tweaked:
nb_labels_norm = Math.ceil((max_norm - min_norm) / step_norm * 10) + 1;
} else {
step_norm = 0;
decade_mult = 1.;
nb_labels_norm = 1;
}
var heights = [];
for (var l = 0; l < nb_labels_norm; l++) {
var level_val = (step_norm * l / 10. + min_norm) * decade_mult;
if (Math.abs(level_val) < (range * 1e-6)) level_val = 0.; //to get a nice 0 at zero
if (!Common.defaults.autoscale && Common.defaults.max_snowheight
&& level_val > Common.defaults.max_snowheight) {
heights.push(Common.defaults.max_snowheight);
break;
}
heights.push(level_val);
}
return heights;
}
/**
* This function returns the points for axis labels for a given axis
* with values ranging from axis.min to axis.max. Both logarithmic and
* non-logarithmic axes are fully supported.
*
* @static
* @method gridify
* @param {Axis} axis An axis object representing the ordinate of a paramter
* @return {Object} Object with Array<Number> heights and min, max, height properties
*/
Grid.gridify = function (axis) {
var pts = Grid.points(Math.abs(axis.pxmax - axis.pxmin)),
height = round(axis.max - axis.min, 2), inc = height / pts, heights = [], i, ii;
if (axis.log && axis.min === 0) // HACK
axis.min = Math.pow(10, Math.floor(log10(axis.max)) - 2);
if (axis.log && axis.max > 0 && axis.min > 0) {
var expmin = log10(axis.min), expmax = log10(axis.max),
mag = expmax - expmin, expinc = Math.ceil(mag), tmp = [], h;
heights.push(axis.min);
heights.push(axis.max);
if (mag > 1) {
for (i = 0, ii = expinc; i <= ii; ++i) {
if (mag < 3 && i) {
h = Math.pow(10, Math.ceil(expmin) + i) / 2;
if (heights.indexOf(height) === -1 ) tmp.push(h);
}
h = Math.pow(10, Math.ceil(expmin) + i);
if (heights.indexOf(h) === -1 ) tmp.push(h);
}
heights.splice.apply(heights, [1, 0].concat(tmp));
} else { // within one magnitude
while (heights.length < 8) {
if (heights.length * 2 >= 8) break;
tmp = [];
for (i = 1, ii = heights.length; i < ii; ++i) {
h = round((heights[i] - heights[i - 1]) / 2 + heights[i - 1], 2);
tmp.push(h);
}
for (i = 0, ii = tmp.length; i < ii; ++i)
heights.splice(2 * i + 1, 0, tmp[i]);
}
}
} else {
for (i = 0, ii = pts; i <= ii; ++i)
heights.push(round(axis.min + i * inc, 2));
}
return {
max: axis.max,
min: axis.min,
height: height,
heights: heights
};
};
// --- Module Exports ---
niviz.Grid = Grid;
}(niviz));