const EPSILON = 0.0001; /** * * @param {number} x - x coor * @param {number} y - y coord * @param {number} angle - rotation angle in radian * @returns {{x:number, y:number}} - x y rotated by angle */ const rotate2DCoord = (x, y, angle) => { const cos = Math.cos(angle); const sin = Math.sin(angle); return { x: x * cos - y * sin, y: y * cos + x * sin }; }; /** * * @param {Array<{x:number, y:number}>} points - points of your polygon not closed * @returns {number} the area of your polygon */ const polygon2DArea = (points) => { let area = 0; for (let i = 0; i < points.length; i += 2) { area += points[(i + 1) % points.length].x * (points[(i + 2) % points.length].y - points[i % points.length].y) + points[(i + 1) % points.length].y * (points[i % points.length].x - points[(i + 2) % points.length].x); } area /= 2; return area; }; /** * Limit the execution of a function every delay ms * * @param {Function} fn - function to be throttled * @param {number} delay - delay in ms * @returns {*} - return what the function should return every delay ms */ const throttle = (fn, delay) => { let lastCalled = 0; return (...args) => { const now = new Date().getTime(); if (now - lastCalled < delay) { return; } lastCalled = now; return fn(...args); }; }; /** * Check if a string is a valid number * inspired of https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number * * @param {string} str - string to check * @returns {boolean} true if it's a valid number */ function isNumeric(str) { if (str === 0) return true; if (str instanceof Object) return false; if (typeof str == 'boolean') return false; return ( !isNaN(str) && // Use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... !isNaN(parseFloat(str)) ); // ...and ensure strings of whitespace fail } /** * Take an array of string and check if it is in vector3 format * * @param {Array<string>} subString - array of string * @returns {boolean} - true if it is vector3 format */ function checkIfSubStringIsVector3(subString) { if (subString.length != 3) { // Need three string return false; } for (let index = 0; index < subString.length; index++) { const element = subString[index]; if (!isNumeric(element)) { // All string should be numerics return false; } } return true; } /** * Take an array of string and check if it is in euler format * * @param {Array<string>} subString - array of string * @returns {boolean} - true if it is euler format */ function checkIfSubStringIsEuler(subString) { if (subString.length != 4) { // Need four string return false; } // Three first string have to be numerics if (!isNumeric(subString[0])) return false; if (!isNumeric(subString[1])) return false; if (!isNumeric(subString[2])) return false; // The last one has to be an euler order if ( subString[3] == 'XYZ' || subString[3] == 'XZY' || subString[3] == 'ZYX' || subString[3] == 'ZXY' || subString[3] == 'YZX' || subString[3] == 'YXZ' ) { return true; } return false; } /** * Taking a string from the unpacking URI and splitting it into an array of strings. * * @param {string} uriComp - The string from the unpacking URI * @returns {Array<string>} - returns the array of strings if it is in vector3 format, otherwise returns null */ function vector3ArrayFromURIComponent(uriComp) { const subString = uriComp.split(','); if (checkIfSubStringIsVector3(subString)) { return subString; } return null; } /** * Taking a string from the unpacking URI and splitting it into an array of strings. * * @param {string} uriComp - The string from the unpacking URI * @returns {Array<string>} - returns the array of strings if it is in euler format, otherwise returns null */ function eulerArrayFromURIComponent(uriComp) { const subString = uriComp.split(','); if (checkIfSubStringIsEuler(subString)) { return subString; } return null; } /** * Take an array of string and check if it is in matrix4 format * * @param {Array<string>} subString - array of string * @returns {boolean} - true if it is matrix4 format */ function checkIfSubStringIsMatrix4(subString) { if (subString.length != 16) { // Need 16 string return false; } for (let index = 0; index < subString.length; index++) { const element = subString[index]; if (!isNumeric(element)) { // All string should be numerics return false; } } return true; } /** * Taking a string from the unpacking URI and splitting it into an array of strings. * * @param {string} uriComp - The string from the unpacking URI * @returns {Array<string>} - returns the array of strings if it is in matrix4 format, otherwise returns null */ function matrix4ArrayFromURIComponent(uriComp) { const subString = uriComp.split(','); if (checkIfSubStringIsMatrix4(subString)) { return subString; } return null; } /** * Convert an Object into a Int32Array * * @param {object} obj - object to convert * @returns {Int32Array} - array converted */ function objectToInt32Array(obj) { const OString = JSON.stringify(obj); const SABuffer = new SharedArrayBuffer( Int32Array.BYTES_PER_ELEMENT * OString.length ); const sArray = new Int32Array(SABuffer); for (let i = 0; i < OString.length; i++) { sArray[i] = OString.charCodeAt(i); } return sArray; } /** * Convert a Int32Array into an Object * * @param {Int32Array} array - array to convert * @returns {object} - object converted */ function int32ArrayToObject(array) { const str = String.fromCharCode.apply(this, array); return JSON.parse(str); } /** * Convert a data URI into a Buffer * * @param {string} uri - data uri to convert * @returns {Buffer|null} - the buffer of the data uri or null if uri is not a data uri */ function dataUriToBuffer(uri) { if (!/^data:/i.test(uri)) { return null; // Its not a Data URI } // Strip newlines uri = uri.replace(/\r?\n/g, ''); // Split the URI up into the "metadata" and the "data" portions const firstComma = uri.indexOf(','); if (firstComma === -1 || firstComma <= 4) { throw new TypeError('malformed data: URI'); } // Remove the "data:" scheme and parse the metadata const meta = uri.substring(5, firstComma).split(';'); let charset = ''; let base64 = false; const type = meta[0] || 'text/plain'; let typeFull = type; for (let i = 1; i < meta.length; i++) { if (meta[i] === 'base64') { base64 = true; } else { typeFull += `;${meta[i]}`; if (meta[i].indexOf('charset=') === 0) { charset = meta[i].substring(8); } } } // Defaults to US-ASCII only if type is not provided if (!meta[0] && !charset.length) { typeFull += ';charset=US-ASCII'; charset = 'US-ASCII'; } // Get the encoded data portion and decode URI-encoded chars const encoding = base64 ? 'base64' : 'ascii'; const data = unescape(uri.substring(firstComma + 1)); const buffer = Buffer.from(data, encoding); // Set `.type` and `.typeFull` properties to MIME type buffer.type = type; buffer.typeFull = typeFull; // Set the `.charset` property buffer.charset = charset; return buffer; } /** * Removes empty fields from a FormData. Useful for update requests that * would update those fields to an empty string if they were sent in the * body. To check if a value is empty, this function just convert it into * a boolean. * * @param {FormData} formData The form data. * @returns {FormData} The same data, without the fields containing an empty value. */ function removeEmptyValues(formData) { const emptyKeys = []; formData.forEach((value, key) => { if (!value) { emptyKeys.push(key); } }); emptyKeys.forEach((key) => { formData.delete(key); }); return formData; } /** * Converts the raw content of an array buffer (as retrieved by a GET request * on a file) to a data URI. This is required, for example, to display images * fetched from the server. As we need authentication headers to retrieve some * protected files, we get the raw data dynamically and need to convert it to * a data URI do display it. * The basic scheme of the URI is defined in the * [RFC 2397](https://tools.ietf.org/html/rfc2397), with the mediaType set to * `mimeType` and the raw data converted to base64. * * @param {ArrayBuffer} arrayBuffer The binary data of the file. * @param {string} mimeType The media type. Any type supported by a data URI * should work. For images, use `image/png` or `image/jpeg` for instance. * @param {number} chunkSize The size of the chunks used to process the raw * data. If you get an exception saying that too many arguments were passed as * parameters, try reducing this value. * @returns {string} - data uri */ function imageToDataURI(arrayBuffer, mimeType, chunkSize = 8 * 1024) { // The response is a raw file, we need to convert it to base64 // File (ArrayBuffer) -> Byte array -> String -> Base64 string const responseArray = new Uint8Array(arrayBuffer); // Make a string from the response array. As the array can be // too long (each value will be passed as an argument to // String.fromCharCode), we need to split it into chunks let responseAsString = ''; for (let i = 0; i < responseArray.length / chunkSize; i++) { responseAsString += String.fromCharCode.apply( null, responseArray.slice(i * chunkSize, (i + 1) * chunkSize) ); } const b64data = 'data:' + mimeType + ';base64,' + btoa(responseAsString); return b64data; } /** * Gets an attribute of an object from the given path. To get nested attributes, * the path qualifiers must be separated by dots ('.'). If the path is not * nested (does not contain any dot), the function is equivalent to `obj[path]`. * * @param {object} obj - object to get attribute * @param {string} path - path to get the attribute * @returns {*} - attribute vaue * @example * const obj = {test: {msg: "Hello world !"}}; * console.log(getAttributeByPath(obj, "test.msg")); // prints "Hello world !"; * console.log(getAttributeByPath(obj, "other")); // undefined */ function getAttributeByPath(obj, path) { const segs = path.split('.'); let val = obj; for (const seg of segs) { val = val[seg]; if (val === undefined) { break; } } return val; } /** * Check if two json object are equals * * @param {object} json1 - first json object * @param {object} json2 - second json object * @returns {boolean} - true if both json are equals, false otherwise */ function objectEquals(json1, json2) { for (const key in json1) { if (json1[key] instanceof Object) { if (json2[key] instanceof Object) { if (objectEquals(json1[key], json2[key])) { continue; } else { return false; } } else { return false; } } else { const areEquals = isNumeric(json1[key]) ? Math.abs(json1[key] - json2[key]) < EPSILON : json2[key] == json1[key]; if (areEquals) { continue; } else { return false; } } } return true; // All check have passed meaning is equals } /** * Overwrite identical key of jsonOverWrited with the one matching in jsonModel * Create key of jsonModel which are not in jsonOverWrited * * @param {object} jsonOverWrited - json object overwrited * @param {object} jsonModel - json object used as model to overwrite * @returns {object} - json object overwrited */ function objectOverWrite(jsonOverWrited, jsonModel) { if (!jsonModel) return jsonOverWrited; // write the ones not in jsonOverWrited for (const key in jsonModel) { if (jsonOverWrited[key] == undefined) { jsonOverWrited[key] = jsonModel[key]; } } // check in jsonOverWrited the ones existing in jsonModel for (const key in jsonOverWrited) { if (jsonOverWrited[key] instanceof Array) { if (jsonModel[key] instanceof Array) { jsonOverWrited[key] = jsonModel[key]; // array are replaced } } else if (jsonOverWrited[key] instanceof Object) { if (jsonModel[key] instanceof Object) objectOverWrite(jsonOverWrited[key], jsonModel[key]); } else { if (jsonModel[key] != undefined) { jsonOverWrited[key] = jsonModel[key]; } } } return jsonOverWrited; } /** * Apply a callback to each key value couple of an object * * @param {object} object - object to parse * @param {Function} cb - callback to apply (first argument is the object containing the key and second is the key) * @returns {object} - object parsed */ function objectParse(object, cb) { for (const key in object) { if (object[key] instanceof Object) { objectParse(object[key], cb); } else { cb(object, key); } } return object; } /** * Replace all valid number string in a json object by a float * * @param {object} json - json object to parse * @returns {object} - json object parsed */ function objectParseNumeric(json) { return objectParse(json, function (j, key) { if (isNumeric(j[key])) { j[key] = parseFloat(j[key]); } }); } /** * Check if both array are equals * * @param {Array} a1 - array 1 * @param {Array} a2 - array 2 * @returns {boolean} - true if equals */ function arrayEquals(a1, a2) { if (a1.length !== a2.length) return false; for (let i = 0; i < a1.length; i++) { if (a1[i] !== a2[i]) return false; } return true; } /** * Compute the last string after the . in the filename * * @param {string} filename - file name * @returns {string} - file format */ function computeFileFormat(filename) { const indexLastPoint = filename.lastIndexOf('.'); return filename.slice(indexLastPoint + 1); } /** * Compute filename from path * * @param {string} path - path * @returns {string} filename */ function computeFilenameFromPath(path) { const indexLastSlash = path.lastIndexOf('/'); return path.slice(indexLastSlash + 1); } /** * Check if the element is alreeady included in the array if not push it * * @param {Array} array - array where to push the element * @param {*} element - element to push * @returns {boolean} true if pushed false otherwise */ function arrayPushOnce(array, element) { if (!array.includes(element)) { array.push(element); return true; } return false; } /** * Remove an element if it's present in an array * * @param {Array} array - array to remove element from * @param {*} element - element to remove * @returns {boolean} true if removed false otherwise */ function removeFromArray(array, element) { const index = array.indexOf(element); if (index >= 0) { array.splice(index, 1); return true; } return false; } /** * * @param {string} originalString - string to modify * @param {number} index - where to insert * @param {string} string to insert * @returns {string} - string injected */ const insert = (originalString, index, string) => { if (index > 0) { return ( originalString.substring(0, index) + string + originalString.substring(index, originalString.length) ); } return string + originalString; }; /** * * @param {number} number - number to round * @returns {string} rounded number */ const round = (number) => { const x = Math.round(number * 10) + ''; return insert(x, x.length - 1, ','); }; /** * * @param {{x:number,y:number,z:number}} v - vector 3 to labelize * @returns {string} vector labelified */ const vector3ToLabel = (v) => { return round(v.x) + ' m; ' + round(v.y) + ' m; ' + round(v.z) + ' m;'; }; module.exports = { EPSILON: EPSILON, throttle: throttle, vector3ToLabel: vector3ToLabel, round: round, insert: insert, isNumeric: isNumeric, arrayEquals: arrayEquals, checkIfSubStringIsEuler: checkIfSubStringIsEuler, checkIfSubStringIsVector3: checkIfSubStringIsVector3, checkIfSubStringIsMatrix4: checkIfSubStringIsMatrix4, vector3ArrayFromURIComponent: vector3ArrayFromURIComponent, eulerArrayFromURIComponent: eulerArrayFromURIComponent, matrix4ArrayFromURIComponent: matrix4ArrayFromURIComponent, objectToInt32Array: objectToInt32Array, int32ArrayToObject: int32ArrayToObject, dataUriToBuffer: dataUriToBuffer, removeEmptyValues: removeEmptyValues, imageToDataURI: imageToDataURI, getAttributeByPath: getAttributeByPath, objectEquals: objectEquals, objectOverWrite: objectOverWrite, objectParse: objectParse, objectParseNumeric: objectParseNumeric, computeFileFormat: computeFileFormat, computeFilenameFromPath: computeFilenameFromPath, arrayPushOnce: arrayPushOnce, removeFromArray: removeFromArray, rotate2DCoord: rotate2DCoord, polygon2DArea: polygon2DArea, ProcessInterval: require('./ProcessInterval'), };