mirror of
https://github.com/cds-astro/aladin-lite.git
synced 2026-06-12 19:11:42 -07:00
659 lines
20 KiB
TypeScript
659 lines
20 KiB
TypeScript
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
// Copyright 2013 - UDS/CNRS
|
|
// The Aladin Lite program is distributed under the terms
|
|
// of the GNU Lesser General Public License version 3
|
|
// or (at your option) any later version.
|
|
//
|
|
// This file is part of Aladin Lite.
|
|
//
|
|
// Aladin Lite is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Aladin Lite 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 Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with Aladin Lite. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
|
|
/******************************************************************************
|
|
* Aladin Lite project
|
|
*
|
|
* File Utils
|
|
*
|
|
* Author: Thomas Boch[CDS]
|
|
*
|
|
*****************************************************************************/
|
|
|
|
import {JSONP_PROXY} from "./Constants";
|
|
import { ALEvent } from './events/ALEvent';
|
|
|
|
|
|
export let Utils: any = {}
|
|
|
|
// list of URL domains that can be safely switched from HTTP to HTTPS
|
|
Utils.HTTPS_WHITELIST = ['alasky.u-strasbg.fr', 'alaskybis.u-strasbg.fr', 'alasky.unistra.fr', 'alaskybis.unistra.fr',
|
|
'alasky.cds.unistra.fr', 'alaskybis.cds.unistra.fr', 'hips.astron.nl', 'jvo.nao.ac.jp',
|
|
'archive.cefca.es', 'cade.irap.omp.eu', 'skies.esac.esa.int']
|
|
|
|
Utils.cssScale = undefined
|
|
|
|
Utils.relMouseCoords = function (event) {
|
|
if (event.offsetX) {
|
|
return {x: event.offsetX, y: event.offsetY}
|
|
} else {
|
|
if (!Utils.cssScale) {
|
|
var st = window.getComputedStyle(document.body, null)
|
|
var tr = st.getPropertyValue('-webkit-transform') ||
|
|
st.getPropertyValue('-moz-transform') ||
|
|
st.getPropertyValue('-ms-transform') ||
|
|
st.getPropertyValue('-o-transform') ||
|
|
st.getPropertyValue('transform')
|
|
var matrixRegex = /matrix\((-?\d*\.?\d+),\s*0,\s*0,\s*(-?\d*\.?\d+),\s*0,\s*0\)/
|
|
var matches = tr.match(matrixRegex)
|
|
if (matches) {
|
|
Utils.cssScale = parseFloat(matches[1])
|
|
} else {
|
|
Utils.cssScale = 1
|
|
}
|
|
}
|
|
var e = event
|
|
// http://www.jacklmoore.com/notes/mouse-position/
|
|
var target = e.target || e.srcElement
|
|
var style = target.currentStyle || window.getComputedStyle(target, null)
|
|
var borderLeftWidth = parseInt(style['borderLeftWidth'], 10)
|
|
var borderTopWidth = parseInt(style['borderTopWidth'], 10)
|
|
var rect = target.getBoundingClientRect()
|
|
|
|
var clientX = e.clientX
|
|
var clientY = e.clientY
|
|
|
|
if (e && e.changedTouches) {
|
|
clientX = e.changedTouches[0].clientX
|
|
clientY = e.changedTouches[0].clientY
|
|
}
|
|
|
|
var offsetX = clientX - borderLeftWidth - rect.left
|
|
var offsetY = clientY - borderTopWidth - rect.top
|
|
|
|
return {x: Math.round(offsetX / Utils.cssScale), y: Math.round(offsetY / Utils.cssScale)}
|
|
}
|
|
}
|
|
|
|
//Function.prototype.bind polyfill from
|
|
//https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
|
|
if (!Function.prototype.bind) {
|
|
Function.prototype.bind = function (obj) {
|
|
// closest thing possible to the ECMAScript 5 internal IsCallable function
|
|
if (typeof this !== 'function') {
|
|
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable')
|
|
}
|
|
|
|
var slice = [].slice,
|
|
args = slice.call(arguments, 1),
|
|
self = this,
|
|
nop = function () {
|
|
},
|
|
bound = function () {
|
|
return self.apply(this instanceof nop ? this : (obj || {}),
|
|
args.concat(slice.call(arguments)))
|
|
}
|
|
|
|
bound.prototype = this.prototype
|
|
|
|
return bound
|
|
}
|
|
}
|
|
|
|
|
|
/* source : http://stackoverflow.com/a/8764051 */
|
|
Utils.urlParam = function (name: string, queryString: string | undefined) {
|
|
if (queryString === undefined) {
|
|
queryString = location.search
|
|
}
|
|
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(queryString) || [, ''])[1].replace(/\+/g, '%20')) || null
|
|
}
|
|
|
|
/* source: http://stackoverflow.com/a/1830844 */
|
|
Utils.isNumber = function (n: string | number) {
|
|
return !isNaN(parseFloat(n as string)) && isFinite(n as number)
|
|
}
|
|
|
|
Utils.isInt = function (n: string | number) {
|
|
return Utils.isNumber(n) && Math.floor(n as number) === n
|
|
}
|
|
|
|
// Newton-Raphson method to find the approximate inverse of f(x)
|
|
Utils.inverseNewtonRaphson = function(y: number, f: Function, fPrime: Function, tolerance=1e-6, maxIterations=100) {
|
|
let x_guess = 0.5; // Initial guess
|
|
let iteration = 0;
|
|
|
|
while (iteration < maxIterations) {
|
|
let f_x = f(x_guess);
|
|
let error = Math.abs(f_x - y);
|
|
|
|
if (error < tolerance) {
|
|
return x_guess; // Found approximate inverse
|
|
}
|
|
|
|
let derivative = fPrime(x_guess);
|
|
x_guess = x_guess - (f_x - y) / derivative; // Newton-Raphson update
|
|
iteration++;
|
|
}
|
|
|
|
return null; // No convergence within maxIterations
|
|
}
|
|
|
|
Utils.binarySearch = function(array, value) {
|
|
var low = 0,
|
|
high = array.length;
|
|
var mid;
|
|
while (low < high) {
|
|
mid = Math.floor((low + high) / 2);
|
|
if (array[mid] === value) {
|
|
return mid;
|
|
} else if (array[mid] < value) {
|
|
low = mid + 1;
|
|
} else {
|
|
high = mid;
|
|
}
|
|
}
|
|
|
|
return low;
|
|
}
|
|
|
|
/* a debounce function, used to prevent multiple calls to the same function if less than delay milliseconds have passed */
|
|
Utils.debounce = function (fn, delay) {
|
|
var timer = null
|
|
return function () {
|
|
var context = this, args = arguments
|
|
clearTimeout(timer)
|
|
timer = setTimeout(function () {
|
|
fn.apply(context, args)
|
|
}, delay)
|
|
}
|
|
}
|
|
|
|
/* return a throttled function, to rate limit the number of calls (by default, one call every 250 milliseconds) */
|
|
Utils.throttle = function (fn, threshhold, scope) {
|
|
threshhold || (threshhold = 250)
|
|
var last,
|
|
deferTimer
|
|
return function () {
|
|
var context = scope || this
|
|
|
|
var now = +new Date,
|
|
args = arguments
|
|
if (last && now < last + threshhold) {
|
|
// hold on to it
|
|
clearTimeout(deferTimer)
|
|
deferTimer = setTimeout(function () {
|
|
last = now
|
|
fn.apply(context, args)
|
|
}, threshhold)
|
|
} else {
|
|
last = now
|
|
fn.apply(context, args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Way of detecting if the computer has trackpad or a regular mouse wheel thanks to that post:
|
|
// https://stackoverflow.com/questions/10744645/detect-touchpad-vs-mouse-in-javascript
|
|
Utils.detectTrackPad = function (e) {
|
|
var isTrackpad = false;
|
|
if (e.wheelDeltaY) {
|
|
if (e.wheelDeltaY === (e.deltaY * -3)) {
|
|
isTrackpad = true;
|
|
}
|
|
}
|
|
else if (e.deltaMode === 0) {
|
|
isTrackpad = true;
|
|
}
|
|
|
|
return isTrackpad
|
|
}
|
|
|
|
|
|
/* A LRU cache, inspired by https://gist.github.com/devinus/409353#file-gistfile1-js */
|
|
// TODO : utiliser le LRU cache pour les tuiles images
|
|
Utils.LRUCache = function (maxsize) {
|
|
this._keys = []
|
|
this._items = {}
|
|
this._expires = {}
|
|
this._size = 0
|
|
this._maxsize = maxsize || 1024
|
|
}
|
|
|
|
Utils.LRUCache.prototype = {
|
|
set: function (key, value) {
|
|
var keys = this._keys,
|
|
items = this._items,
|
|
expires = this._expires,
|
|
size = this._size,
|
|
maxsize = this._maxsize
|
|
|
|
if (size >= maxsize) { // remove oldest element when no more room
|
|
keys.sort(function (a, b) {
|
|
if (expires[a] > expires[b]) return -1
|
|
if (expires[a] < expires[b]) return 1
|
|
return 0
|
|
})
|
|
|
|
size--
|
|
}
|
|
|
|
keys[size] = key
|
|
items[key] = value
|
|
expires[key] = Date.now()
|
|
size++
|
|
|
|
this._keys = keys
|
|
this._items = items
|
|
this._expires = expires
|
|
this._size = size
|
|
},
|
|
|
|
get: function (key) {
|
|
var item = this._items[key]
|
|
if (item) {
|
|
this._expires[key] = Date.now()
|
|
}
|
|
return item
|
|
},
|
|
|
|
keys: function () {
|
|
return this._keys
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////:
|
|
|
|
/**
|
|
Fetch an url with the method GET, given a list of potential mirrors
|
|
An optional object can be given with the following keywords accepted:
|
|
* data: an object storing the params associated to the URL
|
|
* contentType: specify the content type returned from the url (no verification is done, it is not mandatory to put it)
|
|
* timeout: A maximum request time. If exceeded, the request is aborted and the next url will be fetched
|
|
This method assumes the URL are CORS-compatible, no proxy will be used
|
|
|
|
A promise is returned. When all the urls fail, a rejected Promise is returned so that it can be catched afterwards
|
|
*/
|
|
Utils.loadFromUrls = function (urls, options) {
|
|
const data = options && options.data || undefined
|
|
const timeout = options && options.timeout || 5000
|
|
const mode = options && options.mode || 'cors';
|
|
const contentType = options && options.contentType || undefined;
|
|
const dataType = (options && options.dataType) || 'text';
|
|
const desc = options && options.desc;
|
|
// A controller that can abort the query when a timeout is reached
|
|
const controller = new AbortController()
|
|
|
|
// Launch a timemout that will interrupt the fetch if it has not yet succeded:
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
let url = urls[0];
|
|
if (data) {
|
|
url += '?' + new URLSearchParams(data)
|
|
}
|
|
|
|
let params = {
|
|
url,
|
|
// *GET, POST, PUT, DELETE, etc.
|
|
method: 'GET',
|
|
/*headers: {
|
|
'Content-Type': contentType
|
|
},*/
|
|
// no-cors, *cors, same-origin
|
|
mode,
|
|
// *default, no-cache, reload, force-cache, only-if-cached
|
|
cache: 'default',
|
|
// Abort the request when a timeout exceeded
|
|
signal: controller.signal,
|
|
dataType,
|
|
// message description
|
|
desc
|
|
}
|
|
|
|
if (contentType) {
|
|
params.headers = {
|
|
'Content-Type': contentType
|
|
};
|
|
}
|
|
|
|
return Utils.fetch({
|
|
...params,
|
|
success: (resp) => {
|
|
// completed request before timeout fired
|
|
clearTimeout(timeoutId)
|
|
return Promise.resolve(resp)
|
|
},
|
|
error: (e) => {
|
|
// The request aborted because it was to slow, fetch the next url given recursively
|
|
// Base case, when all urls have been fetched and failed
|
|
if (urls && urls.length === 1) {
|
|
let errorMsg = "Status: "+ e.status +"\nMessage: " + e.statusText;
|
|
return Promise.reject(errorMsg);
|
|
} else {
|
|
return Utils.loadFromUrls(urls.slice(1), options);
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/*
|
|
// return the jquery ajax object configured with the requested parameters
|
|
// by default, we use the proxy (safer, as we don't know if the remote server supports CORS)
|
|
Utils.getAjaxObject = function (url, method, dataType, useProxy) {
|
|
if (useProxy !== false) {
|
|
useProxy = true
|
|
}
|
|
|
|
if (useProxy === true) {
|
|
var urlToRequest = JSONP_PROXY + '?url=' + encodeURIComponent(url)
|
|
} else {
|
|
urlToRequest = url
|
|
}
|
|
method = method || 'GET'
|
|
dataType = dataType || null
|
|
|
|
return $.ajax({
|
|
url: urlToRequest,
|
|
method: method,
|
|
dataType: dataType
|
|
})
|
|
}
|
|
*/
|
|
|
|
Utils.fetch = function(params) {
|
|
let url;
|
|
try {
|
|
url = new URL(params.url);
|
|
if (params.useProxy === true) {
|
|
url = Utils.handleCORSNotSameOrigin(url)
|
|
}
|
|
|
|
if (params.data) {
|
|
// add the search params to the url object
|
|
for (const key in params.data) {
|
|
url.searchParams.append(key, params.data[key]);
|
|
}
|
|
}
|
|
} catch(e) {
|
|
// localhost url
|
|
url = params.url;
|
|
}
|
|
|
|
let request = new Request(url, {
|
|
method: params.method || 'GET',
|
|
...params
|
|
})
|
|
|
|
let task = {
|
|
message: params.desc || 'fetching: ' + url,
|
|
id: Utils.uuidv4()
|
|
};
|
|
ALEvent.FETCH.dispatchedTo(document, {task});
|
|
|
|
return fetch(request)
|
|
.then((resp) => {
|
|
if (params.dataType && params.dataType.includes('json')) {
|
|
return resp.json();
|
|
} else if (params.dataType && params.dataType.includes('blob')) {
|
|
return resp.blob();
|
|
} else if (params.dataType && params.dataType.includes('arrayBuffer')) {
|
|
return resp.arrayBuffer();
|
|
} else if (params.dataType && params.dataType.includes('readableStream')) {
|
|
return Promise.resolve(resp.body);
|
|
} else {
|
|
return resp.text()
|
|
}
|
|
})
|
|
.then(data => {
|
|
if (params.success) {
|
|
return params.success(data)
|
|
}
|
|
|
|
return Promise.resolve(data);
|
|
})
|
|
.catch(e => {
|
|
if (params.error) {
|
|
return params.error(e)
|
|
}
|
|
|
|
return Promise.reject(e);
|
|
})
|
|
.finally(() => {
|
|
ALEvent.RESOURCE_FETCHED.dispatchedTo(document, {task});
|
|
})
|
|
}
|
|
|
|
Utils.fetchWithProxy = function(params) {
|
|
return Utils.fetch({
|
|
...params,
|
|
error: (e) => {
|
|
console.error(e)
|
|
console.info("Trying querying the proxy:" + JSONP_PROXY)
|
|
// Try as CORS
|
|
const url = JSONP_PROXY + '?url=' + params.url;
|
|
return Utils.fetch({
|
|
...params,
|
|
url,
|
|
});
|
|
}
|
|
})
|
|
}
|
|
|
|
Utils.on = function(element, events, callback) {
|
|
events.split(' ')
|
|
.forEach(e => {
|
|
element.addEventListener(e, callback)
|
|
})
|
|
}
|
|
|
|
Utils.isTouchScreenDevice = undefined;
|
|
|
|
Utils.hasTouchScreen = function() {
|
|
if (Utils.isTouchScreenDevice === undefined) {
|
|
let hasTouchScreen = false;
|
|
if ("maxTouchPoints" in navigator) {
|
|
hasTouchScreen = navigator.maxTouchPoints > 0;
|
|
} else if ("msMaxTouchPoints" in navigator) {
|
|
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
|
} else {
|
|
const mQ = matchMedia?.("(pointer:coarse)");
|
|
if (mQ?.media === "(pointer:coarse)") {
|
|
hasTouchScreen = !!mQ.matches;
|
|
} else if ("orientation" in window) {
|
|
hasTouchScreen = true; // deprecated, but good fallback
|
|
} else {
|
|
// Only as a last resort, fall back to user agent sniffing
|
|
const UA = navigator.userAgent;
|
|
hasTouchScreen =
|
|
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
|
|
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
|
|
}
|
|
}
|
|
|
|
Utils.isTouchScreenDevice = hasTouchScreen;
|
|
}
|
|
|
|
return Utils.isTouchScreenDevice;
|
|
}
|
|
|
|
|
|
|
|
Utils.openNewTab = function(url) {
|
|
window.open(url, '_blank').focus();
|
|
}
|
|
|
|
// return true if script is executed in a HTTPS context
|
|
// return false otherwise
|
|
Utils.isFileContext = function () {
|
|
return (window.location.protocol === 'file:')
|
|
}
|
|
|
|
Utils.isHttpsContext = function () {
|
|
return (window.location.protocol === 'https:')
|
|
}
|
|
|
|
Utils.isHttpContext = function () {
|
|
return (window.location.protocol === 'http:')
|
|
}
|
|
|
|
Utils.fixURLForHTTPS = function (url) {
|
|
const switchToHttps = Utils.HTTPS_WHITELIST.some(element => {
|
|
return url.includes(element)
|
|
})
|
|
|
|
if (switchToHttps) {
|
|
return url.replace('http://', 'https://')
|
|
}
|
|
|
|
return url
|
|
}
|
|
|
|
Utils.isUrl = function(url) {
|
|
try {
|
|
return new URL(url).href;
|
|
} catch(e) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// generate an absolute URL from a relative URL
|
|
// example: getAbsoluteURL('foo/bar/toto') return http://cds.unistra.fr/AL/foo/bar/toto if executed from page http://cds.unistra.fr/AL/
|
|
Utils.getAbsoluteURL = function (url) {
|
|
var a = document.createElement('a')
|
|
a.href = url
|
|
|
|
return a.href
|
|
}
|
|
|
|
// generate a valid v4 UUID
|
|
Utils.uuidv4 = function () {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8)
|
|
return v.toString(16)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @function
|
|
* @description Deep clone a class instance.
|
|
* @param {object} instance The class instance you want to clone.
|
|
* @returns {object} A new cloned instance.
|
|
*/
|
|
Utils.clone = function (instance) {
|
|
return Object.assign(
|
|
Object.create(
|
|
// Set the prototype of the new object to the prototype of the instance.
|
|
// Used to allow new object behave like class instance.
|
|
Object.getPrototypeOf(instance),
|
|
),
|
|
// Prevent shallow copies of nested structures like arrays, etc
|
|
JSON.parse(JSON.stringify(instance)),
|
|
)
|
|
}
|
|
|
|
Utils.getDroppedFilesHandler = function (ev) {
|
|
// Prevent default behavior (Prevent file from being opened)
|
|
ev.preventDefault()
|
|
|
|
let items
|
|
if (ev.dataTransfer.items) {
|
|
// Use DataTransferItemList interface to access the file(s)
|
|
items = [...ev.dataTransfer.items]
|
|
} else {
|
|
// Use DataTransfer interface to access the file(s)
|
|
items = [...ev.dataTransfer.files]
|
|
}
|
|
|
|
const files = items.filter((item) => {
|
|
// If dropped items aren't files, reject them
|
|
return item.kind === 'file'
|
|
})
|
|
.map((item) => item.getAsFile())
|
|
|
|
return files
|
|
}
|
|
|
|
Utils.dragOverHandler = function (ev) {
|
|
// Prevent default behavior (Prevent file from being opened)
|
|
ev.preventDefault()
|
|
}
|
|
|
|
Utils.requestCORSIfNotSameOrigin = function (url) {
|
|
return (new URL(url, window.location.href)).origin !== window.location.origin
|
|
}
|
|
|
|
// Check the protocol, for http ones, use a CORS compatible proxy
|
|
Utils.handleCORSNotSameOrigin = function (url) {
|
|
if (Utils.requestCORSIfNotSameOrigin(url)) {
|
|
// http(s) protocols and not in localhost
|
|
let proxiedUrl = new URL(JSONP_PROXY)
|
|
proxiedUrl.searchParams.append('url', url)
|
|
|
|
url = proxiedUrl
|
|
}
|
|
|
|
return url
|
|
}
|
|
|
|
Utils.deepCopy = function (orig) {
|
|
return Object.assign(Object.create(Object.getPrototypeOf(orig)), orig)
|
|
}
|
|
|
|
Utils.download = function(url, name = undefined) {
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
if (name) {
|
|
a.download = name;
|
|
}
|
|
a.click()
|
|
}
|
|
|
|
// Copy some text to the clipboard
|
|
// This provides an alternative when the window context is not secure (e.g. http access)
|
|
// Code copied from: https://stackoverflow.com/questions/51805395/navigator-clipboard-is-undefined
|
|
Utils.copy2Clipboard = async function(textToCopy) {
|
|
// Navigator clipboard api needs a secure context (https)
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(textToCopy);
|
|
} else {
|
|
// Use the 'out of viewport hidden text area' trick
|
|
const textArea = document.createElement("textarea");
|
|
textArea.value = textToCopy;
|
|
|
|
// Move textarea out of the viewport so it's not visible
|
|
textArea.style.position = "absolute";
|
|
textArea.style.opacity = "0";
|
|
|
|
document.body.prepend(textArea);
|
|
textArea.select();
|
|
|
|
try {
|
|
// execCommand is deprecated but there seems to be no other
|
|
// alternative and is still used by a lot of applications
|
|
// See: https://stackoverflow.com/questions/60581285/execcommand-is-now-obsolete-whats-the-alternative
|
|
document.execCommand('copy');
|
|
} catch (error) {
|
|
console.error(error);
|
|
} finally {
|
|
textArea.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
Utils.measureTime = function(msg, method) {
|
|
let startTime = performance.now()
|
|
let output = method()
|
|
let deltaTime = performance.now() - startTime;
|
|
console.log(msg, deltaTime);
|
|
|
|
return output;
|
|
};
|