/** @module util */
/**
* Various utilities.
*
* @authors Alain Pitiot, Sotiri Bakagiannis, Thomas Pronk
* @version 2022.2.3
* @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org)
* @license Distributed under the terms of the MIT License
*/
import seedrandom from "seedrandom";
/**
* Syntactic sugar for Mixins
*
*
This is heavily adapted from: http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
*
* @param {Object} superclass
*
* @example
* class BaseClass { ... }
* let Mixin1 = (superclass) => class extends superclass { ... }
* let Mixin2 = (superclass) => class extends superclass { ... }
* class NewClass extends mix(BaseClass).with(Mixin1, Mixin2) { ... }
*/
export let mix = (superclass) => new MixinBuilder(superclass);
class MixinBuilder
{
constructor(superclass)
{
this.superclass = superclass;
}
/**
* @param mixins
* @returns {*}
*/
with(...mixins)
{
return mixins.reduce((c, mixin) => mixin(c), this.superclass);
}
}
/**
* Convert the resulting value of a promise into a tupple.
*
* @param {Promise} promise - the promise
* @return {Object[]} the resulting value in the format [error, return data]
* where error is null if there was no error
*/
export function promiseToTupple(promise)
{
return promise
.then((data) => [null, data])
.catch((error) => [error, null]);
}
/**
* Get a Universally Unique Identifier (RFC4122 version 4) or a pseudo-uuid based on a root
*
See details here: https://www.ietf.org/rfc/rfc4122.txt
*
* @param {string} [root] - the root, for string dependent pseudo uuid's
* @return {string} the uuid
*/
export function makeUuid(root)
{
// bonafide uuid v4 generator:
if (typeof root === "undefined")
{
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0, v = (c === "x") ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
else
{
// our in-house pseudo uuid generator:
const generator = seedrandom(root);
let digits = generator().toString().substring(2);
digits += generator().toString().substring(2);
return `${digits.substring(0, 8)}-${digits.substring(8, 12)}-4${digits.substring(12, 15)}-8${digits.substring(15, 18)}-${digits.substring(18, 30)}`;
}
}
/**
* Get the error stack of the calling, exception-throwing function.
*
* @return {string} the error stack as a string
*/
export function getErrorStack()
{
try
{
throw Error("");
}
catch (error)
{
// we need to remove the second line since it references getErrorStack:
let stack = error.stack.split("\n");
stack.splice(1, 1);
return JSON.stringify(stack.join("\n"));
}
}
/**
* Test if x is an 'empty' value.
*
* @param {Object} x the value to test
* @return {boolean} true if x is one of the following: undefined, [], [undefined]
*/
export function isEmpty(x)
{
if (typeof x === "undefined")
{
return true;
}
if (!Array.isArray(x))
{
return false;
}
if (x.length === 0)
{
return true;
}
if (x.length === 1 && typeof x[0] === "undefined")
{
return true;
}
return false;
}
/**
* Detect the user's browser.
*
*
Note: since user agent is easily spoofed, we use a more sophisticated approach, as described here:
* https://stackoverflow.com/a/9851769
*
* @param {Object} obj - the input object
* @return {number | number[]} the numerical form of the input object
*/
export function toNumerical(obj)
{
const response = {
origin: "util.toNumerical",
context: "when converting an object to its numerical form",
};
try
{
if (obj === null)
{
throw "unable to convert null to a number";
}
if (typeof obj === "undefined")
{
throw "unable to convert undefined to a number";
}
if (typeof obj === "number")
{
return obj;
}
const convertToNumber = (input) =>
{
const n = Number.parseFloat(input);
if (Number.isNaN(n))
{
throw `unable to convert ${input} to a number`;
}
return n;
};
if (Array.isArray(obj))
{
return obj.map(convertToNumber);
}
const arrayMaybe = turnSquareBracketsIntoArrays(obj);
if (Array.isArray(arrayMaybe))
{
return arrayMaybe.map(convertToNumber);
}
if (typeof obj === "string")
{
return convertToNumber(obj);
}
throw "unable to convert the object to a number";
}
catch (error)
{
throw Object.assign(response, { error });
}
}
/**
* Check whether a value looks like a number
*
* @param {*} input - Some value
* @return {boolean} Whether or not the value can be converted into a number
*/
export function isNumeric(input)
{
return Number.isNaN(Number(input)) === false;
}
/**
* Check whether a point lies within a polygon
*
We are using the algorithm described here: https://wrf.ecse.rpi.edu//Research/Short_Notes/pnpoly.html
*
* @param {number[]} point - the point
* @param {Object} vertices - the vertices defining the polygon
* @return {boolean} whether or not the point lies within the polygon
*/
export function IsPointInsidePolygon(point, vertices)
{
const x = point[0];
const y = point[1];
let isInside = false;
for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++)
{
const xi = vertices[i][0], yi = vertices[i][1];
const xj = vertices[j][0], yj = vertices[j][1];
const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect)
{
isInside = !isInside;
}
}
return isInside;
}
/**
* Shuffle an array, or a portion of that array, in place using the Fisher-Yastes's modern algorithm
*
See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
*
* @param {Object[]} array - the input 1-D array
* @param {Function} [randomNumberGenerator= undefined] - A function used to generated random numbers in the interval [0, 1). Defaults to Math.random
* @param [startIndex= undefined] - start index in the array
* @param [endIndex= undefined] - end index in the array
* @return {Object[]} the shuffled array
*/
export function shuffle(array, randomNumberGenerator = undefined, startIndex = undefined, endIndex = undefined)
{
// if array is not an array, we return it untouched rather than throwing an exception:
if (!array || !Array.isArray(array))
{
return array;
}
if (typeof startIndex === "undefined")
{
startIndex = 0;
}
if (typeof endIndex === "undefined")
{
endIndex = array.length - 1;
}
if (typeof randomNumberGenerator === "undefined")
{
randomNumberGenerator = Math.random;
}
for (let i = endIndex; i > startIndex; i--)
{
const j = Math.floor(randomNumberGenerator() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
/**
* linspace
*
* @name module:util.linspace
* @function
* @public
* @param {Object[]} startValue, stopValue, cardinality
* @return {Object[]} an array from startValue to stopValue with cardinality steps
*/
export function linspace(startValue, stopValue, cardinality) {
var arr = [];
var step = (stopValue - startValue) / (cardinality - 1);
for (var i = 0; i < cardinality; i++) {
arr.push(startValue + (step * i));
}
return arr;
}
/**
* Pick a random value from an array, uses `util.shuffle` to shuffle the array and returns the last value.
*
* @param {Object[]} array - the input 1-D array
* @param {Function} [randomNumberGenerator = undefined] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random
* @return {Object[]} a chosen value from the array
*/
export function randchoice(array, randomNumberGenerator = undefined)
{
if (randomNumberGenerator === undefined)
{
randomNumberGenerator = Math.random;
}
const j = Math.floor(randomNumberGenerator() * array.length);
return array[j]
}
/**
* Get the position of the object, in pixel units
*
* @param {Object} object - the input object
* @param {string} units - the units
* @returns {number[]} the position of the object, in pixel units
*/
export function getPositionFromObject(object, units)
{
const response = {
origin: "util.getPositionFromObject",
context: "when getting the position of an object",
};
try
{
if (typeof object === "undefined")
{
throw "cannot get the position of an undefined object";
}
let objectWin = undefined;
// the object has a getPos function:
if (typeof object.getPos === "function")
{
units = object.units;
objectWin = object.win;
object = object.getPos();
}
// convert object to pixel units:
return to_px(object, units, objectWin);
}
catch (error)
{
throw Object.assign(response, { error });
}
}
/**
* Convert the position to pixel units.
*
* @param {number[]} pos - the input position
* @param {string} posUnit - the position units
* @param {Window} win - the associated Window
* @param {boolean} [integerCoordinates = false] - whether or not to round the position coordinates.
* @returns {number[]} the position in pixel units
*/
export function to_px(pos, posUnit, win, integerCoordinates = false)
{
const response = {
origin: "util.to_px",
context: "when converting a position to pixel units",
};
let pos_px;
if (posUnit === "pix")
{
pos_px = pos;
}
else if (posUnit === "norm")
{
pos_px = [pos[0] * win.size[0] / 2.0, pos[1] * win.size[1] / 2.0];
}
else if (posUnit === "height")
{
const minSize = Math.min(win.size[0], win.size[1]);
pos_px = [pos[0] * minSize, pos[1] * minSize];
}
else
{
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
}
if (integerCoordinates)
{
return [Math.round(pos_px[0]), Math.round(pos_px[1])];
}
else
{
return pos_px;
}
}
/**
* Convert the position to norm units.
*
* @param {number[]} pos - the input position
* @param {string} posUnit - the position units
* @param {Window} win - the associated Window
* @returns {number[]} the position in norm units
*/
export function to_norm(pos, posUnit, win)
{
const response = { origin: "util.to_norm", context: "when converting a position to norm units" };
if (posUnit === "norm")
{
return pos;
}
if (posUnit === "pix")
{
return [pos[0] / (win.size[0] / 2.0), pos[1] / (win.size[1] / 2.0)];
}
if (posUnit === "height")
{
const minSize = Math.min(win.size[0], win.size[1]);
return [pos[0] * minSize / (win.size[0] / 2.0), pos[1] * minSize / (win.size[1] / 2.0)];
}
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
}
/**
* Convert the position to height units.
*
* @param {number[]} pos - the input position
* @param {string} posUnit - the position units
* @param {Window} win - the associated Window
* @returns {number[]} the position in height units
*/
export function to_height(pos, posUnit, win)
{
const response = {
origin: "util.to_height",
context: "when converting a position to height units",
};
if (posUnit === "height")
{
return pos;
}
if (posUnit === "pix")
{
const minSize = Math.min(win.size[0], win.size[1]);
return [pos[0] / minSize, pos[1] / minSize];
}
if (posUnit === "norm")
{
const minSize = Math.min(win.size[0], win.size[1]);
return [pos[0] * win.size[0] / 2.0 / minSize, pos[1] * win.size[1] / 2.0 / minSize];
}
throw Object.assign(response, { error: `unknown position units: ${posUnit}` });
}
/**
* Convert the position to window units.
*
* @param {number[]} pos - the input position
* @param {string} posUnit - the position units
* @param {Window} win - the associated Window
* @returns {number[]} the position in window units
*/
export function to_win(pos, posUnit, win)
{
const response = { origin: "util.to_win", context: "when converting a position to window units" };
try
{
if (win._units === "pix")
{
return to_px(pos, posUnit, win);
}
if (win._units === "norm")
{
return to_norm(pos, posUnit, win);
}
if (win._units === "height")
{
return to_height(pos, posUnit, win);
}
throw `unknown window units: ${win._units}`;
}
catch (error)
{
throw Object.assign(response, { response, error });
}
}
/**
* Convert the position to given units.
*
* @param {number[]} pos - the input position
* @param {string} posUnit - the position units
* @param {Window} win - the associated Window
* @param {string} targetUnit - the target units
* @returns {number[]} the position in target units
*/
export function to_unit(pos, posUnit, win, targetUnit)
{
const response = { origin: "util.to_unit", context: "when converting a position to different units" };
try
{
if (targetUnit === "pix")
{
return to_px(pos, posUnit, win);
}
if (targetUnit === "norm")
{
return to_norm(pos, posUnit, win);
}
if (targetUnit === "height")
{
return to_height(pos, posUnit, win);
}
throw `unknown target units: ${targetUnit}`;
}
catch (error)
{
throw Object.assign(response, { error });
}
}
/**
* Convert an object to its string representation, taking care of symbols.
*
*
Note: if the object is not already a string, we JSON stringify it and detect circularity.
*
* @param {Object} object - the input object
* @return {string} a string representation of the object or 'Object (circular)'
*/
export function toString(object)
{
if (typeof object === "undefined")
{
return "undefined";
}
if (!object)
{
return "null";
}
if (typeof object === "string")
{
return object;
}
// if the object is a class and has a toString method:
if (object.constructor.toString().substring(0, 5) === "class" && typeof object.toString === "function")
{
return object.toString();
}
if (typeof object === "function")
{
return ``;
}
try
{
const symbolReplacer = (key, value) =>
{
if (typeof value === "symbol")
{
value = Symbol.keyFor(value);
}
return value;
};
return JSON.stringify(object, symbolReplacer);
}
catch (e)
{
return "Object (circular)";
}
}
if (!String.prototype.format)
{
String.prototype.format = function()
{
var args = arguments;
return this
.replace(/{(\d+)}/g, function(match, number)
{
return typeof args[number] != "undefined" ? args[number] : match;
})
.replace(/{([$_a-zA-Z][$_a-zA-Z0-9]*)}/g, function(match, name)
{
// console.log("n=" + name + " args[0][name]=" + args[0][name]);
return args.length > 0 && args[0][name] !== undefined ? args[0][name] : match;
});
};
}
/**
* Get the most informative error from the server response from a jquery server request.
*
* @param jqXHR
* @param textStatus
* @param errorThrown
*/
export function getRequestError(jqXHR, textStatus, errorThrown)
{
let errorMsg = "unknown error";
if (typeof jqXHR.responseJSON !== "undefined")
{
errorMsg = jqXHR.responseJSON;
}
else if (typeof jqXHR.responseText !== "undefined")
{
errorMsg = jqXHR.responseText;
}
else if (typeof errorThrown !== "undefined")
{
errorMsg = errorThrown;
}
return errorMsg;
}
/**
* Test whether an object is either an integer or the string representation of an integer.
*
This is adapted from: https://stackoverflow.com/a/14794066
*
* @param {Object} obj - the input object
* @returns {boolean} whether or not the object is an integer or the string representation of an integer
*/
export function isInt(obj)
{
if (isNaN(obj))
{
return false;
}
const x = parseFloat(obj);
return (x | 0) === x;
}
/**
* Get the URL parameters.
*
* @returns {URLSearchParams} the iterable URLSearchParams
*
* @example
* const urlParameters = util.getUrlParameters();
* for (const [key, value] of urlParameters)
* console.log(key + ' = ' + value);
*
*/
export function getUrlParameters()
{
const urlQuery = window.location.search.slice(1);
return new URLSearchParams(urlQuery);
/*let urlMap = new Map();
for (const entry of urlParameters)
urlMap.set(entry[0], entry[1])
return urlMap;*/
}
/**
* Add info extracted from the URL to the given dictionary.
*
*
We exclude all URL parameters starting with a double underscore
* since those are reserved for client/server communication
*
* @param {Object} info - the dictionary
*/
export function addInfoFromUrl(info)
{
const infoFromUrl = getUrlParameters();
// note: parameters starting with a double underscore are reserved for client/server communication,
// we do not add them to info
// for (const [key, value] of infoFromUrl)
infoFromUrl.forEach((value, key) =>
{
if (key.indexOf("__") !== 0)
{
info[key] = value;
}
});
return info;
}
/**
* Select values from an array.
*
*
'selection' can be a single integer, an array of indices, or a string to be parsed, e.g.:
*