HiPS.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 HiPS
 *
 * Authors: Thomas Boch & Matthieu Baumann [CDS]
 *
 *****************************************************************************/
import { ALEvent } from "./events/ALEvent.js";
import { ColorCfg } from "./ColorCfg.js";
import { HiPSProperties } from "./HiPSProperties.js";
import { Aladin } from "./Aladin.js"; 
let PropertyParser = {};
// Utilitary functions for parsing the properties and giving default values
/// Mandatory tileSize property
PropertyParser.tileSize = function (properties) {
    let tileSize =
        (properties &&
            properties.hips_tile_width &&
            +properties.hips_tile_width) ||
        512;

    // Check if the tile width size is a power of 2
    if (tileSize & (tileSize - 1 !== 0)) {
        tileSize = 512;
    }

    return tileSize;
};

/// Mandatory frame property
PropertyParser.cooFrame = function (properties) {
    let cooFrame =
        (properties && properties.hips_body && "ICRSd") ||
        (properties && properties.hips_frame) ||
        "ICRS";
    return cooFrame;
};

/// Mandatory maxOrder property
PropertyParser.maxOrder = function (properties) {
    let maxOrder =
        properties && properties.hips_order && +properties.hips_order;
    return maxOrder;
};

/// Mandatory minOrder property
PropertyParser.minOrder = function (properties) {
    const minOrder =
        (properties &&
            properties.hips_order_min &&
            +properties.hips_order_min) ||
        0;
    return minOrder;
};

PropertyParser.formats = function (properties) {
    let formats = (properties && properties.hips_tile_format) || "jpeg";

    formats = formats.split(" ").map((fmt) => fmt.toLowerCase());

    return formats;
};

PropertyParser.initialFov = function (properties) {
    let initialFov =
        properties &&
        properties.hips_initial_fov &&
        +properties.hips_initial_fov;

    if (initialFov && initialFov < 0.00002777777) {
        initialFov = 360;
    }

    return initialFov;
};

PropertyParser.skyFraction = function (properties) {
    const skyFraction =
        (properties &&
            properties.moc_sky_fraction &&
            +properties.moc_sky_fraction) ||
        0.0;
    return skyFraction;
};

PropertyParser.cutouts = function (properties) {
    let cuts =
        properties &&
        properties.hips_pixel_cut &&
        properties.hips_pixel_cut.split(" ");

    const minCutout = cuts && parseFloat(cuts[0]);
    const maxCutout = cuts && parseFloat(cuts[1]);

    return [minCutout, maxCutout];
};

PropertyParser.bitpix = function (properties) {
    const bitpix =
        properties &&
        properties.hips_pixel_bitpix &&
        +properties.hips_pixel_bitpix;
    return bitpix;
};

PropertyParser.isPlanetaryBody = function (properties) {
    return properties && properties.hips_body !== undefined;
};

