UNPKG

@mcmhomes/panorama-viewer

Version:
1,150 lines (1,063 loc) 27 kB
/** * Returns true if the value is set (not undefined and not null). * * @param {*} value * @returns {boolean} */ export const ISSET = (value) => (typeof value !== 'undefined') && (value !== null); /** * Returns true if the value is an array. * * @param {*} value * @returns {boolean} */ export const IS_ARRAY = (value) => Array.isArray(value); /** * Ensures the given value is an array (returns the value wrapped in an array if it's not). * * @param {*} value * @returns {*[]} */ export const ARRAY = (value) => IS_ARRAY(value) ? value : ((typeof value !== 'undefined') ? [value] : []); /** * Returns true if the value is an object. * * @param {*} value * @returns {boolean} */ export const IS_OBJECT = (value) => (typeof value === 'object') && (value !== null) && !Array.isArray(value); /** * Ensures the given value is an object (returns an empty object if it's not). * * @param value * @returns {object} */ export const OBJECT = (value) => IS_OBJECT(value) ? value : {}; /** * Ensures the given value is a string (casts it to a string if it's not, null and undefined will return an empty string). * * @param {*} value * @returns {string} */ export const STRING = (value) => ISSET(value) ? ('' + value) : ''; /** * Returns the first non-null non-undefined value as a string. * * @param {*} values * @returns {string} */ export const STRING_ANY = (...values) => { for(let value of values) { if(ISSET(value)) { return '' + value; } } return ''; }; /** * Ensures the given value is an integer (attempts to cast it to an integer if it's not, null and undefined will return 0). * * @param {*} value * @returns {number} */ export const INT = (value) => Math.round(FLOAT(value)); /** * Returns the first non-null non-undefined int-castable value as an integer. * * @param {*} values * @returns {number} */ export const INT_ANY = (...values) => Math.round(FLOAT_ANY(...values)); /** * Ensures the given value is a float (attempts to cast it to a float if it's not, null and undefined will return 0). * * @param {*} value * @returns {number} */ export const FLOAT = (value) => { const v = +value; if(!isNaN(v)) { return v; } return 0; }; /** * Returns the first non-null non-undefined float-castable value as a float. * * @param {*} values * @returns {number} */ export const FLOAT_ANY = (...values) => { for(let value of values) { if(value !== null) { const v = +value; if(!isNaN(v)) { return v; } } } return 0; }; /** * Ensures the given value is an integer (attempts to cast it to an integer if it's not, null and undefined will return 0). * This version is less strict than INT, as it relies on parseFloat instead of on +value, meaning that it will accept strings that contain a number followed by other characters, which +value doesn't. * * @param {*} value * @returns {number} */ export const INT_LAX = (value) => Math.round(FLOAT_LAX(value)); /** * Returns the first non-null non-undefined int-castable value as an integer. * This version is less strict than INT_ANY, as it relies on parseFloat instead of on +value, meaning that it will accept strings that contain a number followed by other characters, which +value doesn't. * * @param {*} values * @returns {number} */ export const INT_LAX_ANY = (...values) => Math.round(FLOAT_LAX_ANY(...values)); const REGEX_ALL_NON_FLOAT_CHARACTERS = /[^0-9.\-]/g; /** * Ensures the given value is a float (attempts to cast it to a float if it's not, null and undefined will return 0). * This version is less strict than FLOAT, as it relies on parseFloat instead of on +value, meaning that it will accept strings that contain a number followed by other characters, which +value doesn't. * * @param {*} value * @returns {number} */ export const FLOAT_LAX = (value) => { const v = (typeof value === 'number') ? value : parseFloat((value + '').replace(REGEX_ALL_NON_FLOAT_CHARACTERS, '')); if(!isNaN(v)) { return v; } return 0; }; /** * Returns the first non-null non-undefined float-castable value as a float. * This version is less strict than FLOAT_ANY, as it relies on parseFloat instead of on +value, meaning that it will accept strings that contain a number followed by other characters, which +value doesn't. * * @param {*} values * @returns {number} */ export const FLOAT_LAX_ANY = (...values) => { for(let value of values) { if(value !== null) { const v = (typeof value === 'number') ? value : parseFloat((value + '').replace(REGEX_ALL_NON_FLOAT_CHARACTERS, '')); if(!isNaN(v)) { return v; } } } return 0; }; /** * Loops through each element in the given array or object, and calls the callback for each element. * * @param {*[]|object|Function} elements * @param {Function} callback * @param {boolean} [optionalSkipHasOwnPropertyCheck] * @returns {*[]|object|Function} */ export const each = (elements, callback, optionalSkipHasOwnPropertyCheck = false) => { if((elements !== null) && (typeof elements !== 'undefined')) { if(Array.isArray(elements)) { for(let index = 0; index < elements.length; index++) { if(callback.call(elements[index], elements[index], index) === false) { break; } } } else if((typeof elements === 'object') || (typeof elements === 'function')) { for(let index in elements) { if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, index)) { if(callback.call(elements[index], elements[index], index) === false) { break; } } } } else { console.warn('Executed each() on an invalid type: [' + (typeof elements) + ']', elements); } } return elements; }; /** * Returns true if the array or object contains the given value. * * Values are compared by casting both of them to a string. * * @param {array|object|Function} array * @param {*} value * @returns {boolean} */ export const contains = (array, value) => { if(!array) { return false; } let result = false; value = STRING(value); each(array, (val) => { if(STRING(val) === value) { result = true; return false; } }); return result; }; /** * @callback __filterCallback * @param {*} value * @param {*} index * @returns {boolean|undefined} */ /** * Loops through the given elements, and returns a new array or object, with only the elements that returned true (or a value equals to true) from the callback. * If no callback is given, it will return all elements that are of a true value (for example, values that are: not null, not undefined, not false, not 0, not an empty string, not an empty array, not an empty object). * * @param {*[]|object|Function} elements * @param {__filterCallback} [callback] * @param {boolean} [optionalSkipHasOwnPropertyCheck] * @returns {*[]|object|Function} */ export const filter = (elements, callback, optionalSkipHasOwnPropertyCheck = false) => { if((elements !== null) && (typeof elements !== 'undefined')) { if(Array.isArray(elements)) { let result = []; for(let index = 0; index < elements.length; index++) { if((!callback && elements[index]) || (callback && callback.call(elements[index], elements[index], index))) { result.push(elements[index]); } } return result; } else if((typeof elements === 'object') || (typeof elements === 'function')) { let result = {}; for(let index in elements) { if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, index)) { if((!callback && elements[index]) || (callback && callback.call(elements[index], elements[index], index))) { result[index] = elements[index]; } } } return result; } else { console.warn('Executed filter() on an invalid type: [' + (typeof elements) + ']', elements); } } return elements; }; /** * Generates a base64 string (with +/ replaced by -_) that is guaranteed to be unique. * * @returns {string} */ export const uniqueId = (() => { let previousUniqueIdsTime = null; let previousUniqueIds = {}; const safeAtob = (base64string) => { if(typeof atob === 'function') { return atob(base64string); } return window.Buffer.from(base64string, 'base64').toString(); }; const safeBtoa = (string) => { if(typeof btoa === 'function') { return btoa(string); } return window.Buffer.from(string).toString('base64'); }; const numberToBytes = (number) => { const size = (number === 0) ? 0 : Math.ceil((Math.floor(Math.log2(number)) + 1) / 8); const bytes = new Uint8ClampedArray(size); let x = number; for(let i = (size - 1); i >= 0; i--) { const rightByte = x & 0xff; bytes[i] = rightByte; x = Math.floor(x / 0x100); } return bytes; }; const base64ToBytes = (base64string) => { const binary = safeAtob(base64string.trim()); const len = binary.length; let data = new Uint8Array(len); for(let i = 0; i < len; i++) { data[i] = binary.charCodeAt(i); } return data; }; const hexToBase64 = (hexstring) => { return safeBtoa(hexstring.replace(/[^0-9A-F]/gi, '').match(/\w{2}/g).map((a) => String.fromCharCode(parseInt(a, 16))).join('')); }; const bytesToBase64 = (arraybuffer) => { const bytes = new Uint8Array(arraybuffer); const len = bytes.byteLength; let binary = ''; for(let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return safeBtoa(binary); }; const generateUniqueId = () => { let now; try { if(typeof window === 'undefined') { throw new Error(); } // noinspection JSDeprecatedSymbols now = (performance.timeOrigin || performance.timing.navigationStart) + performance.now(); if(typeof now !== 'number') { throw new Error(); } } catch(e) { now = (Date.now ? Date.now() : (new Date()).getTime()); } now = Math.round(now); if(typeof window === 'undefined') { let uuid = null; try { uuid = crypto?.randomUUID(); } catch(e) { uuid = ''; uuid += (Math.random() + ' ').substring(2, 12).padEnd(10, '0'); uuid += (Math.random() + ' ').substring(2, 12).padEnd(10, '0'); uuid += (Math.random() + ' ').substring(2, 12).padEnd(10, '0'); uuid += (Math.random() + ' ').substring(2, 12).padEnd(10, '0'); } return { time:now, id: ('' + now + uuid).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_'), }; } const nowBytes = numberToBytes(now); let uuid = null; try { uuid = crypto?.randomUUID(); } catch(e) { } if(uuid) { uuid = base64ToBytes(hexToBase64(uuid)); } else { const bytesChunkA = numberToBytes((Math.random() + ' ').substring(2, 12).padEnd(10, '0')); const bytesChunkB = numberToBytes((Math.random() + ' ').substring(2, 12).padEnd(10, '0')); const bytesChunkC = numberToBytes((Math.random() + ' ').substring(2, 12).padEnd(10, '0')); const bytesChunkD = numberToBytes((Math.random() + ' ').substring(2, 12).padEnd(10, '0')); uuid = new Uint8Array(bytesChunkA.length + bytesChunkB.length + bytesChunkC.length + bytesChunkD.length); uuid.set(bytesChunkA, 0); uuid.set(bytesChunkB, bytesChunkA.length); uuid.set(bytesChunkC, bytesChunkA.length + bytesChunkB.length); uuid.set(bytesChunkD, bytesChunkA.length + bytesChunkB.length + bytesChunkC.length); } const bytes = new Uint8Array(nowBytes.length + uuid.length); bytes.set(nowBytes, 0); bytes.set(uuid, nowBytes.length); uuid = bytesToBase64(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_'); return { time:now, id: uuid, }; }; return () => { while(true) { const result = generateUniqueId(); if(previousUniqueIdsTime !== result.time) { previousUniqueIdsTime = result.time; previousUniqueIds = {[result.id]:true}; return result.id; } else if(previousUniqueIds[result.id] !== true) { previousUniqueIds[result.id] = true; return result.id; } } }; })(); /** * @callback __setTimeoutCallback * @param {number} deltaTime */ /** * Executes the callback after the given number of milliseconds. Passes the elapsed time in seconds to the callback. * * To cancel the timeout, call remove() on the result of this function (example: "const timeoutHandler = setTimeout((deltaTime)=>{}, 1000); timeoutHandler.remove();") * * @param {__setTimeoutCallback} callback ([number] deltaTime) * @param {number} ms * @returns {{remove:Function}} */ export const setTimeoutRemovable = (callback, ms) => { if(typeof window === 'undefined') { return { remove:() => { }, }; } ms = FLOAT_LAX(ms); let lastTime = performance.now(); /** @type {number|null} */ let handler = window.setTimeout(() => { const currentTime = performance.now(); try { callback((currentTime - lastTime) / 1000); } catch(e) { console.error(e); } lastTime = currentTime; }, ms); return { remove: () => { if(handler !== null) { window.clearTimeout(handler); handler = null; } }, }; }; /** * @callback __setIntervalCallback * @param {number} deltaTime */ /** * Executes the callback every given number of milliseconds. Passes the time difference in seconds between the last frame and now to it. * * To remove the interval, call remove() on the result of this function (example: "const intervalHandler = setInterval((deltaTime)=>{}, 1000); intervalHandler.remove();") * * @param {__setIntervalCallback} callback ([number] deltaTime) * @param {number} [intervalMs] * @param {boolean} [fireImmediately] * @returns {{remove:Function}} */ export const setIntervalRemovable = (callback, intervalMs = 1000, fireImmediately = false) => { intervalMs = FLOAT_LAX_ANY(intervalMs, 1000); if(fireImmediately) { try { callback(0); } catch(e) { console.error(e); } } if(typeof window === 'undefined') { return { remove:() => { }, }; } let lastTime = performance.now(); /** @type {number|null} */ let handler = window.setInterval(() => { const currentTime = performance.now(); try { callback((currentTime - lastTime) / 1000); } catch(e) { console.error(e); } lastTime = currentTime; }, intervalMs); return { remove: () => { if(handler !== null) { window.clearInterval(handler); handler = null; } }, }; }; /** * @callback __setAnimationFrameTimeoutCallback * @param {number} deltaTime */ /** * Executes the callback after the given number of frames. Passes the elapsed time in seconds to the callback. * * To cancel the timeout, call remove() on the result of this function (example: "const timeoutHandler = setAnimationFrameTimeout((deltaTime){}, 5); timeoutHandler.remove();") * * @param {__setAnimationFrameTimeoutCallback} callback ([number] deltaTime) * @param {number} [frames] * @returns {{remove:Function}} */ export const setAnimationFrameTimeoutRemovable = (callback, frames = 1) => { if(typeof window === 'undefined') { return { remove:() => { }, }; } frames = INT_LAX_ANY(frames, 1); let run = true; let requestAnimationFrameId = null; let lastTime = performance.now(); const tick = () => { if(run) { if(frames <= 0) { run = false; requestAnimationFrameId = null; const currentTime = performance.now(); try { callback((currentTime - lastTime) / 1000); } catch(e) { console.error(e); } lastTime = currentTime; return; } frames--; requestAnimationFrameId = window.requestAnimationFrame(tick); } }; tick(); return { remove: () => { run = false; if(requestAnimationFrameId !== null) { window.cancelAnimationFrame(requestAnimationFrameId); requestAnimationFrameId = null; } }, }; }; /** * @callback __setAnimationFrameIntervalCallback * @param {number} deltaTime */ /** * Executes the callback every given number of frames. Passes the time difference in seconds between the last frame and now to it. * * To remove the interval, call remove() on the result of this function (example: "const intervalHandler = setAnimationFrameInterval((deltaTime)=>{}, 5); intervalHandler.remove();") * * @param {__setAnimationFrameIntervalCallback} callback ([number] deltaTime) * @param {number} [intervalFrames] * @param {boolean} [fireImmediately] * @returns {{remove:Function}} */ export const setAnimationFrameIntervalRemovable = (callback, intervalFrames = 1, fireImmediately = false) => { intervalFrames = INT_LAX_ANY(intervalFrames, 1); if(fireImmediately) { try { callback(0); } catch(e) { console.error(e); } } if(typeof window === 'undefined') { return { remove:() => { }, }; } let run = true; let requestAnimationFrameId = null; let lastTime = performance.now(); let frames = intervalFrames; const tick = () => { if(run) { if(frames <= 0) { let currentTime = performance.now(); try { callback((currentTime - lastTime) / 1000); } catch(e) { console.error(e); } lastTime = currentTime; frames = intervalFrames; } frames--; if(run) { requestAnimationFrameId = window.requestAnimationFrame(tick); } } }; window.requestAnimationFrame(tick); return { remove: () => { run = false; if(requestAnimationFrameId !== null) { window.cancelAnimationFrame(requestAnimationFrameId); requestAnimationFrameId = null; } }, }; }; /** * Returns a promise, which will be resolved after the given number of milliseconds. * * @param {number} ms * @returns {Promise} */ export const promiseTimeout = (ms) => { ms = FLOAT_LAX(ms); if(ms <= 0) { return new Promise(resolve => resolve(undefined)); } return new Promise(resolve => setTimeoutRemovable(resolve, ms)); }; /** * Returns a promise, which will be resolved after the given number of frames. * * @param {number} frames * @returns {Promise} */ export const promiseAnimationFrameTimeout = (frames) => { frames = INT_LAX(frames); if(frames <= 0) { return new Promise(resolve => resolve(undefined)); } return new Promise(resolve => setAnimationFrameTimeoutRemovable(resolve, frames)); }; /** * Allows you to do a fetch, with built-in retry and abort functionality. * * @param {string} url * @param {Object} [options] * @returns {{then:Function, catch:Function, finally:Function, remove:Function, isRemoved:Function}|Promise<Response|any>} */ export const retryingFetch = (url, options) => { let currentRetries = 0; const retries = INT_LAX(options?.retries); let controllerAborted = false; let controller = null; if((typeof window !== 'undefined') && (typeof window.AbortController !== 'undefined')) { controller = new AbortController(); } let promise = (async () => { const attemptFetch = async () => { if(controllerAborted || controller?.signal?.aborted) { throw new Error('Aborted'); } try { const response = await fetch(url, { signal:controller?.signal, ...(options ?? {}), retries:undefined, delay: undefined, }); if(!response.ok) { throw new Error('Network request failed: ' + response.status + ' ' + response.statusText); } return response; } catch(error) { if(controllerAborted || controller?.signal?.aborted) { throw new Error('Aborted'); } if(currentRetries >= retries) { throw error; } currentRetries++; await promiseTimeout((typeof options?.delay === 'function') ? INT_LAX_ANY(options?.delay(currentRetries), 500) : (INT_LAX_ANY(options?.delay, 500))); return await attemptFetch(); } }; return await attemptFetch(); })(); let result = {}; result.then = (...args) => { promise = promise.then(...args); return result; }; result.catch = (...args) => { promise = promise.catch(...args); return result; }; result.finally = (...args) => { promise = promise.finally(...args); return result; }; result.remove = (...args) => { controllerAborted = true; if(controller) { controller.abort(...args); } return result; }; result.isRemoved = () => (controllerAborted || !!controller?.signal?.aborted); return result; }; /** * @callback __mapToArrayCallback * @param {*} value * @param {*} index * @returns {*} */ /** * Loops through the given elements, and returns a new array, with the elements that were returned from the callback. Always returns an array. * * @param {*[]|object|Function} elements * @param {__mapToArrayCallback} [callback] * @param {boolean} [optionalSkipHasOwnPropertyCheck] * @returns {*[]} */ export const mapToArray = (elements, callback, optionalSkipHasOwnPropertyCheck = false) => { let result = []; if((elements !== null) && (typeof elements !== 'undefined')) { if(Array.isArray(elements)) { for(let index = 0; index < elements.length; index++) { if(!callback) { result.push(elements[index]); } else { result.push(callback.call(elements[index], elements[index], index)); } } } else if((typeof elements === 'object') || (typeof elements === 'function')) { for(let index in elements) { if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, index)) { if(!callback) { result.push(elements[index]); } else { result.push(callback.call(elements[index], elements[index], index)); } } } } else { console.warn('Executed mapToArray() on an invalid type: [' + (typeof elements) + ']', elements); } } return result; }; /** * @callback __mapCallback * @param {*} value * @param {*} index * @returns {*} */ /** * Loops through the given elements, and returns a new array or object, with the elements that were returned from the callback. * * @param {*[]|object|Function} elements * @param {__mapCallback} [callback] * @param {boolean} [optionalSkipHasOwnPropertyCheck] * @returns {*[]|object|Function} */ export const map = (elements, callback, optionalSkipHasOwnPropertyCheck = false) => { if((elements !== null) && (typeof elements !== 'undefined')) { if(Array.isArray(elements)) { let result = []; for(let index = 0; index < elements.length; index++) { if(!callback) { result[index] = elements[index]; } else { result[index] = callback.call(elements[index], elements[index], index); } } return result; } else if((typeof elements === 'object') || (typeof elements === 'function')) { let result = {}; for(let index in elements) { if((optionalSkipHasOwnPropertyCheck === true) || Object.prototype.hasOwnProperty.call(elements, index)) { if(!callback) { result[index] = elements[index]; } else { result[index] = callback.call(elements[index], elements[index], index); } } } return result; } else { console.warn('Executed map() on an invalid type: [' + (typeof elements) + ']', elements); } } return elements; }; /** * @callback __findIndexValueCallback * @param {*} value * @param {*} index * @returns {boolean|undefined} */ /** * Finds the first element in the given array or object that returns true from the callback, and returns an object with the index and value. * * @param {*[]|object|Function} elements * @param {__findIndexValueCallback} callback * @param {boolean} [optionalSkipHasOwnPropertyCheck] * @returns {{index:*, value:*}|null} */ export const findIndexValue = (elements, callback, optionalSkipHasOwnPropertyCheck = false) => { let result = null; each(elements, (value, index) => { if(callback.call(elements[index], elements[index], index)) { result = {index, value}; return false; } }); return result; }; /** * Finds the first element in the given array or object that returns true from the callback, and returns the index. * * @param {*[]|object|Function} elements * @param {__findIndexValueCallback} callback * @param {boolean} [optionalSkipHasOwnPropertyCheck] * @returns {*|null} */ export const findIndex = (elements, callback, optionalSkipHasOwnPropertyCheck = false) => findIndexValue(elements, callback, optionalSkipHasOwnPropertyCheck)?.index ?? null; /** * Finds the first element in the given array or object that returns true from the callback, and returns the value. * * @param {*[]|object|Function} elements * @param {__findIndexValueCallback} callback * @param {boolean} [optionalSkipHasOwnPropertyCheck] * @returns {*|null} */ export const find = (elements, callback, optionalSkipHasOwnPropertyCheck = false) => findIndexValue(elements, callback, optionalSkipHasOwnPropertyCheck)?.value ?? null; /** * Attempts to obtain and return an error message from the given error, regardless of what is passed to this function. * * @param {*} error * @returns {string} */ export const purgeErrorMessage = (error) => { if(error?.message) { error = error.message; } if(typeof error === 'string') { const errorParts = error.split('threw an error:'); error = errorParts[errorParts.length - 1]; } else { try { error = JSON.stringify(error); } catch(e) { error = 'An unknown error occurred'; } } return error.trim(); }; /** * Returns a data URL of a 1x1 transparent pixel. * * @returns {string} */ export const getEmptyImageSrc = () => { // noinspection SpellCheckingInspection return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; }; /** * Returns true if the given hostname is private (such as localhost, 192.168.1.1, etc). * * Only "localhost" and IPv4 addresses are supported. IPv6 addresses will always return false. * * @param {string} host * @returns {boolean} */ const isGivenHostPrivate = (host) => { host = STRING(host).trim().toLowerCase(); if((host === 'localhost') || (host === '127.0.0.1')) { return true; } if(!/^(\d{1,3}\.){3}\d{1,3}$/.test(host)) { return false; } const parts = host.split('.'); return (parts[0] === '10') || // 10.0.0.0 - 10.255.255.255 ((parts[0] === '172') && ((parseInt(parts[1], 10) >= 16) && (parseInt(parts[1], 10) <= 31))) || // 172.16.0.0 - 172.31.255.255 ((parts[0] === '192') && (parts[1] === '168')); // 192.168.0.0 - 192.168.255.255 }; /** * Returns true if the given host is a private URL (like localhost, 127.0.0.1, etc). * * @param {string} host * @returns {boolean} */ export const isHostPrivate = (() => { const cache = {}; return (host) => { if(!(host in cache)) { let hostLower = host.toLowerCase().trim(); hostLower = hostLower.replace(/^[a-z]+:\/\//, ''); hostLower = hostLower.replace(/[:/].*$/, ''); cache[host] = isGivenHostPrivate(hostLower); } return cache[host]; }; })();