/** * Various utilities. * * @author Alain Pitiot * @version 3.1.4 * @copyright (c) 2019 Ilixa Ltd. ({@link http://ilixa.com}) * @license Distributed under the terms of the MIT License */ /** * Syntactic sugar for Mixins * *
This is heavily adapted from: http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
* * @name module:util.MixinBuilder * @class * @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. * * @name module:util.promiseToTupple * @function * @public * @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) *See details here: https://www.ietf.org/rfc/rfc4122.txt
* * @name module:util.makeUuid * @function * @public * @return {string} the uuid */ export function makeUuid() { 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); }); } /** * Get the error stack of the calling, exception-throwing function. * * @name module:util.getErrorStack * @function * @public * @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. * * @name module:util.isEmpty * @function * @public * @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/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
* * @name module:util.detectBrowser * @function * @public * @return {string} the detected browser, one of 'Opera', 'Firefox', 'Safari', * 'IE', 'Edge', 'Chrome', 'Blink", 'unknown' */ export function detectBrowser() { // Opera 8.0+ const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; if (isOpera) return 'Opera'; // Firefox 1.0+ const isFirefox = typeof InstallTrigger !== 'undefined'; if (isFirefox) return 'Firefox'; // Safari 3.0+ "[object HTMLElementConstructor]" const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification)); if (isSafari) return 'Safari'; // Internet Explorer 6-11 const isIE = /*@cc_on!@*/false || !!document.documentMode; if (isIE) return 'IE'; // Edge 20+ const isEdge = !isIE && !!window.StyleMedia; if (isEdge) return 'Edge'; // Chrome 1+ const isChrome = !!window.chrome && !!window.chrome.webstore; if (isChrome) return 'Chrome'; // Blink engine detection const isBlink = (isChrome || isOpera) && !!window.CSS; if (isBlink) return 'Blink'; return 'unknown'; } /** * Convert obj to its numerical form. * *We are using the algorithm described here: https://wrf.ecse.rpi.edu//Research/Short_Notes/pnpoly.html
* * @name module:util.IsPointInsidePolygon * @function * @public * @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 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
* * @name module:util.shuffle * @function * @public * @param {Object[]} array - the input 1-D array * @return {Object[]} the shuffled array */ export function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } /** * Get the position of the object in pixel units * * @name module:util.getPositionFromObject * @function * @public * @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; // 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. * * @name module:util.to_px * @function * @public * @param {number[]} pos - the input position * @param {string} posUnit - the position units * @param {Window} win - the associated Window * @returns {number[]} the position in pixel units */ export function to_px(pos, posUnit, win) { const response = { origin: 'util.to_px', context: 'when converting a position to pixel units' }; if (posUnit === 'pix') return pos; else if (posUnit === 'norm') return [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]); return [pos[0] * minSize, pos[1] * minSize]; } else throw Object.assign(response, { error: `unknown position units: ${posUnit}` }); } /** * Convert the position to norm units. * * @name module:util.to_norm * @function * @public * @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. * * @name module:util.to_height * @function * @public * @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. * * @name module:util.to_win * @function * @public * @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. * * @name module:util.to_unit * @function * @public * @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 a position to a PIXI Point. * * @name module:util.to_pixiPoint * @function * @public * @param {number[]} pos - the input position * @param {string} posUnit - the position units * @param {Window} win - the associated Window * @returns {number[]} the position as a PIXI Point */ export function to_pixiPoint(pos, posUnit, win) { const pos_px = to_px(pos, posUnit, win); return new PIXI.Point(pos_px[0], pos_px[1]); } /** * 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.
* * @name module:util.toString * @function * @public * @param {Object} object - the input object * @return {string} a string representation of the object or 'Object (circular)' */ export function toString(object) { if (typeof object === 'string') return object; 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 ; }); }; } /** * Test whether an object is either an integer or the string representation of an integer. *This is adapted from: https://stackoverflow.com/a/14794066
* * @name module:util.isInt * @function * @public * @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. * * @name module:util.getUrlParameters * @function * @public * @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
* * @name module:util.addInfoFromUrl * @function * @public * @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) 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.: *