/**
 * HiPS options
 * @typedef {Object} HiPSOptions
 * @property {string} [name] - The name of the survey to be displayed in the UI
 * @property {Function} [successCallback] - A callback executed when the HiPS has been loaded
 * @property {Function} [errorCallback] - A callback executed when the HiPS could not be loaded
 * @property {string} [imgFormat] - Formats accepted 'webp', 'png', 'jpeg' or 'fits'. Will raise an error if the HiPS does not contain tiles in this format
 * @property {CooFrame} [cooFrame="J2000"] - Coordinate frame of the survey tiles
 * @property {number} [maxOrder] - The maximum HEALPix order of the HiPS, i.e the HEALPix order of the most refined tile images of the HiPS.
 * @property {number} [numBitsPerPixel] - Useful if you want to display the FITS tiles of a HiPS. It specifies the number of bits per pixel. Possible values are:
 * -64: double, -32: float, 8: unsigned byte, 16: short, 32: integer 32 bits, 64: integer 64 bits
 * @property {number} [tileSize] - The width of the HEALPix tile images. Mostly 512 pixels but can be 256, 128, 64, 32
 * @property {number} [minOrder] - If not given, retrieved from the properties of the survey.
 * @property {boolean} [longitudeReversed=false] - Set it to True for planetary survey visualization 
 * @property {number} [opacity=1.0] - Opacity of the survey or image (value between 0 and 1).
 * @property {string} [colormap="native"] - The colormap configuration for the survey or image.
 * @property {string} [stretch="linear"] - The stretch configuration for the survey or image.
 * @property {boolean} [reversed=false] - If true, the colormap is reversed; otherwise, it is not reversed.
 * @property {number} [minCut] - The minimum cut value for the color configuration. If not given, 0.0 for JPEG/PNG surveys, the value of the property file for FITS surveys
 * @property {number} [maxCut] - The maximum cut value for the color configuration. If not given, 1.0 for JPEG/PNG surveys, the value of the property file for FITS surveys
 * @property {boolean} [additive=false] - If true, additive blending is applied; otherwise, it is not applied.
 * @property {number} [gamma=1.0] - The gamma correction value for the color configuration.
 * @property {number} [saturation=0.0] - The saturation value for the color configuration.
 * @property {number} [brightness=0.0] - The brightness value for the color configuration.
 * @property {number} [contrast=0.0] - The contrast value for the color configuration.
 */

/**
 * JS {@link https://developer.mozilla.org/fr/docs/Web/API/FileList| FileList} API type
 * 
 * @typedef {Object} FileList
 */

/**
 * Tiles are accessed like so: HIPSLocalFiles[norder][ipix] = {@link File};<br/>
 * The properties file is accessed with: HIPSLocalFiles["properties"]
 * @typedef {Object} HiPSLocalFiles
 * @property {File} properties - The local properties file of the HiPS
 */

 
