Aladin.js

// Copyright 2013 - UDS/CNRS
// The Aladin Lite program is distributed under the terms
// of the GNU General Public License version 3.
//
// 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 General Public License as published by
//    the Free Software Foundation, version 3 of the License.
//
//    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 General Public License for more details.
//
//    The GNU General Public License is available in COPYING file
//    along with Aladin Lite.
//

/******************************************************************************
 * Aladin Lite project
 *
 * File Aladin.js (main class)
 * Facade to expose Aladin Lite methods
 *
 * Author: Thomas Boch[CDS], Matthieu Baumann[CDS]
 *
 *****************************************************************************/

import { version } from "./../../package.json";
import { View } from "./View.js";
import { Utils } from "./Utils";
import { GraphicOverlay } from "./Overlay.js";
import { Logger } from "./Logger.js";
import { ProgressiveCat } from "./ProgressiveCat.js";
import { Sesame } from "./Sesame.js";
import { PlanetaryFeaturesNameResolver } from "./PlanetaryFeaturesNameResolver.js";
import { CooFrameEnum } from "./CooFrameEnum.js";
import { MeasurementTable } from "./MeasurementTable.js";
import { HiPS } from "./HiPS.js";
import { Coo } from "./libs/astro/coo.js";
import { CooConversion } from "./CooConversion.js";
import { HiPSCache } from "./HiPSCache.js";
import { HiPSList } from "./DefaultHiPSList.js";

import { ProjectionEnum } from "./ProjectionEnum.js";

import { ALEvent } from "./events/ALEvent.js";
import { Color } from "./Color.js";
import { Image } from "./Image.js";
import { DefaultActionsForContextMenu } from "./DefaultActionsForContextMenu.js";
import { SAMPConnector } from "./vo/samp.js";
import { Reticle } from "./Reticle.js";
import { requestAnimFrame } from "./libs/RequestAnimationFrame.js";

// GUI
import { AladinLogo } from "./gui/AladinLogo.js";
import { Location } from "./gui/Location.js";
import { FoV } from "./gui/FoV.js";
import { ShareActionButton } from "./gui/Button/ShareView.js";
import { ContextMenu } from "./gui/Widgets/ContextMenu.js";
import { Popup } from "./Popup.js";
import A from "./A.js";
import { StatusBarBox } from "./gui/Box/StatusBarBox.js";
import { FullScreenActionButton } from "./gui/Button/FullScreen.js";
import { ProjectionActionButton } from "./gui/Button/Projection.js";

// features
import { SettingsButton } from "./gui/Button/Settings";
import { SimbadPointer } from "./gui/Button/SimbadPointer";
import { OverlayStackButton } from "./gui/Button/OverlayStack";
import { GridEnabler } from "./gui/Button/GridEnabler";
import { CooFrame } from "./gui/Input/CooFrame";
import { Circle } from "./shapes/Circle";
import { Ellipse } from "./shapes/Ellipse";
import { Polyline } from "./shapes/Polyline";

/**
 * @typedef {Object} AladinOptions
 * @description Options for configuring the Aladin Lite instance.
 *
 * @property {string} [survey="P/DSS2/color"] URL or ID of the survey to use
 * @property {string[]} [surveyUrl]
 *   Array of URLs for the survey images. This replaces the survey parameter.
 * @property {Object[]|string[]} [hipsList] A list of predefined HiPS for the Aladin instance.
 *   This option is used for searching for a HiPS in a list of surveys
 *   This list can have string item (either a CDS ID or an HiPS url) or an object that describes the HiPS
 *   more exhaustively. See the example below to see the different form that this item can have to describe a HiPS.
 * @property {string} [target="0 +0"] - Target coordinates for the initial view.
 * @property {CooFrame} [cooFrame="J2000"] - Coordinate frame.
 * @property {number} [fov=60] - Field of view in degrees.
 * @property {number} [northPoleOrientation=0] - North pole orientation in degrees. By default it is set to 0 deg i.e. the north pole will be found vertically north to the view.
 *  Positive orientation goes towards east i.e. in counter clockwise order as the east lies in the left direction of the view.
 * @property {string} [backgroundColor="rgb(60, 60, 60)"] - Background color in RGB format.
 *
 * @property {boolean} [showZoomControl=true] - Whether to show the zoom control toolbar.
 * This element belongs to the FoV UI thus its CSS class is `aladin-fov` 
 * @property {boolean} [showLayersControl=true] - Whether to show the layers control toolbar.
 * CSS class for that button is `aladin-stack-control` 
 * @property {boolean} [expandLayersControl=false] - Whether to show the stack box opened at starting
 * CSS class for the stack box is `aladin-stack-box`
 * @property {boolean} [showFullscreenControl=true] - Whether to show the fullscreen control toolbar.
 * CSS class for that button is `aladin-fullScreen-control` 
 * @property {boolean} [showSimbadPointerControl=false] - Whether to show the Simbad pointer control toolbar.
 * CSS class for that button is `aladin-simbadPointer-control` 
 * @property {boolean} [showCooGridControl=false] - Whether to show the coordinate grid control toolbar.
 * CSS class for that button is `aladin-grid-control` 
 * @property {boolean} [showSettingsControl=false] - Whether to show the settings control toolbar.
 * CSS class for that button is `aladin-settings-control` 
 * @property {boolean} [showShareControl=false] - Whether to show the share control toolbar.
 * CSS class for that button is `aladin-share-control` 
 * @property {boolean} [showStatusBar=true] - Whether to show the status bar. Enabled by default.
 * CSS class for that button is `aladin-status-bar` 
 * @property {boolean} [showFrame=true] - Whether to show the viewport frame.
 * CSS class for that button is `aladin-cooFrame` 
 * @property {boolean} [showFov=true] - Whether to show the field of view indicator.
 * CSS class for that button is `aladin-fov` 
 * @property {boolean} [showCooLocation=true] - Whether to show the coordinate location indicator.
 * CSS class for that button is `aladin-location` 
 * @property {boolean} [showProjectionControl=true] - Whether to show the projection control toolbar.
 * CSS class for that button is `aladin-projection-control` 
 * @property {boolean} [showContextMenu=false] - Whether to show the context menu.
 * @property {boolean} [showReticle=true] - Whether to show the reticle.
 * @property {boolean} [showCatalog=true] - Whether to show the catalog.
 * @property {boolean} [showCooGrid=true] - Whether the coordinates grid should be shown at startup.
 *
 * @property {boolean} [fullScreen=false] - Whether to start in full-screen mode.
 * @property {string} [reticleColor="rgb(178, 50, 178)"] - Color of the reticle in RGB format.
 * @property {number} [reticleSize=22] - Size of the reticle.
 * 
 * @property {string} [gridColor="rgb(178, 50, 178)"] - Color of the grid in RGB format. 
 *                                                      Is overshadowed by gridOptions.color if defined.
 * @property {number} [gridOpacity=0.8] - Opacity of the grid (0 to 1). 
 *                                        Is overshadowed by gridOptions.opacity if defined.
 * @property {Object} [gridOptions] - More options for the grid.
 * @property {string} [gridOptions.color="rgb(178, 50, 178)"] - Color of the grid. Can be specified as a named color 
 *                    (see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/named-color| named colors}),
 *                    as rgb (ex: "rgb(178, 50, 178)"), or as a hex color (ex: "#86D6AE").              
 * @property {number} [gridOptions.thickness=2] - The thickness of the grid, in pixels.
 * @property {number} [gridOptions.opacity=0.8] - Opacity of the grid and labels. It is comprised between 0 and 1.
 * @property {boolean} [gridOptions.showLabels=true] - Whether the grid has labels.
 * @property {number} [gridOptions.labelSize=15] - The font size of the labels.
 * 
 * @property {string} [projection="SIN"] - Projection type. Can be 'SIN' for orthographic, 'MOL' for mollweide, 'AIT' for hammer-aitoff, 'ZEA' for zenital equal-area or 'MER' for mercator
 * @property {boolean} [log=true] - Whether to log events.
 * @property {boolean} [samp=false] - Whether to enable SAMP (Simple Application Messaging Protocol).
 * @property {boolean} [realFullscreen=false] - Whether to use real fullscreen mode.
 * @property {boolean} [pixelateCanvas=true] - Whether to pixelate the canvas.
 * @property {boolean} [manualSelection=false] - When set to true, no selection will be performed, only events will be generated.
 * @property {Object} [selector] - More options for the the selector.
 * @property {string} [selector.color] - Color of the selector, defaults to the color of the reticle. Can be a hex color or a function returning a hex color.
 * @property {number} [selector.lineWidth=2] - Width of the selector line.
 * 
 * @example
 * let aladin = A.aladin({
    target: 'galactic center',
    fov: 10,
    hipsList: [
        // url
        "https://alaskybis.unistra.fr/DSS/DSSColor",
        // ID from HiPS list
        "CDS/P/2MASS/color",
        // Not full HiPS described
        {
            name: 'DESI Legacy Surveys color (g, r, i, z)',
            id: 'CDS/P/DESI-Legacy-Surveys/DR10/color',
        },
        // HiPS with options. Fields accepted are those described in {@link A.hiPSOptions}.
        {
            name: "SDSS9 band-g",
            id: "P/SDSS9/g",
            creatorDid: "ivo://CDS/P/SDSS9/g",
            maxOrder: 10,
            tileSize: 512,
            numBitsPerPixel: 16,
            imgFormat: 'fits',
            cooFrame: 'equatorial',
            minCut: 0,
            maxCut: 1.8,
            stretch: 'linear',
            colormap: "redtemperature",
        }
    ]
})*/

/**
 * @typedef {Object} CircleSelection
 * @description Options for configuring the Aladin Lite instance.
 *
 * @property {number} x - x coordinate of the center's circle in pixels
 * @property {number} y - y coordinate of the center's circle in pixels
 * @property {number} r - radius of the circle in pixels
 * @property {function} contains - function taking a {x, y} object telling if the vertex is contained or not
 * @property {function} bbox - returns the bbox of the selection in pixels
 */

/**
 * @typedef {Object} RectSelection
 * @description Options for configuring the Aladin Lite instance.
 *
 * @property {number} x - top left x coordinate of the rectangle in pixels
 * @property {number} y - top left y coordinate of the rectangle in pixels
 * @property {number} w - width of the selection in pixels
 * @property {number} h - height of the selection in pixels
 * @property {function} contains - function taking a {x, y} object telling if the vertex is contained in the selection or not
 * @property {function} bbox - returns the bbox of the selection in pixels
 */

/**
 * @typedef {Object} PolygonSelection
 * @description Options for configuring the Aladin Lite instance.
 *
 * @property {Object[]} vertices - vertices of the polygon selection in pixels. Each vertex has a x and y key in pixels.
 * @property {function} contains - function taking a {x, y} object telling if the vertex is contained in the selection or not
 * @property {function} bbox - returns the bbox of the selection in pixels
 */

/**
 * @typedef {string} CooFrame
 * String with possible values: 'equatorial', 'ICRS', 'ICRSd', 'j2000', 'gal, 'galactic'
 */

/**
 * @typedef {string} ListenerCallback
 * String with possible values:
 *      'select' (deprecated, use objectsSelected instead),
 *      'objectsSelected',
        'objectClicked',
        'objectHovered',
        'objectHoveredStop',

        'footprintClicked',
        'footprintHovered',

        'positionChanged',
        'zoomChanged',

        'click',
        'rightClickMove',
        'mouseMove',

        'fullScreenToggled',
        'cooFrameChanged',
        'resizeChanged',
        'projectionChanged',
        'layerChanged'
 */