export let HiPS = (function () {
    /**
     * The object describing an image survey
     *
     * @class
     * @constructs HiPS
     *
     * @param {string} id - Mandatory unique identifier for the layer. Can be an arbitrary name
     * @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 {HiPSOptions} [options] - The option for the survey
     *
     * @description Giving a CDS ID will do a query to the MOCServer first to retrieve metadata. Then it will also check for the presence of faster HiPS nodes to choose a faster url to query to tiles from.
     */
    function HiPS(id, location, options) {
        this.added = false;
        // Unique identifier for a survey
        this.id = id;

        this.options = options;
        this.name = (options && options.name) || id;
        this.startUrl = options.startUrl;

        this.slice = 0;

        if (location instanceof FileList) {
            let localFiles = {};
            for (var file of location) {
                let path = file.webkitRelativePath;
                if (path.includes("Norder") && path.includes("Npix")) {
                    const order = +path.substring(path.indexOf("Norder") + 6).split("/")[0];
                    if (!localFiles[order]) {
                        localFiles[order] = {}
                    }

                    let tile = path.substring(path.indexOf("Npix") + 4).split(".");
                    const ipix = +tile[0];
                    const fmt = tile[1];

                    if (!localFiles[order][ipix]) {
                        localFiles[order][ipix] = {}
                    }

                    localFiles[order][ipix][fmt] = file;
                }

                if (path.includes("properties")) {
                    localFiles['properties'] = file;
                }

                if (path.includes("Moc")) {
                    localFiles['moc'] = file;
                }
            }

            this.localFiles = localFiles;
        } else if (location instanceof Object) {
            this.localFiles = location;
        }

        this.url = location;

        this.maxOrder = options.maxOrder;
        this.minOrder = options.minOrder || 0;
        this.cooFrame = options.cooFrame;
        this.tileSize = options.tileSize;
        this.skyFraction = options.skyFraction;
        this.longitudeReversed =
            options.longitudeReversed === undefined
                ? false
                : options.longitudeReversed;
        this.imgFormat = options.imgFormat;
        this.formats = options.formats;
        this.defaultFitsMinCut = options.defaultFitsMinCut;
        this.defaultFitsMaxCut = options.defaultFitsMaxCut;
        this.numBitsPerPixel = options.numBitsPerPixel;
        this.creatorDid = options.creatorDid;
        this.errorCallback = options.errorCallback;
        this.successCallback = options.successCallback;

        this.colorCfg = new ColorCfg(options);
    };

    HiPS.prototype._fetchFasterUrlFromProperties = function(properties) {
        let self = this;

        HiPSProperties.getFasterMirrorUrl(properties)
            .then((url) => {
                if (self.url !== url) {
                    console.info(
                        "Change url of ",
                        self.id,
                        " to ",
                        url
                    );

                    self.url = url;

                    // If added to the backend, then we need to tell it the url has changed
                    if (self.added) {
                        self.view.wasm.setHiPSUrl(
                            self.creatorDid,
                            url
                        );
                    }
                }
            })
            .catch((e) => {
                console.error(self);
                console.error(e);
            });
    }

    HiPS.prototype._parseProperties = function(properties) {
        let self = this;
        self.creatorDid = properties.creator_did || self.creatorDid;

        // Cube depth
        self.cubeDepth = properties && properties.hips_cube_depth && +properties.hips_cube_depth;
        self.cubeFirstFrame = properties && properties.hips_cube_firstframe && +properties.hips_cube_firstframe;

        // Max order
        self.maxOrder =
            PropertyParser.maxOrder(properties) || self.maxOrder;

        // Tile size
        self.tileSize =
            PropertyParser.tileSize(properties) || self.tileSize;

        // Tile formats
        self.formats =
            PropertyParser.formats(properties) || self.formats;

        // min order
        self.minOrder =
            PropertyParser.minOrder(properties) || self.minOrder;

        // Frame
        self.cooFrame =
            PropertyParser.cooFrame(properties) || self.cooFrame;

        // sky fraction
        self.skyFraction = PropertyParser.skyFraction(properties);

        // Initial fov/ra/dec
        self.initialFov = PropertyParser.initialFov(properties);
        self.initialRa =
            properties &&
            properties.hips_initial_ra &&
            +properties.hips_initial_ra;
        self.initialDec =
            properties &&
            properties.hips_initial_dec &&
            +properties.hips_initial_dec;

        // Cutouts
        const cutoutFromProperties = PropertyParser.cutouts(properties);
        self.defaultFitsMinCut = cutoutFromProperties[0];
        self.defaultFitsMaxCut = cutoutFromProperties[1];

        // Bitpix
        self.numBitsPerPixel =
            PropertyParser.bitpix(properties) || self.numBitsPerPixel;

        // HiPS body
        if (properties.hips_body) {
            self.hipsBody = properties.hips_body;
            // Use the property to define and check some user given infos
            // Longitude reversed
            self.longitudeReversed = true;
        }

        // Give a better name if we have the HiPS metadata
        self.name = self.name || properties.obs_title;

        self.name = self.name || self.id || self.url;
        self.name = self.name.replace(/  +/g, ' ');

        self.creatorDid = self.creatorDid || self.id || self.url;

        // Image format
        if (self.imgFormat) {
            // transform to lower case
            self.imgFormat = self.imgFormat.toLowerCase();
            // convert JPG -> JPEG
            if (self.imgFormat === "jpg") {
                self.imgFormat = "jpeg";
            }

            // user wants a fits but the properties tells this format is not available
            if (
                self.imgFormat === "fits" &&
                self.formats &&
                self.formats.indexOf("fits") < 0
            ) {
                throw self.name + " does not provide fits tiles";
            }

            if (
                self.imgFormat === "webp" &&
                self.formats &&
                self.formats.indexOf("webp") < 0
            ) {
                throw self.name + " does not provide webp tiles";
            }

            if (
                self.imgFormat === "png" &&
                self.formats &&
                self.formats.indexOf("png") < 0
            ) {
                throw self.name + " does not provide png tiles";
            }

            if (
                self.imgFormat === "jpeg" &&
                self.formats &&
                self.formats.indexOf("jpeg") < 0
            ) {
                throw self.name + " does not provide jpeg tiles";
            }
        } else {
            // user wants nothing then we choose one from the properties
            if (self.formats.indexOf("webp") >= 0) {
                self.imgFormat = "webp";
            } else if (self.formats.indexOf("png") >= 0) {
                self.imgFormat = "png";
            } else if (self.formats.indexOf("jpeg") >= 0) {
                self.imgFormat = "jpeg";
            } else if (self.formats.indexOf("fits") >= 0) {
                self.imgFormat = "fits";
            } else {
                throw (
                    "Unsupported format(s) found in the properties: " +
                    self.formats
                );
            }
        }

        // Cutouts
        let minCut, maxCut;
        if (self.imgFormat === "fits") {
            // Take into account the default cuts given by the property file (this is true especially for FITS HiPSes)
            minCut = self.colorCfg.minCut || self.defaultFitsMinCut || 0.0;
            maxCut = self.colorCfg.maxCut || self.defaultFitsMaxCut || 1.0;
        } else {
            minCut = self.colorCfg.minCut || 0.0;
            maxCut = self.colorCfg.maxCut || 1.0;
        }

        self.setOptions({minCut, maxCut});

        // Coo frame
        if (
            self.cooFrame == "ICRS" ||
            self.cooFrame == "ICRSd" ||
            self.cooFrame == "equatorial" ||
            self.cooFrame == "j2000"
        ) {
            self.cooFrame = "ICRS";
        } else if (self.cooFrame == "galactic" || self.cooFrame == "GAL") {
            self.cooFrame = "GAL";
        } else {
            console.warn(
                "Invalid cooframe given: " +
                    self.cooFrame +
                    '. Coordinate systems supported: "ICRS", "ICRSd", "j2000" or "galactic". ICRS is chosen by default'
            );
            self.cooFrame = "ICRS";

        }

        self.formats = self.formats || [self.imgFormat];

        self._saveInCache();
    }

    /**
     * Checks if the HiPS represents a planetary body.
     *
     * This method returns a boolean indicating whether the HiPS corresponds to a planetary body, e.g. the earth or a celestial body.
     *
     * @memberof HiPS
     *
     * @returns {boolean} Returns true if the HiPS represents a planetary body; otherwise, returns false.
     */
    HiPS.prototype.isPlanetaryBody = function () {
        return this.hipsBody !== undefined;
    };

    /**
     * Sets the image format for the HiPS.
     *
     * This method updates the image format of the HiPS, performs format validation, and triggers the update of metadata.
     *
     * @memberof HiPS
     *
     * @param {string} format - The desired image format. Should be one of ["fits", "png", "jpg", "webp"].
     *
     * @throws {string} Throws an error if the provided format is not one of the supported formats or if the format is not available for the specific HiPS.
     */
    HiPS.prototype.setImageFormat = function (format) {
        let self = this;
        self.query.then(() => {
            let imgFormat = format.toLowerCase();

            if (
                imgFormat !== "fits" &&
                imgFormat !== "png" &&
                imgFormat !== "jpg" &&
                imgFormat !== "jpeg" &&
                imgFormat !== "webp"
            ) {
                throw 'Formats must lie in ["fits", "png", "jpg", "webp"]';
            }

            if (imgFormat === "jpg") {
                imgFormat = "jpeg";
            }

            // Passed the check, we erase the image format with the new one
            // We do nothing if the imgFormat is the same
            if (self.imgFormat === imgFormat) {
                return;
            }

            // Check the properties to see if the given format is available among the list
            // If the properties have not been retrieved yet, it will be tested afterwards
            const availableFormats = self.formats;
            // user wants a fits but the metadata tells this format is not available
            if (
                imgFormat === "fits" &&
                availableFormats.indexOf("fits") < 0
            ) {
                throw self.id + " does not provide fits tiles";
            }

            if (
                imgFormat === "webp" &&
                availableFormats.indexOf("webp") < 0
            ) {
                throw self.id + " does not provide webp tiles";
            }

            if (
                imgFormat === "png" &&
                availableFormats.indexOf("png") < 0
            ) {
                throw self.id + " does not provide png tiles";
            }

            if (
                imgFormat === "jpeg" &&
                availableFormats.indexOf("jpeg") < 0
            ) {
                throw self.id + " does not provide jpeg tiles";
            }

            // Switch from png/webp/jpeg to fits
            if (
                (self.imgFormat === "png" ||
                    self.imgFormat === "webp" ||
                    self.imgFormat === "jpeg") &&
                imgFormat === "fits"
            ) {
                if (Number.isFinite(self.defaultFitsMinCut) && Number.isFinite(self.defaultFitsMaxCut)) {
                    // reset cuts to those given from the properties
                    self.setCuts(self.defaultFitsMinCut, self.defaultFitsMaxCut);
                }
                // Switch from fits to png/webp/jpeg
            } else if (self.imgFormat === "fits") {
                self.setCuts(0.0, 1.0);
            }

            // Check if it is a fits
            self.imgFormat = imgFormat;

            self._updateMetadata();
        });
    };

    /**
     * Sets the opacity factor when rendering the HiPS
     *
     * @memberof HiPS
     *
     * @returns {string[]} Returns the formats accepted for the survey, i.e. the formats of tiles that are availables. Could be PNG, WEBP, JPG and FITS.
     */
    HiPS.prototype.getAvailableFormats = function () {
        return this.formats;
    };

    /**
     * Sets the opacity factor when rendering the HiPS
     *
     * @memberof HiPS
     *
     * @param {number} opacity - Opacity of the survey to set. Between 0 and 1
     */
    HiPS.prototype.setOpacity = function (opacity) {
        this.setOptions({opacity})
    };

    /**
     * Sets the blending mode when rendering the HiPS
     *
     * @memberof HiPS
     *
     * @param {boolean} [additive=false] - When rendering this survey on top of the already rendered ones, the final color of the screen is computed like:
     * <br />
     * <br />opacity * this_survey_color + (1 - opacity) * already_rendered_color for the default mode
     * <br />opacity * this_survey_color + already_rendered_color for the additive mode
     * <br />
     * <br />
     * Additive mode allows you to do linear survey color combination i.e. let's define 3 surveys named s1, s2, s3. Each could be associated to one color channel, i.e. s1 with red, s2 with green and s3 with the blue color channel.
     * If the additive blending mode is enabled, then the final pixel color of your screen will be: rgb = [s1_opacity * s1_color; s2_opacity * s2_color; s3_opacity * s3_color]
     */
    HiPS.prototype.setBlendingConfig = function (additive = false) {
        this.setOptions({additive});
    };

    /**
     * Sets the colormap when rendering the HiPS.
     *
     * @memberof HiPS
     *
     * @param {string} [colormap="grayscale"] - The colormap label to use. See {@link https://matplotlib.org/stable/users/explain/colors/colormaps.html|here} for more info about colormaps.
     *      Possible values are:
     * <br>"blues"
     * <br>"cividis"
     * <br>"cubehelix"
     * <br>"eosb"
     * <br>"grayscale"
     * <br>"inferno"
     * <br>"magma"
     * <br>"native"
     * <br>"parula"
     * <br>"plasma"
     * <br>"rainbow"
     * <br>"rdbu"
     * <br>"rdylbu"
     * <br>"redtemperature"
     * <br>"sinebow"
     * <br>"spectral"
     * <br>"summer"
     * <br>"viridis"
     * <br>"ylgnbu"
     * <br>"ylorbr"
     * <br>"red"
     * <br>"green"
     * <br>"blue"
     * @param {Object} [options] - Options for the colormap
     * @param {string} [options.stretch] - Stretching function of the colormap. Possible values are 'linear', 'asinh', 'log', 'sqrt', 'pow'. If no given, will not change it.
     * @param {boolean} [options.reversed=false] - Reverse the colormap axis.
     */
    HiPS.prototype.setColormap = function (colormap, options) {
        this.setOptions({colormap, ...options})
    };

    /**
     * Sets the gamma correction factor for the HiPS.
     *
     * This method updates the gamma of the HiPS.
     *
     * @memberof HiPS
     *
     * @param {number} minCut - The low cut value to set for the HiPS.
     * @param {number} maxCut - The high cut value to set for the HiPS.
     */
    HiPS.prototype.setCuts = function (minCut, maxCut) {
        this.setOptions({minCut, maxCut})
    };

    /**
     * Returns the low and high cuts under the form of a 2 element array
     *
     * @memberof HiPS
     *
     * @returns {number[]} The low and high cut values for the HiPS.
     */
    HiPS.prototype.getCuts = function () {
        return this.colorCfg.getCuts();
    };

    /**
     * Sets the gamma correction factor for the HiPS.
     *
     * This method updates the gamma of the HiPS.
     *
     * @memberof HiPS
     *
     * @param {number} gamma - The saturation value to set for the HiPS. Between 0.1 and 10
     */
    HiPS.prototype.setGamma = function (gamma) {
        this.setOptions({gamma})
    };

    /**
     * Sets the saturation for the HiPS.
     *
     * This method updates the saturation of the HiPS.
     *
     * @memberof HiPS
     *
     * @param {number} saturation - The saturation value to set for the HiPS. Between 0 and 1
     */
    HiPS.prototype.setSaturation = function (saturation) {
        this.setOptions({saturation})
    };

    /**
     * Sets the brightness for the HiPS.
     *
     * This method updates the brightness of the HiPS.
     *
     * @memberof HiPS
     *
     * @param {number} brightness - The brightness value to set for the HiPS. Between 0 and 1
     */
    HiPS.prototype.setBrightness = function (brightness) {
        this.setOptions({brightness})
    };

    /**
     * Sets the contrast for the HiPS.
     *
     * This method updates the contrast of the HiPS and triggers the update of metadata.
     *
     * @memberof HiPS
     *
     * @param {number} contrast - The contrast value to set for the HiPS. Between 0 and 1
     */
    HiPS.prototype.setContrast = function (contrast) {
        this.setOptions({contrast})
    };

    HiPS.prototype.setSliceNumber = function(slice) {
        this.slice = slice;

        if (this.added) {
            this.view.wasm.setSliceNumber(this.layer, slice);
        }
    }

    // Private method for updating the backend with the new meta
    HiPS.prototype._updateMetadata = function () {
        try {
            if (this.added) {
                this.view.wasm.setImageMetadata(this.layer, {
                    ...this.colorCfg.get(),
                    longitudeReversed: this.longitudeReversed,
                    imgFormat: this.imgFormat,
                });
                // once the meta have been well parsed, we can set the meta
                ALEvent.HIPS_LAYER_CHANGED.dispatchedTo(this.view.aladinDiv, {
                    layer: this,
                });

                // Save it in the JS HiPS cache
                this._saveInCache();
            }
        } catch (e) {
            // Display the error message
            console.error(e);
        }
    };

    /**
     * Set color options generic method for changing colormap, opacity, ... of the HiPS
    *
    * @memberof HiPS
    *  
    * @param {Object} options
    * @param {number} [options.opacity=1.0] - Opacity of the survey or image (value between 0 and 1).
    * @param {string} [options.colormap="native"] - The colormap configuration for the survey or image.
    * @param {string} [options.stretch="linear"] - The stretch configuration for the survey or image.
    * @param {boolean} [options.reversed=false] - If true, the colormap is reversed; otherwise, it is not reversed.
    * @param {number} [options.minCut] - The minimum cut value for the color configuration. If not given, 0.0 for JPEG/PNG surveys, the value of the property file for FITS surveys
    * @param {number} [options.maxCut] - The maximum cut value for the color configuration. If not given, 1.0 for JPEG/PNG surveys, the value of the property file for FITS surveys
    * @param {boolean} [options.additive=false] - If true, additive blending is applied; otherwise, it is not applied.
    * @param {number} [options.gamma=1.0] - The gamma correction value for the color configuration.
    * @param {number} [options.saturation=0.0] - The saturation value for the color configuration.
    * @param {number} [options.brightness=0.0] - The brightness value for the color configuration.
    * @param {number} [options.contrast=0.0] - The contrast value for the color configuration.
     */
    HiPS.prototype.setOptions = function(options) {
        this.colorCfg.setOptions(options);
        this.options = {...this.options, ...options};

        this._updateMetadata();
    };

    /**
     * Toggle the HiPS turning its opacity to 0 back and forth
    *
    * @memberof HiPS
    */
    HiPS.prototype.toggle = function () {
        const opacity = this.getOpacity()
        if (opacity != 0.0) {
            this.prevOpacity = opacity;
            this.setOpacity(0.0);
        } else {
            this.setOpacity(this.prevOpacity);
        }
    };

    /**
     * Old method for setting the opacity use {@link HiPS#setOpacity} instead
     * 
     * @memberof HiPS
     * @deprecated
     */
    HiPS.prototype.setAlpha = HiPS.prototype.setOpacity;

    // @api
    HiPS.prototype.getColorCfg = function () {
        return this.colorCfg;
    };
    
    /**
     * Get the opacity of the HiPS layer
     * 
     * @memberof HiPS
     * 
     * @returns {number} The opacity of the layer
     */
    HiPS.prototype.getOpacity = function () {
        return this.colorCfg.getOpacity();
    };

    HiPS.prototype.getAlpha = HiPS.prototype.getOpacity;

    /**
     * Read a specific screen pixel value
     * 
     * @todo This has not yet been implemented
     * @memberof HiPS
     * @param {number} x - x axis in screen pixels to probe
     * @param {number} y - y axis in screen pixels to probe
     * @returns {number} the value of that pixel
     */
    HiPS.prototype.readPixel = function (x, y) {
        return this.view.wasm.readPixel(x, y, this.layer);
    };

    HiPS.prototype._setView = function (view) {
        let self = this;

        // do not allow to call setView multiple times otherwise
        // the querying to the properties and the search to the best
        // HiPS node will be done again for the same hiPS
        if (this.view) {
            return;
        }
        this.view = view;

        if (this.localFiles) {
            // Fetch the properties file
            self.query = (async () => {
                // look for the properties file
                await HiPSProperties.fetchFromFile(self.localFiles["properties"])
                    .then((p) => {
                        self._parseProperties(p);

                        self.url = "local";

                        delete self.localFiles["properties"]
                    })

                return self;
            })();
            
            return;
        }

        let isIncompleteOptions = true;

        // This is very dirty but it allows me to differentiate the location from whether it is an ID or a plain url
        let isID = this.url.includes("P/") || this.url.includes("C/")
        
        if (this.imgFormat === "fits") {
            // a fits is given
            isIncompleteOptions = !(
                this.maxOrder &&
                (!isID && this.url) &&
                this.imgFormat &&
                this.tileSize &&
                this.cooFrame &&
                this.numBitsPerPixel
            );
        } else {
            isIncompleteOptions = !(
                this.maxOrder &&
                (!isID && this.url) &&
                this.imgFormat &&
                this.tileSize &&
                this.cooFrame
            );
        }

        self.query = (async () => {
            if (isIncompleteOptions) {
                // ID typed url
                if (self.startUrl && isID) {

                    // First download the properties from the start url
                    await HiPSProperties.fetchFromUrl(self.startUrl)
                        .then((p) => {
                            self._parseProperties(p);
                        })

                    try {
                        // the url stores a "CDS ID" we take it prioritaly
                        // if the url is null, take the id, this is for some tests
                        // to pass because some users might just give null as url param and a "CDS ID" as id param
                        let id = self.url || self.id;

                        self.url = self.startUrl;

                        setTimeout(
                            () => {
                                if (!self.added)
                                    return;

                                HiPSProperties.fetchFromID(id)
                                    .then((p) => {
                                        //self.url = self.startUrl;
                                        self._fetchFasterUrlFromProperties(p);
                                    })
                            },
                            1000
                        );
                    } catch (e) {
                        throw e;
                    }
                } else if (!this.startUrl && isID) {
                    try {
                        // the url stores a "CDS ID" we take it prioritaly
                        // if the url is null, take the id, this is for some tests
                        // to pass because some users might just give null as url param and a "CDS ID" as id param
                        let id = self.url || self.id;

                        await HiPSProperties.fetchFromID(id)
                            .then((p) => {
                                self.url = p.hips_service_url;

                                self._parseProperties(p);
                                self._fetchFasterUrlFromProperties(p);
                            })
                    } catch (e) {
                        throw e;
                    }
                } else {
                    await HiPSProperties.fetchFromUrl(self.url)
                        .then((p) => {
                            self._parseProperties(p);
                        })
                }
            } else {
                self._parseProperties({
                    hips_order: this.maxOrder,
                    hips_service_url: this.url,
                    hips_tile_width: this.tileSize,
                    hips_frame: this.cooFrame
                })
            }

            return self;
        })()
    };

    /* Precondition: view is attached */
    HiPS.prototype._saveInCache = function () {
        let self = this;
        let hipsCache = this.view.aladin.hipsCache;

        if (hipsCache.contains(self.id)) {
            hipsCache.append(self.id, this.options)
        }
    };

    HiPS.prototype._add = function (layer) {
        this.layer = layer;
        let self = this;

        const config = {
            layer,
            properties: {
                creatorDid: self.creatorDid,
                url: self.url,
                maxOrder: self.maxOrder,
                cooFrame: self.cooFrame,
                tileSize: self.tileSize,
                formats: self.formats,
                bitpix: self.numBitsPerPixel,
                skyFraction: self.skyFraction,
                minOrder: self.minOrder,
                hipsInitialFov: self.initialFov,
                hipsInitialRa: self.initialRa,
                hipsInitialDec: self.initialDec,
                hipsCubeDepth: self.cubeDepth,
                isPlanetaryBody: self.isPlanetaryBody(),
                hipsBody: self.hipsBody,
            },
            meta: {
                ...this.colorCfg.get(),
                longitudeReversed: this.longitudeReversed,
                imgFormat: this.imgFormat,
            }
        };

        let localFiles;
        if (this.localFiles) {
            localFiles = new Aladin.wasmLibs.core.HiPSLocalFiles(this.localFiles["moc"]);

            let fmt;
            for (var order in this.localFiles) {
                if (order === "moc")
                    continue;

                for (var ipix in this.localFiles[order]) {
                    for (var f in this.localFiles[order][ipix]) {
                        if (f === "png") {
                            fmt = Aladin.wasmLibs.core.ImageExt.Png;
                        } else if (f === "fits") {
                            fmt = Aladin.wasmLibs.core.ImageExt.Fits;
                        } else {
                            fmt = Aladin.wasmLibs.core.ImageExt.Jpeg;
                        }

                        const tileFile = this.localFiles[order][+ipix][f];
                        localFiles.insert(+order, BigInt(+ipix), fmt, tileFile)
                    }
                }
            }
        }

        this.view.wasm.addHiPS(
            config,
            localFiles
        );

        return Promise.resolve(this)
            .then((hips) => {
                this.added = true;

                if (hips.successCallback) {
                    hips.successCallback(hips)
                }

                return hips
            });
    };

    HiPS.DEFAULT_SURVEY_ID = "P/DSS2/color";

    return HiPS;
})();