export let Aladin = (function () {
    /**
     * Creates an instance of the Aladin interactive sky atlas.
     *
     * @class
     * @constructs Aladin
     * @param {string|HTMLElement} aladinDiv - The ID of the HTML element or the HTML element itself
     *                                         where the Aladin sky atlas will be rendered.
     * @param {AladinOptions} requestedOptions - Options to customize the behavior and appearance of the Aladin atlas.
     * @throws {Error} Throws an error if aladinDiv is not provided or is invalid.
     *
     * @example
     * // Usage example:
     * import { A } from 'aladin-lite';
     *
     * let aladin = A.Aladin('#aladin-lite-div', { option1: 'value1', option2: 'value2' });
     */
    var Aladin = function (aladinDiv, requestedOptions) {
        this.callbacksByEventName = {}; // we store the callback functions (on 'zoomChanged', 'positionChanged', ...) here
        this.hipsCache = new HiPSCache();

        // check that aladinDiv exists, stop immediately otherwise
        if (!aladinDiv) {
            console.error(
                "Aladin div has not been found. Please check its name!"
            );
            return;
        }
        this.wasm = null;
        this.aladinDiv = aladinDiv;

        const self = this;

        ALEvent.HIPS_LAYER_ADDED.listenedBy(aladinDiv, (imageLayer) => {
            this.callbacksByEventName["layerChanged"] &&
            this.callbacksByEventName["layerChanged"](imageLayer.detail.layer, imageLayer.detail.layer.layer, "ADDED");
        });

        ALEvent.HIPS_LAYER_REMOVED.listenedBy(aladinDiv, (imageLayer) => {
            this.callbacksByEventName["layerChanged"] &&
            this.callbacksByEventName["layerChanged"](imageLayer.detail.layer, imageLayer.detail.layer.layer, "REMOVED");
        });

        // if not options was set, try to retrieve them from the query string
        if (requestedOptions === undefined) {
            requestedOptions = this.getOptionsFromQueryString();
        }
        requestedOptions = requestedOptions || {};

        // 'fov' option was previsouly called 'zoom'
        if ("zoom" in requestedOptions) {
            var fovValue = requestedOptions.zoom;
            delete requestedOptions.zoom;
            requestedOptions.fov = fovValue;
        }

        // merge with default options
        var options = {};

        for (var key in Aladin.DEFAULT_OPTIONS) {
            if (requestedOptions[key] !== undefined) {
                options[key] = requestedOptions[key];
            } else {
                options[key] = Aladin.DEFAULT_OPTIONS[key];
            }
        }

        // 'gridOptions' is an object, so it need it own loop
        if ("gridOptions" in requestedOptions) {
            for (var key in Aladin.DEFAULT_OPTIONS.gridOptions) {
                if (requestedOptions.gridOptions[key] === undefined) {
                    options.gridOptions[key] =
                        Aladin.DEFAULT_OPTIONS.gridOptions[key];
                }
            }
        }

        for (var key in requestedOptions) {
            if (Aladin.DEFAULT_OPTIONS[key] === undefined) {
                options[key] = requestedOptions[key];
            }
        }

        this.options = options;

        this.reduceDeformations = true;

        // Init the measurement table
        this.measurementTable = new MeasurementTable(this);

        // parent div
        aladinDiv.classList.add("aladin-container");
        // set different options
        // Reticle
        this.view = new View(this);

        // Aladin logo
        new AladinLogo(this.aladinDiv);

        this.reticle = new Reticle(this.options, this);
        this.popup = new Popup(this.aladinDiv, this.view);

        this.ui = [];

        // Background color
        if (options.backgroundColor) {
            this.backgroundColor = options.backgroundColor;
            this.setBackgroundColor(this.backgroundColor);
        }

        // Grid
        let gridOptions = options.gridOptions;

        // color and opacity can be defined by two variables. The item in gridOptions
        // should take precedence.
        gridOptions["color"] = options.gridOptions.color || options.gridColor;
        gridOptions["opacity"] =
            options.gridOptions.opacity || options.gridOpacity;
        if (options && options.showCooGrid) {
            gridOptions.enabled = true;
        }

        this.setCooGrid(gridOptions);

        this.gotoObject(options.target, undefined);

        if (options.log) {
            var params = options;
            params["version"] = Aladin.VERSION;
            Logger.log("startup", params);
        }

        if (options.catalogUrls) {
            for (var k = 0, len = options.catalogUrls.length; k < len; k++) {
                this.createCatalogFromVOTable(options.catalogUrls[k]);
            }
        }

        let hipsList = [].concat(options.hipsList);

        const fillHiPSCache = () => {
            for (var survey of hipsList) {
                let id, url, name;
                let cachedSurvey = {};

                if (typeof survey === "string") {
                    try {
                        url = new URL(survey).href;
                    } catch (e) {
                        id = survey;
                    }

                    name = url || id;
                } else if (survey instanceof Object) {
                    if (survey.id) {
                        id = survey.id;
                    }
                    if (survey.url) {
                        url = survey.url;
                    }

                    name = survey.name || survey.id || survey.url;

                    cachedSurvey = { ...cachedSurvey, ...survey };
                } else {
                    console.warn(
                        "unable to parse the survey list item: ",
                        survey
                    );
                    continue;
                }

                if (id) {
                    cachedSurvey["id"] = id;
                }
                if (url) {
                    cachedSurvey["url"] = url;
                }
                if (name) {
                    cachedSurvey["name"] = name;
                }

                // at least id or url is defined
                let key = name || id || url;

                // Merge what is already in the cache for that HiPS with new properties
                // coming from the MOCServer
                self.hipsCache.append(key, cachedSurvey);
            }
        };
        this._setupUI(options);

        fillHiPSCache();

        if (options.survey) {
            if (Array.isArray(options.survey)) {
                let i = 0;
                options.survey.forEach((rootURLOrId) => {
                    if (i == 0) {
                        this.setBaseImageLayer(rootURLOrId);
                    } else {
                        this.setOverlayImageLayer(rootURLOrId, Utils.uuidv4());
                    }
                    i++;
                });
            } else if (options.survey === HiPS.DEFAULT_SURVEY_ID) {
                // DSS is cached inside HiPS class, no need to provide any further information
                const survey = this.createImageSurvey(
                    HiPS.DEFAULT_SURVEY_ID
                );

                this.setBaseImageLayer(survey);
            } else {
                this.setBaseImageLayer(options.survey);
            }
        } else {
            // Add the image layers
            // For that we check the survey key of options
            // It can be given as a single string or an array of strings
            // for multiple blending surveys
            // take in priority the surveyUrl parameter
            let url;
            if (Array.isArray(options.surveyUrl)) {
                // mirrors given, take randomly one
                let numMirrors = options.surveyUrl.length;
                let id = Math.floor(Math.random() * numMirrors);
                url = options.surveyUrl[id];
            } else {
                url = options.surveyUrl;
            }

            this.setBaseImageLayer(url);
        }

        this.view.showCatalog(options.showCatalog);

        // FullScreen toolbar icon
        this.isInFullscreen = false;
        // go to full screen ?
        if (options.fullScreen === true) {
            // strange behaviour to wait for a sec
            self.toggleFullscreen(self.options.realFullscreen);
        }

        // maximize control
        if (options.showFullscreenControl) {
            // react to fullscreenchange event to restore initial width/height (if user pressed ESC to go back from full screen)
            Utils.on(
                document,
                "fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange",
                function (e) {
                    var fullscreenElt =
                        document.fullscreenElement ||
                        document.webkitFullscreenElement ||
                        document.mozFullScreenElement ||
                        document.msFullscreenElement;
                    if (fullscreenElt === null || fullscreenElt === undefined) {
                        //self.aladinDiv.classList.remove('aladin-fullscreen');

                        var fullScreenToggledFn =
                            self.callbacksByEventName["fullScreenToggled"];
                        typeof fullScreenToggledFn === "function" &&
                            fullScreenToggledFn(self.isInFullscreen);
                    }
                }
            );
        }

        // set right click context menu
        if (options.showContextMenu) {
            this.contextMenu = new ContextMenu(this);
            this.contextMenu.attach(
                DefaultActionsForContextMenu.getDefaultActions(this)
            );
        }

        if (options.samp) {
            this.samp = new SAMPConnector(this);
        }

        if (options.inertia !== undefined) {
            this.wasm.setInertia(options.inertia);
        }

        if (options.northPoleOrientation) {
            this.setViewCenter2NorthPoleAngle(options.northPoleOrientation);
        }
    };

    Aladin.prototype._setupUI = function (options) {
        let self = this;

        // Status bar
        if (options.showStatusBar) {
            this.statusBar = new StatusBarBox(this);
            this.addUI(this.statusBar);
        }

        // Add the frame control
        if (options.showFrame) {
            this.addUI(new CooFrame(this));
        }

        // Add the location info
        if (options.showCooLocation) {
            this.addUI(new Location(this));
        }

        // Add the FoV info
        if (options.showFov || options.showZoomControl) {
            this.addUI(new FoV(this, options));
        }

        ////////////////////////////////////////////////////
        let stack = new OverlayStackButton(this);
        let simbad = new SimbadPointer(this);
        let grid = new GridEnabler(this);
        this.addUI(stack);
        this.addUI(simbad);
        this.addUI(grid);

        // Add the layers control
        if (!options.showLayersControl) {
            stack._hide();
        }

        // Add the simbad pointer control
        if (!options.showSimbadPointerControl) {
            simbad._hide();
        }

        // Add the projection control
        // Add the coo grid control
        if (!options.showCooGridControl) {
            grid._hide();
        }

        // Settings control
        if (options.showSettingsControl) {
            let settings = new SettingsButton(this, {
                features: { stack, simbad, grid },
            });
            this.addUI(settings);
        }

        // share control panel
        if (options.showShareControl) {
            this.addUI(new ShareActionButton(self));
        }

        if (options.showProjectionControl) {
            this.projBtn = new ProjectionActionButton(this);
            this.addUI(this.projBtn);
        }

        if (options.showFullscreenControl) {
            this.addUI(new FullScreenActionButton(self));
        }

        if (options.expandLayersControl) {
            stack.click();
        }

        this._applyMediaQueriesUI();
    };

    Aladin.prototype._applyMediaQueriesUI = function () {
        const applyMediaQuery = function (
            maxWidth,
            matchingCallback,
            unmatchingCallback
        ) {
            function mqFunction(x) {
                if (x.matches) {
                    // If media query matches
                    matchingCallback();
                } else {
                    unmatchingCallback();
                }
            }

            // Create a MediaQueryList object
            var mq = window.matchMedia("(max-width: " + maxWidth + ")");

            // Attach listener function on state changes
            mq.addEventListener("change", function () {
                mqFunction(mq);
            });

            mqFunction(mq);
        };

        let self = this;

        applyMediaQuery(
            "48rem",
            () => {
                if (self.projBtn) {
                    self.projBtn.update({ verbosity: "reduced" });
                }
            },
            () => {
                if (self.projBtn) {
                    self.projBtn.update({ verbosity: "full" });
                }
            }
        );
    };

    /**** CONSTANTS ****/
    Aladin.VERSION = version;

    Aladin.JSONP_PROXY = "https://alaskybis.cds.unistra.fr/cgi/JSONProxy";
    //Aladin.JSONP_PROXY = "https://alaskybis.unistra.fr/cgi/JSONProxy";

    Aladin.URL_PREVIEWER = "https://aladin.cds.unistra.fr/AladinLite/";

    // access to WASM libraries
    Aladin.wasmLibs = {};
    Aladin.DEFAULT_OPTIONS = {
        survey: HiPS.DEFAULT_SURVEY_ID,
        // surveys suggestion list
        hipsList: HiPSList.DEFAULT,
        //surveyUrl: ["https://alaskybis.unistra.fr/DSS/DSSColor", "https://alasky.unistra.fr/DSS/DSSColor"],
        target: "0 +0",
        cooFrame: "J2000",
        fov: 60,
        northPoleOrientation: 0,
        inertia: true,
        backgroundColor: "rgb(60, 60, 60)",
        // Zoom toolbar
        showZoomControl: true,
        // Menu toolbar
        showLayersControl: true,
        expandLayersControl: false,
        showFullscreenControl: true,
        showSimbadPointerControl: false,
        showCooGridControl: false,
        showSettingsControl: false,
        // Share toolbar
        showShareControl: false,

        // Viewport toolbar
        showFrame: true,
        showFov: true,
        showCooLocation: true,
        showProjectionControl: true,

        // Other UI elements
        showContextMenu: false,
        showStatusBar: true,
        // Internal
        showReticle: true,
        showCatalog: true, // TODO: still used ??

        fullScreen: false,
        reticleColor: "rgb(178, 50, 178)",
        reticleSize: 22,
        gridColor: "rgb(178, 50, 178)",
        gridOpacity: 0.8,
        gridOptions: {
            enabled: false,
            showLabels: true,
            thickness: 2,
            labelSize: 15,
        },
        projection: "SIN",
        log: true,
        samp: false,
        realFullscreen: false,
        pixelateCanvas: true,
        manualSelection: false
    };

    // realFullscreen: AL div expands not only to the size of its parent, but takes the whole available screen estate
    Aladin.prototype.toggleFullscreen = function (realFullscreen) {
        let self = this;

        realFullscreen = Boolean(realFullscreen);
        self.isInFullscreen = !self.isInFullscreen;

        ContextMenu.hideAll();

        this.ui.forEach(ui => {
            if (ui.toggle) {
                ui.toggle();
                ui.toggle();
            }
        })

        //this.fullScreenBtn.attr('title', isInFullscreen ? 'Restore original size' : 'Full screen');

        if (this.aladinDiv.classList.contains("aladin-fullscreen")) {
            this.aladinDiv.classList.remove("aladin-fullscreen");
        } else {
            this.aladinDiv.classList.add("aladin-fullscreen");
        }

        if (realFullscreen) {
            // go to "real" full screen mode
            if (self.isInFullscreen) {
                var d = this.aladinDiv;

                if (d.requestFullscreen) {
                    d.requestFullscreen();
                } else if (d.webkitRequestFullscreen) {
                    d.webkitRequestFullscreen();
                } else if (d.mozRequestFullScreen) {
                    // notice the difference in capitalization for Mozilla functions ...
                    d.mozRequestFullScreen();
                } else if (d.msRequestFullscreen) {
                    d.msRequestFullscreen();
                }
            }
            // exit from "real" full screen mode
            else {
                if (document.exitFullscreen) {
                    document.exitFullscreen();
                } else if (document.webkitExitFullscreen) {
                    document.webkitExitFullscreen();
                } else if (document.mozCancelFullScreen) {
                    document.mozCancelFullScreen();
                } else if (document.webkitExitFullscreen) {
                    document.webkitExitFullscreen();
                }
            }
        }

        // Delay the fixLayoutDimensions layout for firefox
        /*setTimeout(function () {
            self.view.fixLayoutDimensions();
        }, 1000);*/

        // force call to zoomChanged callback
        var fovChangedFn = self.callbacksByEventName["zoomChanged"];
        typeof fovChangedFn === "function" && fovChangedFn(self.view.fov);

        var fullScreenToggledFn =
            self.callbacksByEventName["fullScreenToggled"];
        typeof fullScreenToggledFn === "function" &&
            fullScreenToggledFn(self.isInFullscreen);
    };

    Aladin.prototype.getOptionsFromQueryString = function () {
        var options = {};
        var requestedTarget = Utils.urlParam("target");
        if (requestedTarget) {
            options.target = requestedTarget;
        }
        var requestedFrame = Utils.urlParam("frame");
        if (requestedFrame && CooFrameEnum[requestedFrame]) {
            options.frame = requestedFrame;
        }
        var requestedSurveyId = Utils.urlParam("survey");
        if (
            requestedSurveyId &&
            HiPS.getSurveyInfoFromId(requestedSurveyId)
        ) {
            options.survey = requestedSurveyId;
        }
        var requestedZoom = Utils.urlParam("zoom");
        if (requestedZoom && requestedZoom > 0 && requestedZoom < 180) {
            options.zoom = requestedZoom;
        }

        var requestedShowreticle = Utils.urlParam("showReticle");
        if (requestedShowreticle) {
            options.showReticle = requestedShowreticle.toLowerCase() == "true";
        }

        var requestedCooFrame = Utils.urlParam("cooFrame");
        if (requestedCooFrame) {
            options.cooFrame = requestedCooFrame;
        }

        var requestedFullscreen = Utils.urlParam("fullScreen");
        if (requestedFullscreen !== undefined) {
            options.fullScreen = requestedFullscreen;
        }

        return options;
    };

    /**
     * Sets the field of view (FoV) of the Aladin instance to the specified angle in degrees.
     *
     * @memberof Aladin
     * @param {number} FoV - The angle of the field of view in degrees.
     *
     * @example
     * let aladin = A.aladin('#aladin-lite-div');
     * aladin.setFoV(60);
     */
    Aladin.prototype.setFoV = function (FoV) {
        this.view.setZoom(FoV);
    };

    Aladin.prototype.setFov = Aladin.prototype.setFoV;

    // @API
    // (experimental) try to adjust the FoV to the given object name. Does nothing if object is not known from Simbad
    Aladin.prototype.adjustFovForObject = function (objectName) {
        var self = this;
        this.getFovForObject(objectName, function (fovDegrees) {
            self.setFoV(fovDegrees);
        });
    };

    Aladin.prototype.getFovForObject = Aladin.prototype.getFoVForObject =
        function (objectName, callback) {
            var query =
                "SELECT galdim_majaxis, V FROM basic JOIN ident ON oid=ident.oidref JOIN allfluxes ON oid=allfluxes.oidref WHERE id='" +
                objectName +
                "'";
            var url =
                "//simbad.u-strasbg.fr/simbad/sim-tap/sync?query=" +
                encodeURIComponent(query) +
                "&request=doQuery&lang=adql&format=json&phase=run";

            Utils.fetch({
                url,
                method: "GET",
                dataType: "json",
                success: (result) => {
                    var defaultFov = 4 / 60; // 4 arcmin
                    var fov = defaultFov;

                    if ("data" in result && result.data.length > 0) {
                        var galdimMajAxis = Utils.isNumber(result.data[0][0])
                            ? result.data[0][0] / 60.0
                            : null; // result gives galdim in arcmin
                        var magV = Utils.isNumber(result.data[0][1])
                            ? result.data[0][1]
                            : null;

                        if (galdimMajAxis !== null) {
                            fov = 2 * galdimMajAxis;
                        } else if (magV !== null) {
                            if (magV < 10) {
                                fov = (2 * Math.pow(2.0, 6 - magV / 2.0)) / 60;
                            }
                        }
                    }

                    typeof callback === "function" && callback(fov);
                },
            });
        };

    /**
     * Sets the coordinate frame of the Aladin instance to the specified frame.
     *
     * @memberof Aladin
     * @param {string} frame - The name of the coordinate frame. Possible values: 'j2000d', 'j2000', 'gal', 'icrs'. The given string is case insensitive.
     *
     * @example
     * // Set the coordinate frame to 'J2000'
     * const aladin = A.aladin('#aladin-lite-div');
     * aladin.setFrame('J2000');
     */
    Aladin.prototype.setFrame = function (frame) {
        if (!frame) {
            return;
        }
        var newFrame = CooFrameEnum.fromString(frame, CooFrameEnum.J2000);
        if (newFrame == this.view.cooFrame) {
            return;
        }

        this.view.changeFrame(newFrame);

        var frameChangedFunction = this.callbacksByEventName["cooFrameChanged"];
        if (typeof frameChangedFunction === "function") {
            frameChangedFunction(newFrame.label);
        }
    };

    /**
     * Sets the projection of the Aladin instance to the specified type.
     *
     * @memberof Aladin
     * @param {string} projection The type of projection to set. Possible values
     * <br>"TAN" (Gnomonic projection)
     * <br>"STG" (Stereographic projection)
     * <br>"SIN" (Orthographic projection)
     * <br>"ZEA" (Zenital equal-area projection)
     * <br>"MER" (Mercator projection)
     * <br>"AIT" (Hammer-Aitoff projection)
     * <br>"MOL" (Mollweide projection)
     *
     * @example
     * // Set the projection to 'orthographic'
     * let aladin = A.aladin('#aladin-lite-div');
     * aladin.setProjection('SIN');
     */
    Aladin.prototype.setProjection = function (projection) {
        if (!projection) {
            return;
        }
        this.view.setProjection(projection);

        ALEvent.PROJECTION_CHANGED.dispatchedTo(this.aladinDiv, {
            projection,
        });
    };

    /**
     * Append a message to the status bar
     *
     * @memberof Aladin
     * @param {Object} options - The message to display
     * @param {string} options.id - The id of the message, is useful for removing unlimited time messages
     * @param {string} options.message - The message to display
     * @param {string|number} options.duration - The duration of the message. Accepts a time in milliseconds or 'unlimited'
     * @param {string} options.type - The type of the message. Can be 'loading', 'tooltip', 'info'
     *
     * @example
     *
     * aladin.addStatusBarMessage({
     *       duration: 10000,
     *       type: 'info',
     *       message: 'Aladin Lite v3.3 is out. New features available:<ul><li>New Button, Box objects</li><li>Polygonal, circular selection</li></ul>'
     * })
     */
    Aladin.prototype.addStatusBarMessage = function (options) {
        if (this.statusBar) {
            this.statusBar.appendMessage(options);
        }
    };

/**
     * Remove a message from the status bar
     *
     * @memberof Aladin
     * @param {string} id - The id of the message to remove
     */
    Aladin.prototype.removeStatusBarMessage = function (id) {
        if (this.statusBar) {
            this.statusBar.removeMessage(id);
        }
    };

    Aladin.prototype.getProjectionName = function () {
        const self = this;

        let projName = undefined;
        for (let key in ProjectionEnum) {
            if (ProjectionEnum[key] == self.view.projection) {
                projName = key;
                break;
            }
        }

        return projName;
    };
    ``;

    /**
     * Returns the current coordinate system: possible values are 'J2000', 'J2000d', and 'Galactic' .
     *
     * @memberof Aladin
     * @returns {string} The current coordinate system: possible values are 'J2000', 'J2000d', and 'Galactic' .
     *
     * @example
     * const aladin = A.aladin('#aladin-lite-div', {cooFrame: 'galactic'});
     * let cooFrame = aladin.getFrame();
     * assert(cooFrame, 'galactic')
     */
    Aladin.prototype.getFrame = function () {
        return this.view.cooFrame.label;
    };

    /**
     * Moves the Aladin instance to the specified astronomical object.
     *
     * @memberof Aladin
     * @param {string} targetName - The name or identifier of the astronomical object to move to.
     * @param {Object} [callbackOptions] - Optional callback options.
     * @param {function} [callbackOptions.success] - The callback function to execute on successful navigation.
     * @param {function} [callbackOptions.error] - The callback function to execute on error during navigation.
     *
     * @example
     * // Move to the astronomical object named 'M42' with callbacks
     * const aladinInstance = A.aladin('#aladin-lite-div');
     * aladinInstance.gotoObject('M42', {
     *   success: () => {
     *     console.log('Successfully moved to M42.');
     *   },
     *   error: (err) => {
     *     console.error('Error moving to M42:', err);
     *   }
     * });
     */
    Aladin.prototype.gotoObject = function (targetName, callbackOptions) {
        let successCallback = undefined;
        let errorCallback = undefined;
        if (typeof callbackOptions === "object") {
            if (callbackOptions.hasOwnProperty("success")) {
                successCallback = callbackOptions.success;
            }
            if (callbackOptions.hasOwnProperty("error")) {
                errorCallback = callbackOptions.error;
            }
        }
        // this is for compatibility reason with the previous method signature which was function(targetName, errorCallback)
        else if (typeof callbackOptions === "function") {
            errorCallback = callbackOptions;
        }

        var isObjectName = /[a-zA-Z]/.test(targetName);

        // try to parse as a position
        if (!isObjectName) {
            var coo = new Coo();
            coo.parse(targetName);
            // Convert from view coo sys to icrs
            const [ra, dec] = this.wasm.viewToICRSCooSys(coo.lon, coo.lat);

            this.view.pointTo(ra, dec);

            typeof successCallback === "function" &&
                successCallback(this.getRaDec());
        }
        // ask resolution by Sesame
        else {
            var self = this;
            // sky case
            (async () => {
                let baseImageLayer;
                if (this.getBaseImageLayer()) {
                    baseImageLayer = await this.getBaseImageLayer().query;
                }
                if (
                    this.getBaseImageLayer() === undefined ||
                    !baseImageLayer.isPlanetaryBody()
                ) {
                    Sesame.resolve(
                        targetName,
                        function (data) {
                            // success callback
                            // Location given in icrs at J2000
                            const coo = data.coo;
                            self.view.pointTo(coo.jradeg, coo.jdedeg);

                            typeof successCallback === "function" &&
                                successCallback(self.getRaDec());
                        },
                        function (data) {
                            // errror callback
                            if (console) {
                                console.log(
                                    "Could not resolve object name " +
                                        targetName
                                );
                                console.log(data);
                            }
                            typeof errorCallback === "function" &&
                                errorCallback();
                        }
                    );
                }
                // planetary case
                else {
                    const body = baseImageLayer.hipsBody;
                    PlanetaryFeaturesNameResolver.resolve(
                        targetName,
                        body,
                        function (data) {
                            // success callback
                            self.view.pointTo(data.lon, data.lat);

                            typeof successCallback === "function" &&
                                successCallback(self.getRaDec());
                        },
                        function (data) {
                            // error callback
                            if (console) {
                                console.log(
                                    "Could not resolve object name " +
                                        targetName
                                );
                                console.log(data);
                            }
                            typeof errorCallback === "function" &&
                                errorCallback();
                        }
                    );
                }
            })();
        }
    };

    /**
     * Moves the Aladin instance to the specified position.
     *
     * @memberof Aladin
     * @param {number} lon - longitude in degrees
     * @param {number} lat - latitude in degrees
     * @param {string} [frame] - The name of the coordinate frame. Possible values: 'j2000d', 'j2000', 'gal', 'icrs'. The given string is case insensitive.
     *
     * @example
     * // Move to position
     * const aladin = A.aladin('#aladin-lite-div');
     * aladin.gotoPosition(20, 10, "galactic");
     */
    Aladin.prototype.gotoPosition = function (lon, lat, frame) {
        var radec;
        // convert the frame from string to CooFrameEnum
        if (frame) {
            frame = CooFrameEnum.fromString(
                this.options.cooFrame,
                CooFrameEnum.J2000
            );
        }
        // both are CooFrameEnum
        let positionGivenFrame = frame || this.view.cooFrame;
        // First, convert to J2000 if needed
        if (positionGivenFrame === CooFrameEnum.GAL) {
            radec = CooConversion.GalacticToJ2000([lon, lat]);
        } else {
            radec = [lon, lat];
        }

        this.gotoRaDec(radec[0], radec[1]);
    };

    var idTimeoutAnim;
    var doAnimation = function (aladin) {
        /*if (idTimeoutAnim) {
            clearTimeout(idTimeoutAnim)
        }*/

        var params = aladin.animationParams;
        if (params == null || !params["running"]) {
            return;
        }
        var now = new Date().getTime();
        // this is the animation end: set the view to the end position, and call complete callback
        if (now > params["end"]) {
            aladin.gotoRaDec(params["raEnd"], params["decEnd"]);

            if (params["complete"]) {
                params["complete"]();
            }

            return;
        }

        // compute current position
        var fraction =
            (now - params["start"]) / (params["end"] - params["start"]);
        var curPos = intermediatePoint(
            params["raStart"],
            params["decStart"],
            params["raEnd"],
            params["decEnd"],
            fraction
        );
        var curRa = curPos[0];
        var curDec = curPos[1];
        //var curRa =  params['raStart'] + (params['raEnd'] - params['raStart']) * (now-params['start']) / (params['end'] - params['start']);
        //var curDec = params['decStart'] + (params['decEnd'] - params['decStart']) * (now-params['start']) / (params['end'] - params['start']);

        aladin.gotoRaDec(curRa, curDec);

        //idTimeoutAnim = setTimeout(function () { doAnimation(aladin); }, 10);
        requestAnimFrame(() => {
            doAnimation(aladin);
        });
    };

    /*
     * Stop all animations that have been initiated  by animateToRaDec or by zoomToFoV
     * @API
     *
     */
    Aladin.prototype.stopAnimation = function () {
        if (this.zoomAnimationParams) {
            this.zoomAnimationParams["running"] = false;
        }
        if (this.animationParams) {
            this.animationParams["running"] = false;
        }
    };

    /*
     * animate smoothly from the current position to the given ra, dec
     *
     * the total duration (in seconds) of the animation can be given (otherwise set to 5 seconds by default)
     *
     * complete: a function to call once the animation has completed
     *
     * @API
     *
     */
    Aladin.prototype.animateToRaDec = function (ra, dec, duration, complete) {
        duration = duration || 5;

        this.animationParams = null;

        var animationParams = {};
        animationParams["start"] = new Date().getTime();
        animationParams["end"] = new Date().getTime() + 1000 * duration;
        var raDec = this.getRaDec();
        animationParams["raStart"] = raDec[0];
        animationParams["decStart"] = raDec[1];
        animationParams["raEnd"] = ra;
        animationParams["decEnd"] = dec;
        animationParams["complete"] = complete;
        animationParams["running"] = true;

        this.animationParams = animationParams;

        doAnimation(this);
    };

    var doZoomAnimation = function (aladin) {
        var params = aladin.zoomAnimationParams;
        if (params == null || !params["running"]) {
            return;
        }
        var now = new Date().getTime();
        // this is the zoom animation end: set the view to the end fov, and call complete callback
        if (now > params["end"]) {
            aladin.setFoV(params["fovEnd"]);

            if (params["complete"]) {
                params["complete"]();
            }

            return;
        }

        // compute current position
        var fraction =
            (now - params["start"]) / (params["end"] - params["start"]);
        var curFov =
            params["fovStart"] +
            (params["fovEnd"] - params["fovStart"]) * Math.sqrt(fraction);

        aladin.setFoV(curFov);

        setTimeout(function () {
            doZoomAnimation(aladin);
        }, 50);
    };
    /*
     * zoom smoothly from the current FoV to the given new fov to the given ra, dec
     *
     * the total duration (in seconds) of the animation can be given (otherwise set to 5 seconds by default)
     *
     * complete: a function to call once the animation has completed
     *
     * @API
     *
     */
    Aladin.prototype.zoomToFoV = function (fov, duration, complete) {
        duration = duration || 5;

        this.zoomAnimationParams = null;

        var zoomAnimationParams = {};
        zoomAnimationParams["start"] = new Date().getTime();
        zoomAnimationParams["end"] = new Date().getTime() + 1000 * duration;
        var fovArray = this.getFov();
        zoomAnimationParams["fovStart"] = Math.max(fovArray[0], fovArray[1]);
        zoomAnimationParams["fovEnd"] = fov;
        zoomAnimationParams["complete"] = complete;
        zoomAnimationParams["running"] = true;

        this.zoomAnimationParams = zoomAnimationParams;
        doZoomAnimation(this);
    };

    /**
     *  Compute intermediate point between points (lng1, lat1) and (lng2, lat2)
     *  at distance fraction times the total distance (fraction between 0 and 1)
     *
     *  Return intermediate points in degrees
     *
     */
    function intermediatePoint(lng1, lat1, lng2, lat2, fraction) {
        function degToRad(d) {
            return (d * Math.PI) / 180;
        }
        function radToDeg(r) {
            return (r * 180) / Math.PI;
        }
        var lat1 = degToRad(lat1);
        var lng1 = degToRad(lng1);
        var lat2 = degToRad(lat2);
        var lng2 = degToRad(lng2);
        var d =
            2 *
            Math.asin(
                Math.sqrt(
                    Math.pow(Math.sin((lat1 - lat2) / 2), 2) +
                        Math.cos(lat1) *
                            Math.cos(lat2) *
                            Math.pow(Math.sin((lng1 - lng2) / 2), 2)
                )
            );
        var A = Math.sin((1 - fraction) * d) / Math.sin(d);
        var B = Math.sin(fraction * d) / Math.sin(d);
        var x =
            A * Math.cos(lat1) * Math.cos(lng1) +
            B * Math.cos(lat2) * Math.cos(lng2);
        var y =
            A * Math.cos(lat1) * Math.sin(lng1) +
            B * Math.cos(lat2) * Math.sin(lng2);
        var z = A * Math.sin(lat1) + B * Math.sin(lat2);
        var lon = Math.atan2(y, x);
        var lat = Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)));

        return [radToDeg(lon), radToDeg(lat)];
    }

    /**
     * Gets the current [Right Ascension, Declination] position of the center of the Aladin view.
     *
     * This method returns the celestial coordinates of the center of the Aladin view in the International
     * Celestial Reference System (ICRS) or J2000 equatorial coordinates.
     *
     * @memberof Aladin
     * @returns {number[]} - An array representing the [Right Ascension, Declination] coordinates in degrees.
     *                       The first element is the Right Ascension (RA), and the second element is the Declination (Dec).
     */
    Aladin.prototype.getRaDec = function () {
        let radec = this.wasm.getCenter(); // This is given in the frame of the view
        // We must convert it to ICRS
        const radec_j2000 = this.wasm.viewToICRSCooSys(radec[0], radec[1]);

        if (radec_j2000[0] < 0) {
            return [radec_j2000[0] + 360.0, radec_j2000[1]];
        }

        return radec_j2000;
    };

    /**
     * Moves the Aladin instance to the specified position given in ICRS frame
     *
     * @memberof Aladin
     * @param {number} ra - Right-ascension in degrees
     * @param {number} dec - Declination in degrees
     *
     * @example
     * const aladin = A.aladin('#aladin-lite-div');
     * aladin.gotoRaDec(20, 10);
     */
    Aladin.prototype.gotoRaDec = function (ra, dec) {
        this.view.pointTo(ra, dec);
    };

    Aladin.prototype.showHealpixGrid = function (show) {
        this.view.showHealpixGrid(show);
    };

    Aladin.prototype.healpixGrid = function () {
        return this.view.displayHpxGrid;
    };

    Aladin.prototype.showSurvey = function (show) {
        this.view.showSurvey(show);
    };

    Aladin.prototype.showCatalog = function (show) {
        this.view.showCatalog(show);
    };

    Aladin.prototype.showReticle = function (show) {
        this.reticle.update({ show });
    };

    Aladin.prototype.getReticle = function () {
        return this.reticle;
    };

    // these 4 methods should be merged into a unique "add" method
    Aladin.prototype.addCatalog = function (catalog) {
        this.view.addCatalog(catalog);

        ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.dispatchedTo(this.aladinDiv, {
            layer: catalog,
        });
    };

    Aladin.prototype.addOverlay = function (overlay) {
        this.view.addOverlay(overlay);

        ALEvent.GRAPHIC_OVERLAY_LAYER_ADDED.dispatchedTo(this.aladinDiv, {
            layer: overlay,
        });
    };

    Aladin.prototype.addMOC = function (moc) {
        this.view.addMOC(moc);

        // see MOC.setView for sending it to outside the UI
    };

    Aladin.prototype.removeUIByName = function(name) {
        let index = this.ui.findIndex((elm) => elm.name === name)
        if (index >= 0) {
            this.ui[index].remove();

            this.ui.splice(index, 1);
        }
    };

    Aladin.prototype.addUI = function (ui) {
        ui = [].concat(ui);

        for (var ui of ui) {
            this.ui.push(ui);
            ui.attachTo(this.aladinDiv);
    
            // as the ui is pushed to the dom, setting position may need the aladin instance to work
            // so we recompute it
            if (ui.options) {
                ui.update({ position: { ...ui.options.position, aladin: this } });
            }
        }
    };

    // @API
    Aladin.prototype.findLayerByUUID = function (uuid) {
        const result = this.view.allOverlayLayers.filter(
            (layer) => layer.uuid === uuid
        );
        if (result.length == 0) {
            return null;
        }

        return result[0];
    };

    /**
     * Remove all the overlays (MOC, Overlay, ProgressiveCat, Catalog) from the view
     * @memberof Aladin
     */
    Aladin.prototype.removeOverlays = function () {
        this.view.removeOverlays();
    };

    /**
     * @deprecated
     * Old method name, use {@link Aladin.prototype.removeOverlays} instead.
     * @memberof Aladin
     */
    Aladin.prototype.removeLayers = Aladin.prototype.removeOverlays;
    /**
    * @typedef {MOC|Catalog|ProgressiveCat|GraphicOverlay} Overlay
    * @description Possible overlays
    */
    /**
     * Remove an overlay by its layer name
     *
     * @memberof Aladin
     * @param {string|Overlay} overlay - The name of the overlay to remove or the overlay object itself
     */
    Aladin.prototype.removeOverlay = function (overlay) {
        if(typeof overlay === 'string' || overlay instanceof String) {
            this.view.removeOverlayByName(overlay);
        } else {
            this.view.removeOverlay(overlay);
        }
    };

    /**
     * @deprecated
     * Old method name, use {@link Aladin.prototype.removeOverlay} instead.
     * @memberof Aladin
     */
    Aladin.prototype.removeLayer = Aladin.prototype.removeOverlay;

    /**
     * @memberof Aladin
     * @param {string} id - Mandatory unique identifier for the survey.
     * @param {string} [name] - A convinient name for the survey, optional
     * @param {string|FileList|HiPSLocalFiles} url - Can be:
     * <ul>
     * <li>An http url towards a HiPS.</li>
     * <li>A relative path to your HiPS</li>
     * <li>A special ID pointing towards a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
     * <li>A dict storing a local HiPS files. This object contains a tile file: hips[order][ipix] = File and refers to the properties file like so: hips["properties"] = File. </li>
     *     A javascript {@link FileList} pointing to the opened webkit directory is also accepted.
     * </ul>
     * @param {string} [cooFrame] - Values accepted: 'equatorial', 'icrs', 'icrsd', 'j2000', 'gal', 'galactic'
     * @param {number} [maxOrder] - The maximum HEALPix order of the HiPS, i.e the HEALPix order of the most refined tile images of the HiPS.
     * @param {HiPSOptions} [options] - Options describing the survey
     * @returns {HiPS} A HiPS image object.
     */
    Aladin.prototype.createImageSurvey = function (
        id,
        name,
        url,
        cooFrame,
        maxOrder,
        options
    ) {
        let hipsOptions = { id, name, maxOrder, url, cooFrame, ...options };
        let hips = new HiPS(id, url || id, hipsOptions)

        if (this instanceof Aladin && !this.hipsCache.contains(hips.id)) {
            // Add it to the cache as soon as possible if we have a reference to the aladin object
            this.hipsCache.append(hips.id, hipsOptions)
        }

        return hips;
    };

    /**
     * @function createImageSurvey
     * @memberof Aladin
     * @static
     * @param {string} id - Mandatory unique identifier for the survey.
     * @param {string} [name] - A convinient name for the survey, optional
     * @param {string|FileList|HiPSLocalFiles} url - Can be:
     * <ul>
     * <li>An http url towards a HiPS.</li>
     * <li>A relative path to your HiPS</li>
     * <li>A special ID pointing towards a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
     * <li>A dict storing a local HiPS files. This object contains a tile file: hips[order][ipix] = File and refers to the properties file like so: hips["properties"] = File. </li>
     *     A javascript {@link FileList} pointing to the opened webkit directory is also accepted.
     * </ul>
     * @param {string} [cooFrame] - Values accepted: 'equatorial', 'icrs', 'icrsd', 'j2000', 'gal', 'galactic'
     * @param {number} [maxOrder] - The maximum HEALPix order of the HiPS, i.e the HEALPix order of the most refined tile images of the HiPS.
     * @param {HiPSOptions} [options] - Options describing the survey
     * @returns {HiPS} A HiPS image object.
     */
    Aladin.createImageSurvey = Aladin.prototype.createImageSurvey;

    /**
     * Remove a HiPS/FITS image from the list of favorites.
     * 
     * @throws A warning when the asset is currently present in the view
     *
     * @memberof Aladin
     * @param {string|HiPS|Image} urlOrHiPSOrFITS - Can be:
     * <ul>
     * <li>1. An url that refers to a HiPS</li>
     * <li>2. Or it can be a CDS identifier that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
     * <li>3. A {@link HiPS} HiPS object created from {@link A.HiPS}</li>
     * <li>4. A {@link Image} FITS image object</li>
     * </ul>
     */
    Aladin.prototype.removeHiPSFromFavorites = function (survey) {
        if (this.contains(survey)) {
            // TODO: handle this case
            console.warn(survey + ' is among the list of HiPS currently in the view.');
        }

        let id;
        if (typeof survey !== "string") {
            id = survey.name
        } else {
            id = survey
        }
        
        this.hipsCache.delete(id);
    }

    /**
     * Check whether a survey is currently in the view
     *
     * @memberof Aladin
     * @param {string|HiPS|Image} urlOrHiPSOrFITS - Can be:
     * <ul>
     * <li>1. An url that refers to a HiPS</li>
     * <li>2. Or it can be a CDS identifier that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
     * <li>3. A {@link HiPS} HiPS object</li>
     * <li>4. A {@link Image} Image object</li>
     * </ul>
     */
    Aladin.prototype.contains = function(survey) {
        this.view.contains(survey)
    }

    /**
     * Creates a FITS image object
     * @deprecated prefer use {@link A.image}
     *
     * @function createImageFITS
     * @memberof Aladin
     * @static
     * @param {string} url - The url of the fits.
     * @param {ImageOptions} [options] - Options for rendering the image
     * @param {function} [success] - A success callback
     * @param {function} [error] - A success callback
     * @returns {Image} A FITS image object.
     */
    Aladin.prototype.createImageFITS = function (
        url,
        options,
        successCallback,
        errorCallback
    ) {
        try {
            url = new URL(url);
        } catch (e) {
            // The url could be created
            url = Utils.getAbsoluteURL(url);
            url = new URL(url);
        }

        url = url.toString();

        // Do not use proxy with CORS headers until we solve that: https://github.com/MattiasBuelens/wasm-streams/issues/20
        //url = Utils.handleCORSNotSameOrigin(url).href;

        let imageOptions = {...options, successCallback, errorCallback};
        let image = new Image(url, imageOptions);

        return image;
    };

    /**
     * @deprecated prefer use {@link A.imageFITS} instead
     * Creates a FITS image object
     *
     * @function createImageFITS
     * @memberof Aladin
     * @static
     * @param {string} url - The url of the fits.
     * @param {ImageOptions} [options] - Options for rendering the image
     * @param {function} [success] - A success callback
     * @param {function} [error] - A success callback
     * @returns {Image} A FITS image object.
     */
    Aladin.createImageFITS = Aladin.prototype.createImageFITS;

    /**
     * @deprecated
     * Create a new layer from an url or CDS ID.
     * Please use {@link A.hiPS} instead for creating a new survey image
     *
     * @memberof Aladin
     * @param {string} id - Can be:
     * <ul>
     * <li>An http url towards a HiPS.</li>
     * <li>A relative path to your HiPS</li>
     * <li>A special ID pointing towards a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
     * </ul>
     * @param {HiPSOptions} [options] - Options for rendering the image
     * @param {function} [success] - A success callback
     * @param {function} [error] - A success callback
     * @returns {HiPS} A FITS image object.
     */
    Aladin.prototype.newImageSurvey = function (id, options) {
        // a wrapper on createImageSurvey that aggregates all params in an options object
        return this.createImageSurvey(
            id, 
            options && options.name,
            id,
            options && options.cooFrame,
            options && options.maxOrder,
            options
        );
    };

    /**
     * Add a new HiPS layer to the view on top of the others
     *
     * @memberof Aladin
     * @param {string|HiPS|Image} [survey="P/DSS2/color"] - Can be:
     * <ul>
     * <li>1. An url that refers to a HiPS.</li>
     * <li>2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}.</li>
     * <li>3. It can also be an {@link A.HiPS} HiPS object created from {@link A.HiPS}</li>
     * </ul>
     * By default, the {@link https://alasky.cds.unistra.fr/DSS/DSSColor/|Digital Sky Survey 2} survey will be displayed
     */
    Aladin.prototype.addNewImageLayer = function (survey = "P/DSS2/color") {
        let layerName = Utils.uuidv4();
        return this.setOverlayImageLayer(survey, layerName);
    };

    /**
     * Change the base layer of the view
     *
     * It internally calls {@link Aladin#setBaseImageLayer|Aladin.setBaseImageLayer} with the url/{@link HiPS}/{@link Image} given
     *
     * @memberof Aladin
     * @param {string|HiPS|Image} urlOrHiPSOrFITS - Can be:
     * <ul>
     * <li>1. An url that refers to a HiPS.</li>
     * <li>2. Or it can be a CDS identifier that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
     * <li>3. A {@link HiPS} HiPS object created from {@link A.HiPS}</li>
     * <li>4. A {@link Image} FITS image object</li>
     * </ul>
     */
    Aladin.prototype.setImageLayer = function (imageLayer) {
        this.setBaseImageLayer(imageLayer);
    };

    /**
     * Change the base layer of the view
     *
     * It internally calls {@link Aladin#setBaseImageLayer|Aladin.setBaseImageLayer} with the url/{@link HiPS}/{@link Image} given
     *
     * @memberof Aladin
     * @param {string|HiPS|Image} urlOrHiPSOrFITS - Can be:
     * <ul>
     * <li>1. An url that refers to a HiPS.</li>
     * <li>2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
     * <li>3. A {@link HiPS} HiPS object created from {@link A.HiPS}</li>
     * <li>4. A {@link Image} FITS image object</li>
     * </ul>
     */
    Aladin.prototype.setImageSurvey = Aladin.prototype.setImageLayer;

    // @param imageSurvey : ImageSurvey object or image survey identifier
    // @api
    // @old
    Aladin.prototype.setBackgroundColor = function (rgb) {
        this.backgroundColor = new Color(rgb);
        // Once the wasm is ready, send the color to change it

        ALEvent.AL_USE_WASM.dispatchedTo(this.aladinDiv, {
            callback: (wasm) => {
                wasm.setBackgroundColor(this.backgroundColor);
                ALEvent.BACKGROUND_COLOR_CHANGED.dispatchedTo(this.aladinDiv, {
                    color: this.backgroundColor,
                });
            },
        });
    };

    Aladin.prototype.getBackgroundColor = function () {
        return this.backgroundColor;
    };

    /**
     * Remove an image layer/overlay from the instance
     *
     * @memberof Aladin
     * @param {string|Overlay} item - the overlay object or image layer name to remove
     */
     Aladin.prototype.remove = function (item) {
        const layers = this.getStackLayers()
        let idxToDelete = layers.findIndex(l => l === item);
        if (idxToDelete >= 0) {
            this.view.removeImageLayer(item);
            return;
        }

        // must be an overlay
        this.view.removeOverlay(item)
    };

    /**
     * Remove a specific layer
     *
     * @memberof Aladin
     * @param {string} layer - The name of the layer to remove or the HiPS/Image object
     */
    Aladin.prototype.removeImageLayer = function (layer) {
        this.view.removeImageLayer(layer);
    };

    /**
     * Change the base layer of the view
     *
     * @memberof Aladin
     * @param {string|HiPS|Image} urlOrHiPSOrFITS - Can be:
     * <ul>
     * <li>1. An url that refers to a HiPS.</li>
     * <li>2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
     * <li>3. A {@link HiPS} HiPS object created from {@link A.HiPS}</li>
     * <li>4. A {@link Image} FITS image object</li>
     * </ul>
     */
    Aladin.prototype.setBaseImageLayer = function (urlOrHiPSOrFITS) {
        return this.setOverlayImageLayer(urlOrHiPSOrFITS, "base");
    };

    /**
     * Get the base image layer object
     *
     * @memberof Aladin
     * @returns {HiPS|Image} - Returns the image layer corresponding to the base layer
     */
    Aladin.prototype.getBaseImageLayer = function () {
        return this.view.getImageLayer("base");
    };

    /**
     * Add a new HiPS/FITS image layer in the view
     *
     * @memberof Aladin
     * @param {string|HiPS|Image} urlOrHiPSOrFITS - Can be:
     * <ul>
     * <li>1. An url that refers to a HiPS.</li>
     * <li>2. Or it can be a CDS ID that refers to a HiPS. One can found the list of IDs {@link https://aladin.cds.unistra.fr/hips/list| here}</li>
     * <li>3. A {@link HiPS} HiPS object created from {@link A.HiPS}</li>
     * <li>4. A {@link Image} FITS/jpeg/png image</li>
     * </ul>
     * @param {string} [layer="overlay"] - A layer name. By default 'overlay' is chosen and it is destined to be plot
     * on top the 'base' layer. If the layer is already present in the view, it will be replaced by the new HiPS/FITS image given here.
     */
    Aladin.prototype.setOverlayImageLayer = function (
        urlOrHiPSOrFITS,
        layer = "overlay"
    ) {
        let imageLayer;

        let hipsCache = this.hipsCache;
        // 1. User gives an ID
        if (typeof urlOrHiPSOrFITS === "string") {
            const idOrUrl = urlOrHiPSOrFITS;
            // many cases here
            // 1/ It has been already added to the cache
            let cachedOptions = hipsCache.get(idOrUrl)
            if (cachedOptions) {
                imageLayer = A.HiPS(idOrUrl, cachedOptions);
            } else {
                // 2/ Not in the cache, then we create the hips from this url/id and 
                // go to the case 3
                imageLayer = A.HiPS(idOrUrl);
                return this.setOverlayImageLayer(imageLayer, layer);
            }
        } else {
            // 3/ It is an image survey.
            imageLayer = urlOrHiPSOrFITS;


            if (imageLayer instanceof HiPS) {
                let cachedLayerOptions = hipsCache.get(imageLayer.id)

                if (!cachedLayerOptions) {
                    hipsCache.append(imageLayer.id, imageLayer.options)
                } else {
                    // first set the options of the cached layer to the one of the user
                    // if it is in the cache we get it from the cache
                    imageLayer = A.HiPS(imageLayer.id, cachedLayerOptions)
                }
            }
        }

        let imageLayerCopied = Object.assign(Object.create(Object.getPrototypeOf(imageLayer)), imageLayer)
        imageLayerCopied.layer = layer;

        return this.view.setOverlayImageLayer(imageLayerCopied, layer);
    };

    /**
     * Get an image layer from a layer name
     *
     * @memberof Aladin
     * @param {string} [layer="overlay"] - The name of the layer

     * @returns {HiPS|Image} - The requested image layer.
     */
    Aladin.prototype.getOverlayImageLayer = function (layer = "overlay") {
        const survey = this.view.getImageLayer(layer);
        return survey;
    };

    // @api
    Aladin.prototype.increaseZoom = function () {
        this.view.increaseZoom(0.01);
    };

    Aladin.prototype.decreaseZoom = function () {
        this.view.decreaseZoom(0.01);
    };

     /**
     * Set the view center rotation in degrees
     *
     * @memberof Aladin
     * @param {number} rotation - The center rotation in degrees. Positive angles rotates the
     * view in the counter clockwise order (or towards the east)
     */
    Aladin.prototype.setViewCenter2NorthPoleAngle = function (rotation) {
        this.view.setViewCenter2NorthPoleAngle(rotation);
    };

     /**
     * Get the view center to north pole angle in degrees. This is equivalent to getting the 3rd Euler angle
     *
     * @memberof Aladin
     * 
     * @returns {number} - Angle between the position center and the north pole
     */
    Aladin.prototype.getViewCenter2NorthPoleAngle = function () {
        return this.view.wasm.getViewCenter2NorthPoleAngle();
    };

    // @api
    // Set the current layer that is targeted
    // Rightclicking for changing the cuts is done the targeted layer
    Aladin.prototype.selectLayer = function (layer) {
        this.view.selectLayer(layer);
    };

    Aladin.prototype.getSelectedLayer = function () {
        return this.view.selectedLayer;
    };

    /**
     * Get list of overlays layers
     *
     * @memberof Aladin
     * @returns {MOC[]|Catalog[]|ProgressiveCat[]|GraphicOverlay[]} - Returns the ordered list of image layers. Items can be {@link HiPS} or {@link Image} objects.
     */
    Aladin.prototype.getOverlays = function () {
        return this.view.allOverlayLayers;
    };

    /**
     * Get list of layers
     *
     * @memberof Aladin
     * @returns {HiPS[]|Image[]} - Returns the ordered list of image layers. Items can be {@link HiPS} or {@link Image} objects.
     */
    Aladin.prototype.getStackLayers = function () {
        return this.view.overlayLayers;
    };

    Aladin.prototype.isHpxGridDisplayed = function () {
        return this.view.displayHpxGrid;
    };

    Aladin.prototype.isReticleDisplayed = function () {
        return this.reticle.isVisible();
    };

    /**
     * @deprecated
     * Please use {@link A.catalogHiPS} instead
     */
    Aladin.prototype.createProgressiveCatalog = function (
        url,
        frame,
        maxOrder,
        options
    ) {
        return new ProgressiveCat(url, frame, maxOrder, options);
    };

    /**
     * @deprecated
     * Please use {@link A.graphicOverlay} instead
     */
    Aladin.prototype.createOverlay = function (options) {
        return new GraphicOverlay(options);
    };

    // Select corresponds to rectangular selection
    Aladin.AVAILABLE_CALLBACKS = [
        "select", // deprecated, use objectsSelected instead
        "objectsSelected",

        "objectClicked",
        "objectHovered",
        "objectHoveredStop",

        "footprintClicked",
        "footprintHovered",

        "positionChanged",
        "zoomChanged",

        "click",
        "rightClickMove",
        "mouseMove",

        "fullScreenToggled",
        "cooFrameChanged",
        "resizeChanged",
        "projectionChanged",
        "layerChanged"
    ];

    /**
     * Listen aladin for specific events
     *
     * @memberof Aladin
     * @param {ListenerCallback} what - e.g. objectHovered, select, zoomChanged, positionChanged
     * @param {function} myFunction - a callback function.
     * Note: <ul>
     * <li>positionChanged and zoomChanged are throttled every 100ms.</li>
     * <li>positionChanged's callback gives an object having ra and dec keywords of the current position in ICRS frame. See the below example.</li>
     * </ul>
     * @example
// define function triggered when  a source is hovered
aladin.on('objectHovered', function(object, xyMouseCoords) {
    if (object) {
        msg = 'You hovered object ' + object.data.name + ' located at ' + object.ra + ', ' + object.dec + '; mouse coords - x: '
            + xyMouseCoords.x + ', y: ' + xyMouseCoords.y;
    }
    else {
        msg = 'No object hovered';
    }
    $('#infoDiv').html(msg);
});

aladin.on('objectHoveredStop', function(object, xyMouseCoords) {
    if (object) {
        msg = 'You stopped hove object ' + object.data.name + ' located at ' + object.ra + ', ' + object.dec + '; mouse coords - x: '
            + xyMouseCoords.x + ', y: ' + xyMouseCoords.y;
    }
    $('#infoDiv').html(msg);
});

// define function triggered when an object is clicked
var objClicked;
aladin.on('objectClicked', function(object, xyMouseCoords) {
    if (object) {
        objClicked = object;
        object.select();
        msg = 'You clicked object ' + object.data.name + ' located at ' + object.ra + ', ' + object.dec + '; mouse coords - x: '
            + xyMouseCoords.x + ', y: ' + xyMouseCoords.y;
    }
    else {
        objClicked.deselect();
        msg = 'You clicked in void';
    }
    $('#infoDiv').html(msg);
});

aladin.on("objectsSelected", (objs) => {
    console.log("objs", objs)
})

aladin.on("positionChanged", ({ra, dec}) => {
    console.log("positionChanged", ra, dec)
})

aladin.on("layerChanged", (layer, layerName, state) => {
    console.log("layerChanged", layer, layerName, state)
})
     */
    Aladin.prototype.on = function (what, myFunction) {
        if (Aladin.AVAILABLE_CALLBACKS.indexOf(what) < 0) {
            return;
        }

        this.callbacksByEventName[what] = myFunction;

        /*if (what === "positionChanged") {
            // tell the backend about that callback
            // because it needs to be called when the inertia is done
            ALEvent.AL_USE_WASM.dispatchedTo(this.aladinDiv, {
                callback: (wasm) => {
                    let myFunctionThrottled = Utils.throttle(
                        myFunction,
                        View.CALLBACKS_THROTTLE_TIME_MS
                    );

                wasm.setCallbackPositionChanged(myFunctionThrottled);
            }})
        }*/
    };

    Aladin.prototype.addListener = function (alEventName, customFn) {
        new ALEvent(alEventName).listenedBy(this.aladinDiv, customFn);
    };

    /**
     * Select specific objects in the view
     * 
     * @memberof Aladin
     * @param {?Array.<Source, Footprint, Circle, Ellipse, Polyline, Vector>} objects - If null is passed then nothing will be selected and sources already selected will be deselected
     */
    Aladin.prototype.selectObjects = function (objects) {
        if (!objects) {
            this.view.unselectObjects();
            return;
        }

        let objListPerCatalog = {};

        for (let o of objects) {
            let cat = o.getCatalog();
            if (cat) {
                let objList = objListPerCatalog[cat.name];
                if (!objList) {
                    objList = [];
                } else {
                    objList.push(o);
                }
            }
        }
        objects = Object.values(objListPerCatalog);

        this.view.selectObjects(objects);
    };

    /**
     * Enters selection mode
     *
     * @memberof Aladin
     * @param {string} [mode='rect'] - The mode of selection, can be either, 'rect', 'poly', or 'circle'
     * @param {function} [callback] - A function called once the selection has been done
     * The callback accepts one parameter depending of the mode used: <br/>
     * - If mode='circle' that parameter is of type {@link CircleSelection} <br/>
     * - If mode='rect' that parameter is of type {@link RectSelection} <br/>
     * - If mode='poly' that parameter is of type {@link PolygonSelection}
     *
     * @example
     * // Creates and add a MOC from the user polygonal selection
     * aladin.select('poly', p => {
     *    try {
     *        let ra = []
     *        let dec = []
     *        for (const v of p.vertices) {
     *            let [lon, lat] = aladin.pix2world(v.x, v.y);
     *            ra.push(lon)
     *            dec.push(lat)
     *        }
     *
     *        let moc = A.MOCFromPolygon(
     *            {ra, dec},
     *            {name: 'poly', lineWidth: 3.0, color: 'pink'},
     *        );
     *        aladin.addMOC(moc)
     *    } catch(_) {
     *        alert('Selection covers a region out of the projection definition domain.');
     *    }
     *})
     */
    Aladin.prototype.select = async function (mode = "rect", callback) {
        await this.reticle.loaded;

        this.fire("selectstart", { mode, callback });
    };

    Aladin.prototype.fire = function (what, params) {
        if (what === "selectstart") {
            const { mode, callback } = params;
            this.view.startSelection(mode, callback);
        } else if (what === "simbad") {
            this.view.setMode(View.TOOL_SIMBAD_POINTER);
        } else if (what === "default") {
            this.view.setMode(View.PAN);
        }
    };

    Aladin.prototype.hideBoxes = function () {
        if (this.boxes) {
            for (var k = 0; k < this.boxes.length; k++) {
                if (typeof this.boxes[k].hide === "function") {
                    this.boxes[k].hide();
                }
            }
        }
    };

    // TODO : LayerBox (or Stack?) must be extracted as a separate object
    Aladin.prototype.showLayerBox = function () {
        this.stack.showImageLayerBox();
    };

    /**
     * Sets the coordinate grid options for the Aladin Lite view.
     *
     * This method allows you to customize the appearance of the coordinate grid in the Aladin Lite view.
     *
     * @memberof Aladin
     * @param {Object} options - Options to customize the coordinate grid.
     * @param {string} [options.color] - The color of the coordinate grid.
     * @param {number} [options.opacity] - The opacity of the coordinate grid (value between 0 and 1).
     * @param {number} [options.labelSize] - The size of the coordinate grid labels in pixels.
     * @param {number} [options.thickness] - The thickness of the coordinate grid lines.
     * @param {boolean} [options.enabled] - If true, the coordinate grid is enabled; otherwise, it is disabled.
     *
     * @example
     * // Set the coordinate grid color to red
     * aladin.setCooGrid({ color: 'red' });
     *
     * // Enable the coordinate grid
     * aladin.setCooGrid({ enabled: true });
     */
    Aladin.prototype.setCooGrid = function (options) {
        if (options.color) {
            // 1. the user has maybe given some
            options.color = new Color(options.color);
            // 3. convert from 0-255 to 0-1
            options.color.r /= 255;
            options.color.g /= 255;
            options.color.b /= 255;
        }

        this.view.setGridOptions(options);
    };

    Aladin.prototype.getGridOptions = function () {
        return this.view.getGridOptions();
    };

    Aladin.prototype.showCooGrid = function () {
        this.setCooGrid({ enabled: true });
    };

    Aladin.prototype.hideCooGrid = function () {
        this.setCooGrid({ enabled: false });
    };

    Aladin.prototype.layerByName = function (name) {
        var c = this.view.allOverlayLayers;
        for (var k = 0; k < c.length; k++) {
            if (name == c[k].name) {
                return c[k];
            }
        }
        return null;
    };

    // TODO : integrate somehow into API ?
    Aladin.prototype.exportAsPNG = function (downloadFile = false) {
        (async () => {

            const url = await this.getViewDataURL();

            if (downloadFile) {
                Utils.download(url, "screenshot");
            } else {
                // open a new window
                var w = window.open();
                w.document.write(
                    '<img src="' + url + '" width="' + this.view.width + 'px">'
                );
                w.document.title = "Aladin Lite snapshot";
            }
        })();
    };

    /**
     * Return the current view as a png data URL (base64-formatted string)
     *
     * @memberof Aladin
     *
     * @param {Object} [options] Object with attributs, options are:
     * @param {Object} [options.format] 'image/png' or 'image/jpeg'
     * @param {Object} options.width Width in pixels of the image to output
     * @param {Object} options.height Height in pixels of the image to output
     * @param {Object} [options.logo=true] Boolean to display the Aladin Lite logo
     * @returns {Promise<string>} The image as a png data URL
     */
    Aladin.prototype.getViewDataURL = async function (options) {
        var options = options || {};
        // support for old API signature
        if (typeof options !== "object") {
            var imgFormat = options;
            options = { format: imgFormat };
        }

        const canvasDataURL = await this.view.getCanvasDataURL(
            options.format,
            options.width,
            options.height,
            options.logo
        );

        return canvasDataURL;
    };

    /**
     * Return the current view as a png ArrayBuffer
     *
     * @memberof Aladin
     *
     * @param {boolean} withLogo Display or not the Aladin Lite logo
     * @returns {Promise<ArrayBuffer>} The image as a png ArrayBuffer
     */
    Aladin.prototype.getViewArrayBuffer = async function (withLogo) {
        return await this.view.getCanvasArrayBuffer("image/png", null, null, withLogo);
    }

    /**
     * Return the current view as a png Blob
     *
     * @memberof Aladin
     *
     * @param {string} dataType The type of data to return. Can be 'url', 'arraybuffer' or 'blob'
     * @param {string} [imgType='image/png'] The type of image to return. Can be 'image/png', 'image/jpeg' or 'image/webp'
     * @param {boolean} [withLogo=true] Display or not the Aladin Lite logo
     * @returns {Promise<any>}
     */
    Aladin.prototype.getViewData = async function (dataType, imgType="image/png", withLogo=true){
        switch (dataType) {
            case "url":
                return await this.view.getCanvasDataURL(imgType, null, null, withLogo);
            case "arraybuffer":
                return await this.view.getCanvasArrayBuffer(imgType, null, null, withLogo);
            case "blob":
                return await this.view.getCanvasBlob(imgType, null, null, withLogo);
            default:
                throw new Error("Unknown data type: " + dataType);
        }
    }

    /**
     * Return the current view WCS as a key-value dictionary
     * Can be useful in coordination with getViewDataURL
     *
     * @memberof Aladin
     * @returns {Object} - A JS object describing the WCS of the view.
     */
    Aladin.prototype.getViewWCS = function () {
        // get general view properties
        const center = this.wasm.getCenter();
        const fov = this.getFov();
        const width = this.view.width;
        const height = this.view.height;

        // get values common for all
        let cdelt1 = -fov[0] / width;
        const cdelt2 = fov[1] / height;
        const projName = this.getProjectionName();

        if (projName == "FEYE")
            return "Fish eye projection is not supported by WCS standards.";

        // reversed longitude case
        if (this.getBaseImageLayer().longitudeReversed) {
            cdelt1 = -cdelt1;
        }

        // solar system object dict from planetary fits standard
        // https://agupubs.onlinelibrary.wiley.com/doi/10.1029/2018EA000388
        const solarSystemObjects = {
            earth: "EA",
            moon: "SE",
            mercury: "ME",
            venus: "VE",
            mars: "MA",
            jupiter: "JU",
            saturn: "SA",
            uranus: "UR",
            neptune: "NE",
            // satellites other than the Moon
            satellite: "ST", // not findable in the hips properties?
        };

        // we define a generic LON LAT keyword for unknown body types
        let cooType1 = "LON--";
        let cooType2 = "LAT--";

        // just in case it would be equatorial
        let radesys;

        if (this.getBaseImageLayer().isPlanetaryBody()) {
            const body = this.getBaseImageLayer().hipsBody;
            if (body in solarSystemObjects) {
                cooType1 = `${solarSystemObjects[body]}LN-`;
                cooType2 = `${solarSystemObjects[body]}LT-`;
            }
        } else {
            switch (this.getFrame()) {
                case "ICRS":
                case "ICRSd":
                    cooType1 = "RA---";
                    cooType2 = "DEC--";
                    radesys = "ICRS    ";
                    break;
                case "GAL":
                    cooType1 = "GLON-";
                    cooType2 = "GLAT-";
            }
        }

        const WCS = {
            NAXIS: 2,
            NAXIS1: width,
            NAXIS2: height,
            CRPIX1: width / 2 + 0.5,
            CRPIX2: height / 2 + 0.5,
            CRVAL1: center[0],
            CRVAL2: center[1],
            CTYPE1: cooType1 + projName,
            CTYPE2: cooType2 + projName,
            CUNIT1: "deg     ",
            CUNIT2: "deg     ",
            CDELT1: cdelt1,
            CDELT2: cdelt2,
        };

        // handle the case of equatorial coordinates that need
        // the radecsys keyword
        if (radesys == "ICRS    ") WCS.RADESYS = radesys;

        const isProjZenithal = ['TAN', 'SIN', 'STG', 'ZEA'].some((p) => p === projName)
        if (isProjZenithal) {
            // zenithal projections
            // express the 3rd euler angle for zenithal projection
            let thirdEulerAngle = this.getViewCenter2NorthPoleAngle();
            WCS.LONPOLE = 180 - thirdEulerAngle
        } else {
            // cylindrical or pseudo-cylindrical projections
            if (WCS.CRVAL2 === 0) {
                // ref point on the equator not handled (yet)
                console.warn('TODO: 3rd euler rotation is not handled for ref point located at delta_0 = 0')
            } else {
                // ref point not on the equator
                const npLonlat = this.view.wasm.getNorthPoleCelestialPosition();
                let dLon = WCS.CRVAL1 - npLonlat[0];

                // dlon angle must lie between -PI and PI
                // For dlon angle between -PI;-PI/2 or PI/2;PI one must invert LATPOLE
                if (this.getViewCenter2NorthPoleAngle() < -90 || this.getViewCenter2NorthPoleAngle() > 90) {
                    // so that the south pole becomes upward to the ref point
                    WCS.LATPOLE = -90
                }

                const toRad = Math.PI / 180
                const toDeg = 1.0 / toRad;

                // Reverse the Eq 9 from the WCS II paper from Mark Calabretta to obtain LONPOLE
                // function of CRVAL2 and native coordinates of the fiducial ref point, i.e. (phi_0, theta_0) = (0, 0)
                // for cylindrical projections 
                WCS.LONPOLE = Math.asin(Math.sin(dLon * toRad) * Math.cos(WCS.CRVAL2 * toRad)) * toDeg;

                if (WCS.CRVAL2 < 0) {
                    // ref point is located in the south hemisphere
                    WCS.LONPOLE = -180 - WCS.LONPOLE;
                }
            }
        }

        return WCS;
    };

    /**
     * Restrict the FoV range between a min and a max value
     *
     * @memberof Aladin
     * @param {number} minFoV - in degrees when zoom in at max. If undefined, the zooming in is not limited
     * @param {number} maxFoV - in degrees when zoom out at max. If undefined, the zooming out is not limited
     *
     * @example
     * let aladin = A.aladin('#aladin-lite-div');
     * aladin.setFoVRange(30, 60);
     */
    Aladin.prototype.setFoVRange = function (minFoV, maxFoV) {
        this.view.setFoVRange(minFoV, maxFoV);
    };

    Aladin.prototype.setFOVRange = Aladin.prototype.setFoVRange;

    /**
     * Transform pixel coordinates to world coordinates.
     *
     * The origin (0,0) of pixel coordinates is at the top-left corner of the Aladin Lite view.
     *
     * @memberof Aladin
     * @param {number} x - The x-coordinate in pixel coordinates.
     * @param {number} y - The y-coordinate in pixel coordinates.
     * @param {CooFrame} [frame] - The frame in which we want to retrieve the coordinates.
     * If not given, the frame chosen is the one from the view
     *
     * @returns {number[]} - An array representing the [Right Ascension, Declination] coordinates in degrees in the `frame`.
     * If not specified, returns the coo in the frame of the current view.
     *
     * @throws {Error} Throws an error if an issue occurs during the transformation.
     */
    Aladin.prototype.pix2world = function (x, y, frame) {
        if (frame) {
            frame = CooFrameEnum.fromString(frame, CooFrameEnum.J2000);
            if (frame.label == CooFrameEnum.SYSTEMS.GAL) {
                frame = Aladin.wasmLibs.core.CooSystem.GAL;
            }
            else {
                frame = Aladin.wasmLibs.core.CooSystem.ICRS;
            }
        }

        let lonlat = this.view.wasm.pix2world(x, y, frame);

        let [lon, lat] = lonlat;

        if (lon < 0) {
            return [lon + 360.0, lat];
        }

        return [lon, lat];
    };

    /**
     * Transform world coordinates to pixel coordinates in the view.
     *
     * @memberof Aladin
     * @param {number} lon - Londitude coordinate in degrees.
     * @param {number} lat - Latitude coordinate in degrees.
     * @param {CooFrame} [frame] - If not specified, the frame used is ICRS

     * @returns {number[]} - An array representing the [x, y] coordinates in pixel coordinates in the view.
     *
     * @throws {Error} Throws an error if an issue occurs during the transformation.
     */
    Aladin.prototype.world2pix = function (lon, lat, frame) {
        if (frame) {
            if (frame instanceof string) {
                frame = CooFrameEnum.fromString(frame, CooFrameEnum.J2000);
            }
    
            if (frame.label == CooFrameEnum.SYSTEMS.GAL) {
                frame = Aladin.wasmLibs.core.CooSystem.GAL;
            }
            else {
                frame = Aladin.wasmLibs.core.CooSystem.ICRS;
            }
        }

        return this.view.wasm.world2pix(lon, lat, frame);
    };

    /**
     * Get the angular distance in degrees between two locations
     *
     * @memberof Aladin
     * @param {number} x1 - The x-coordinate of the first pixel coordinates.
     * @param {number} y1 - The y-coordinate of the first pixel coordinates.
     * @param {number} x2 - The x-coordinate of the second pixel coordinates.
     * @param {number} y2 - The y-coordinate of the second pixel coordinates.
     * @param {CooFrame} [frame] - The frame in which we want to retrieve the coordinates.
     * If not given, the frame chosen is the one from the view
     *
     * @returns {number} - The angular distance between the two pixel coordinates in degrees
     *
     * @throws {Error} Throws an error if an issue occurs during the transformation.
     */
    Aladin.prototype.angularDist = function (x1, y1, x2, y2, frame) {
        const [ra1, dec1] = this.pix2world(x1, y1, frame);
        const [ra2, dec2] = this.pix2world(x2, y2, frame);

        return this.wasm.angularDist(ra1, dec1, ra2, dec2);
    };

    /**
     * Gets a set of points along the current Field of View (FoV) corners.
     *
     * @memberof Aladin
     * @param {number} [nbSteps=1] - The number of points to return along each side (the total number of points returned is 4 * nbSteps).
     * @param {CooFrame} [frame] - The frame in which the coo will be given. Default to the view frame.
     *
     * @returns {number[][]} - A set of positions along the current FoV with the following format: [[ra1, dec1], [ra2, dec2], ..., [ra_n, dec_n]].
     *                         The positions will be given in degrees
     *
     * @throws {Error} Throws an error if an issue occurs during the transformation.
     *
     */
    Aladin.prototype.getFoVCorners = function (nbSteps, frame) {
        // default value: 1
        if (!nbSteps || nbSteps < 1) {
            nbSteps = 1;
        }

        var points = [];
        var x1, y1, x2, y2;
        for (var k = 0; k < 4; k++) {
            x1 = k == 0 || k == 3 ? 0 : this.view.width - 1;
            y1 = k < 2 ? 0 : this.view.height - 1;
            x2 = k < 2 ? this.view.width - 1 : 0;
            y2 = k == 1 || k == 2 ? this.view.height - 1 : 0;

            for (var step = 0; step < nbSteps; step++) {
                let radec = this.pix2world(
                    x1 + (step / nbSteps) * (x2 - x1),
                    y1 + (step / nbSteps) * (y2 - y1),
                    frame
                );
                points.push(radec);
            }
        }

        return points;
    };

    /**
     * Gets the current Field of View (FoV) size in degrees as a 2-element array.
     *
     * @memberof Aladin
     * @returns {number[]} - A 2-element array representing the current FoV size in degrees. The first element is the FoV width,
     *                       and the second element is the FoV height.
     */
    Aladin.prototype.getFov = function () {
        // can go up to 1000 deg
        var fovX = this.view.fov;
        var s = this.getSize();

        // constrain to the projection definition domain
        fovX = Math.min(fovX, this.view.projection.fov);
        var fovY = (s[1] / s[0]) * fovX;

        fovY = Math.min(fovY, 180);
        // TODO : take into account AITOFF projection where fov can be larger than 180

        return [fovX, fovY];
    };

    Aladin.prototype.getFoV = Aladin.prototype.getFov;

    /**
     * Returns the size in pixels for the Aladin view
     *
     * @memberof Aladin
     * @returns {number[]} - A 2-element array representing the current Aladin view size in pixels. The first element is the width,
     *                       and the second element is the height.
     */
    Aladin.prototype.getSize = function () {
        return [this.view.width, this.view.height];
    };

    /**
     * Returns the HTML div element
     *
     * @memberof Aladin
     * @return {HTMLElement} - The aladin lite div HTML element
     */
    Aladin.prototype.getParentDiv = function () {
        return this.aladinDiv;
    };

    // @API
    /*
     * return a Box GUI element to insert content
     */
    /*Aladin.prototype.box = function (options) {
        var box = new Box(options);
        box.$parentDiv.appendTo(this.aladinDiv);

        return box;
    };*/

    // @API
    /*
     * show popup at ra, dec position with given title and content
     *
     * If circleRadius, the corresponding circle will also be plotted
     */
    Aladin.prototype.showPopup = function (
        ra,
        dec,
        title,
        content,
        circleRadius
    ) {
        this.view.catalogForPopup.removeAll();
        this.view.overlayForPopup.removeAll();

        let marker;
        if (circleRadius !== undefined) {
            this.view.overlayForPopup.add(
                A.circle(ra, dec, circleRadius, {
                    fillColor: "rgba(255, 0, 0, 0.2)",
                })
            );
            marker = A.marker(ra, dec, {
                popupTitle: title,
                popupDesc: content,
                useMarkerDefaultIcon: true,
            });
        } else {
            marker = A.marker(ra, dec, {
                popupTitle: title,
                popupDesc: content,
                useMarkerDefaultIcon: false,
            });
        }

        this.view.catalogForPopup.addSources(marker);

        this.view.overlayForPopup.show();
        this.view.catalogForPopup.show();

        this.popup.setTitle(title);
        this.popup.setText(content);

        this.popup.setSource(marker);
        this.popup.show();
    };

    // @API
    /*
     * hide popup
     */
    Aladin.prototype.hidePopup = function () {
        this.popup.hide();
    };

    // @API
    /*
     * return a URL allowing to share the current view
     */
    Aladin.prototype.getShareURL = function () {
        var radec = this.getRaDec();
        var coo = new Coo();
        coo.prec = 7;
        coo.lon = radec[0];
        coo.lat = radec[1];

        return (
            Aladin.URL_PREVIEWER +
            "?target=" +
            encodeURIComponent(coo.format("s")) +
            "&fov=" +
            this.getFov()[0].toFixed(2) +
            "&survey=" +
            encodeURIComponent(
                this.getBaseImageLayer().id || this.getBaseImageLayer().rootUrl
            )
        );
    };

    // @API
    /*
     * return, as a string, the HTML embed code
     */
    Aladin.prototype.getEmbedCode = function () {
        var radec = this.getRaDec();
        var coo = new Coo();
        coo.prec = 7;
        coo.lon = radec[0];
        coo.lat = radec[1];

        var survey = this.getBaseImageLayer().url;
        var fov = this.getFov()[0];
        let s = "";
        const NL = "\n";
        s +=
            '<div id="aladin-lite-div" style="width:400px;height:400px;"></div>' +
            NL;
        s +=
            '<script src="https://aladin.cds.unistra.fr/AladinLite/api/v3/latest/aladin.js" charset="utf-8"></script>' +
            NL;
        s += "<script>" + NL;
        s +=
            "let aladin;" +
            NL +
            "A.init.then(() => {" +
            NL +
            "   aladin = A.aladin('#aladin-lite-div', {survey: '" +
            survey +
            "', fov: " +
            fov.toFixed(2) +
            ', target: "' +
            coo.format("s") +
            '"});' +
            NL +
            "});" +
            NL;
        s += "</script>";

        return s;
    };

    /**
     * Display a FITS image in the Aladin Lite.
     *
     * @memberof Aladin
     * @param {string} url - The URL of the FITS image.
     * @param {ImageOptions} [options] - Options to customize the display
     * @param {Function} [successCallback=<center the view on the FITS file>] - The callback function to be executed on a successful display.
     *      The callback gives the ra, dec, and fov of the image; By default, it centers the view on the FITS file loaded.
     * @param {Function} [errorCallback] - The callback function to be executed if an error occurs during display.
     * @param {string} [layer="base"] - The name of the layer. If not specified, it will be replace the base layer.
     *
     * @example
aladin.displayFITS(
    'https://fits.gsfc.nasa.gov/samples/FOCx38i0101t_c0f.fits', // url of the fits file
    {
        minCut: 5000,
        maxCut: 17000,
        colormap: 'viridis'
    },
    (ra, dec, fov, image) => {
        // ra, dec and fov are centered around the fits image
        image.setColormap("magma", {stretch: "asinh"});

        aladin.gotoRaDec(ra, dec);
        aladin.setFoV(fov);
    },
);
     */
    Aladin.prototype.displayFITS = function (
        url,
        options,
        successCallback,
        errorCallback,
        layer = "base"
    ) {
        successCallback =
            successCallback ||
            ((ra, dec, fov, _) => {
                this.gotoRaDec(ra, dec);
                this.setFoV(fov);
            });
        const image = this.createImageFITS(
            url,
            options,
            successCallback,
            errorCallback
        );
        return this.setOverlayImageLayer(image, layer);
    };

    /**
     * Display a JPEG image in the Aladin Lite view.
     *
     * @memberof Aladin
     * @param {string} url - The URL of the JPEG image.
     * @param {Object} [options] - Options to customize the display. Can include the following properties:
     * @param {string} [options.label="JPG/PNG image"]  - A label for the displayed image.
     * @param {number} [options.order] - The desired HEALPix order format.
     * @param {boolean} [options.nocache] - True if you want to disable the cache
     * @param {number} [options.transparency=1.0] - Opacity of the image rendered in aladin lite. Between 0 and 1.
     * @param {Function} [successCallback] - The callback function to be executed on a successful display.
     *      The callback gives the ra, dec, and fov of the image;
     * @param {Function} [errorCallback] - The callback function to be executed if an error occurs during display.
     * @param {string} [layer="overlay"] - The name of the layer. If not specified, it will add a new overlay layer on top of the base.
     *
     * @example
     * aladin.displayJPG(
     *  // the JPG to transform to HiPS
     *   'https://noirlab.edu/public/media/archives/images/large/noirlab1912a.jpg',
     *   {
     *       transparency: 0.6,
     *       label: 'NOIRLab image'
     *   },
     *   (ra, dec, fov) => {
     *      // your code here
     *   })
     *);
     */
    Aladin.prototype.displayJPG = function (
        url,
        options,
        successCallback,
        errorCallback,
        layer = "overlay"
    ) {
        options = options || {};
        options.color = true;
        options.label = options.label || "JPG/PNG image";
        options.outputFormat = "png";

        options = options || {};

        var data = { url };
        if (options.color) {
            data.color = true;
        }
        if (options.outputFormat) {
            data.format = options.outputFormat;
        }
        if (options.order) {
            data.order = options.order;
        }
        if (options.nocache) {
            data.nocache = options.nocache;
        }
        let self = this;

        const request = (url, params = {}, method = "GET") => {
            let options = {
                method,
            };
            if ("GET" === method) {
                url += "?" + new URLSearchParams(params).toString();
            } else {
                options.body = JSON.stringify(params);
            }

            return fetch(url, options).then((response) => response.json());
        };
        const get = (url, params) => request(url, params, "GET");

        get("https://alasky.unistra.fr/cgi/fits2HiPS", data).then(
            async (response) => {
                if (response.status != "success") {
                    console.error("An error occured: " + response.message);
                    if (errorCallback) {
                        errorCallback(response.message);
                    }
                    return;
                }
                var label = options.label;
                var meta = response.data.meta;

                const survey = self.createImageSurvey(
                    response.data.url,
                    label,
                    response.data.url
                );
                self.setOverlayImageLayer(survey, layer);

                var transparency = (options && options.transparency) || 1.0;
                survey.setOpacity(transparency);

                var executeDefaultSuccessAction = true;
                if (successCallback) {
                    executeDefaultSuccessAction = successCallback(
                        meta.ra,
                        meta.dec,
                        meta.fov
                    );
                }
                if (executeDefaultSuccessAction === true) {
                    self.wasm.setCenter(meta.ra, meta.dec);
                    self.setFoV(meta.fov);
                }

                // TODO! set an image survey once the already loaded surveys
                // are READY! Otherwise it can lead to some congestion and avoid
                // downloading the base tiles of the other surveys loading!
                // This has to be fixed in the backend but a fast fix is just to wait
                // before setting a new image survey
            }
        );
    };

    Aladin.prototype.displayPNG = Aladin.prototype.displayJPG;

    /**
     * Add a custom colormap from a list of colors
     *
     * @memberof Aladin
     * 
     * @returns - The list of all the colormap labels
     */
    Aladin.prototype.getListOfColormaps = function() {
        return this.view.wasm.getAvailableColormapList();
    };

    /**
     * Add a custom colormap from a list of colors
     *
     * @memberof Aladin
     * @param {string} label - The label of the colormap
     * @param {string[]} colors - A list string colors
     * 
     * @example
     * 
     * aladin.addColormap('mycmap', ["lightblue", "red", "violet", "#ff00aaff"])
     */
    Aladin.prototype.addColormap = function(label, colors) {
        colors = colors.map((label) => {
            return new Color(label).toHex() + 'ff';
        });

        this.view.wasm.createCustomColormap(label, colors);

        ALEvent.UPDATE_CMAP_LIST.dispatchedTo(this.aladinDiv, {
            cmaps: this.getListOfColormaps()
        });
    };

    /*
    Aladin.prototype.setReduceDeformations = function (reduce) {
        this.reduceDeformations = reduce;
        this.view.requestRedraw();
    }
    */

    return Aladin;
})();