Source: public/javascript/modules/olqv_ned.js

import { Constants } from "./constants.js";
import { DecDeg2HMS, DecDeg2DMS, redshift2Velocity } from "./utils.js";
import { EventFactory } from "./customevents.js";
import { DOMAccessor } from "./domelements.js";


/**
 * Removes everything from the DOM element containing the results
 */
function cleanContainer(domElement) {
    if(domElement !== null){
        while (domElement.firstChild) {
            domElement.firstChild.remove()
        }
    }
}

/**
 * 
 * All NED response fields : 
 * 
 * Associations, DEC, Diameter Points, Magnitude and Filter, No.
​ * Notes, Object Name, Photometry Points, Positions, RA, Redshift
​ * Redshift Flag, Redshift Points, References, Separation, Type, Velocity
 * 
 */
/**
 * Returns separation in arcsec ( for lisibility )
 * @param {*} RA1 in degrees
 * @param {*} DEC1 in degrees
 * @param {*} RA2 in degrees
 * @param {*} DEC2 in degrees
 */
function separation(RA1, DEC1, RA2, DEC2) {
    const alpha_line = parseFloat(RA1) * Math.PI / 180;
    const delta_line = parseFloat(DEC1) * Math.PI / 180;
    const alpha_glob = parseFloat(RA2) * Math.PI / 180;
    const delta_glob = parseFloat(DEC2) * Math.PI / 180;
    const step1 = Math.sin(delta_line) * Math.sin(delta_glob) + Math.cos(delta_line) * Math.cos(delta_glob) * Math.cos(alpha_line - alpha_glob);
    return (Math.acos(step1) * 180 / Math.PI * 60) * 60;
}

/**
 * Returns tangential comoving distance
 * @params {float}  WK
 * @params {float}  DCMR
 * 
 * @returns {float}
 */
function dcmt(wk, dcmr) {
    let ratio = 1.00;
    const x = Math.sqrt(Math.abs(wk)) * dcmr;
    if (x > 0.1) {
        if (wk > 0) {
            ratio = 0.5 * (Math.exp(x) - Math.exp(-x)) / x;
        } else {
            ratio = Math.sin(x) / x;
        }
        return ratio * dcmr;
    }

    let y = x * x
    if (wk < 0) {
        y = -y;
    }
    ratio = 1 + y / 6 + y * y / 120;
    y = ratio * dcmr;
    return y;
}


/**
 * Returns an object containing the distant in Mpc, the scale in kpc, 
 * the luminosity distance in Mpc and the z value in Gyr
 * 
 * @param {number} Hnot Hubble constant value
 * @param {number} OmegaM mass density value 
 * @param {number} z redshift value
 * @returns {object}
 */
function computeDistanceQuantities(Hnot, OmegaM, z) {
    //number of points in integrals
    const n = 1000; 
    const H0 = Hnot; 
    const WM = OmegaM;
    const WV = 1. - OmegaM;
    const h = H0 / 100;
    // velocity of light in km/sec
    const c = Constants.SPEED_OF_LIGHT / 1000;
    // coefficent for converting 1/H into Gyr
    const Tyr = 977.8;
    // value of age in Gyr
    let age_Gyr = 0.0;
    let DCMR_Mpc = 0.0;
    let DCMR_Gyr = 0.0;
    // angular size distance
    let DA = 0.0; 
    let DA_Mpc = 0.0;
    let DA_Gyr = 0.0;
    //let kpc_DA = 0.0;
    // luminosity distance
    let DL = 0.0; 
    let DL_Mpc = 0.0;
    let DL_Gyr = 0.0; // DL in units of billions of light years
    //const a = 1.0; // 1/(1+z), the scale factor of the Universe

    // entry point for the input form to pass values back to this script
    // includes 3 massless neutrino species, T0 = 2.72528
    const WR = 4.165E-5 / (h * h); 
    // Omega curvaturve = 1-Omega(total)
    const WK = 1 - WM - WR - WV; 
    const az = 1.0 / (1 + 1.0 * z);
    let age = 0;

    for (let i = 0; i < n; i++) {
        const a = az * (i + 0.5) / n;
        const adot = Math.sqrt(WK + (WM / a) + (WR / (a * a)) + (WV * a * a));
        age = age + 1 / adot;
    }

    // age of Universe at redshift z in units of 1/H0
    let zage = az * age / n;
    /* correction for annihilations of particles not present now like e+/e-
       added 13-Aug-03 based on T_vs_t.f */
    const lpz = Math.log((1 + 1.0 * z)) / Math.log(10.0);
    let dzage = 0
    if (lpz > 7.500)
        dzage = 0.002 * (lpz - 7.500);
    if (lpz > 8.000)
        dzage = 0.014 * (lpz - 8.000) + 0.001;
    if (lpz > 8.500)
        dzage = 0.040 * (lpz - 8.500) + 0.008;
    if (lpz > 9.000)
        dzage = 0.020 * (lpz - 9.000) + 0.028;
    if (lpz > 9.500)
        dzage = 0.019 * (lpz - 9.500) + 0.039;
    if (lpz > 10.000)
        dzage = 0.048;
    if (lpz > 10.775)
        dzage = 0.035 * (lpz - 10.775) + 0.048;
    if (lpz > 11.851)
        dzage = 0.069 * (lpz - 11.851) + 0.086;
    if (lpz > 12.258)
        dzage = 0.461 * (lpz - 12.258) + 0.114;
    if (lpz > 12.382)
        dzage = 0.024 * (lpz - 12.382) + 0.171;
    if (lpz > 13.055)
        dzage = 0.013 * (lpz - 13.055) + 0.188;
    if (lpz > 14.081)
        dzage = 0.013 * (lpz - 14.081) + 0.201;
    if (lpz > 15.107)
        dzage = 0.214;

    zage = zage * Math.pow(10.0, dzage);
    const zage_Gyr = (Tyr / H0) * zage;
    let DTT = 0.0;
    let DCMR = 0.0;
    // do integral over a=1/(1+z) from az to 1 in n steps, midpoint rule
    for (let i = 0; i < n; i++) {
        const a = az + (1. - az) * (i + 0.5) / n;
        const adot = Math.sqrt(WK + (WM / a) + (WR / (a * a)) + (WV * a * a));
        DTT = DTT + 1. / adot;
        DCMR = DCMR + 1. / (a * adot);
    }

    DTT = (1. - az) * DTT / n;
    DCMR = (1. - az) * DCMR / n;
    age = DTT + zage;
    age_Gyr = age * (Tyr / H0);
    DCMR_Gyr = (Tyr / H0) * DCMR;
    DCMR_Mpc = (c / H0) * DCMR;
    DA = az * dcmt(WK, DCMR);

    DA_Mpc = (c / H0) * DA;
    const scale_DA = DA_Mpc / 206.264806;
    DA_Gyr = (Tyr / H0) * DA;
    DL = DA / (az * az);
    DL_Mpc = (c / H0) * DL;
    DL_Gyr = (Tyr / H0) * DL;

    return {
        "distance_Mpc": DA_Mpc,
        "scale_kpc": scale_DA,
        "luminosity_distance_Mpc": DL_Mpc,
        "z_age_Gyr": zage_Gyr
    };
}

/**
 * Returns luminosity distance in Mpc calculated by computeDistanceQuantities
 * @param {number} Hnot Hubble constant value
 * @param {number} OmegaM mass density value 
 * @param {number} z redshift value
 * @returns luminosity distance in Mpc
 */
function computeDl(Hnot, OmegaM, z){
    return computeDistanceQuantities(Hnot, OmegaM, z)["luminosity_distance_Mpc"];
}

/**
 * An object representing a list of sources in the sky
 */
class SourceTable {
    /**
     * 
     * @param {string} table_target DOM element containing the table
     * @param {string} table_title  Title of the table
     * @param {object} spectroUI    spectroscopy user interface
     */
    constructor(table_target, table_title, spectroUI) {
        this.nedResult = [];
        this.tableTarget = table_target;
        this.tableTitle = table_title;
        // purely internal table id
        this._tableId = "ned-table";
        // name of area displaying a warning message to user when DL value is not calculated
        this.dlWarning = "dl-warning";
        this._sourceInfo = "ned-source-info";
        this._dlInfo = "ned-dl-info";
        this.spectroUI = spectroUI;
        this.previousSelection = null;

        this.radius = null;
        this.Hnot = 69.6;
        this.OmegaM = 0.286;
        this._lastZ = null;

        this.cache = {};
        this.data = [];

        // a list a listeners that are called when a new value is selected in table
        this.selectionListeners = [];

        this._getHyperlinkCell = this._getHyperlinkCell.bind(this);
        this._getTableHeader = this._getTableHeader.bind(this);
        this._getTable = this._getTable.bind(this);
        this._showResult = this._showResult.bind(this);
        this._onClick = this._onClick.bind(this);
        this._getDataTable = this._getDataTable.bind(this);
        this._writeInfoMessage = this._writeInfoMessage.bind(this);
        this._showSpinner = this._showSpinner.bind(this);
        this.getSources = this.getSources.bind(this);
        this._setTitle = this._setTitle.bind(this);
        this._getNedTapUrl = this._getNedTapUrl.bind(this);
        this._executeListeners = this._executeListeners.bind(this);
        this.addListener = this.addListener.bind(this);
        this.removeListener = this.removeListener.bind(this);

    }

    get lastZ(){
        return this._lastZ;
    }

    /**
     * Returns a TAP request to NED service
     * @param {float} RA 
     * @param {float} DEC 
     * @returns {string} query url
     */
    _getNedTapUrl(RA, DEC, radius) {
        const url = "https://ned.ipac.caltech.edu/tap/sync?query=SELECT+prefname,ra,dec,z,pretype,n_crosref+FROM+objdir+WHERE+CONTAINS(POINT('',ra,dec),CIRCLE(''," + RA + "," + DEC + "," + radius + "))=1+AND+z+IS+NOT+null+ORDER+BY+n_crosref+DESC&LANG=ADQL&REQUEST=doQuery&FORMAT=votable%2ftd";
        return url;
    }

    /**
     * Returns a cone search request to NED service
     * @param {float} RA 
     * @param {float} DEC 
     * @returns {string} query url
     */
    _getNedWebUrl(RA, DEC, radius) {
        // ned cone search radius unit is arcmin
        const search_radius = radius * 60;
        const url = "https://ned.ipac.caltech.edu/conesearch?in_csys=Equatorial&in_equinox=J2000&coordinates=" + RA + "d%20" + DEC + "d&radius=" + search_radius + "&hconst=67.8&omegam=0.308&omegav=0.692&wmap=4&corr_z=1&z_constraint=Available&z_unit=z&ot_include=ANY&nmp_op=ANY&search_type=Near%20Position%20Search&out_csys=Equatorial&out_equinox=Same%20as%20Input&obj_sort=Distance%20to%20search%20center";
        return url;
    }

    /** Returns a link to the NED page listing distances for a source
     * 
     * @param {object} source source name
     * @returns url
     */
    _getDistanceLink(source){
        return "http://ned.ipac.caltech.edu/cgi-bin/nDistance?name="+encodeURI(source);
    }

    /**
     * Builds the content of the container displaying the source infos
     * @param {object} source  a source name
     * @returns DOMElement
     */
    _buildSourceInfo(source){
        let span = document.createElement('span');
        span.innerText = "Selected source : " + source;
        return span;
    }

    /**
     * Builds the content of the container displaying link to NED DL page
     * for this source
     * @param {object} source  a source name
     * @returns DOMElement
     */
    _buildDLInfo(source){
        let link = document.createElement("a");
        link.innerText = "(Redshift-independant DL)";
        link.href = this._getDistanceLink(source);
        link.target = "_blank";
        return link;
    }

    /**
     * Fills the title area of the sources container
     * @param {float} RA 
     * @param {float} DEC 
     * @param {float} radius 
     */
    _setTitle(RA, DEC, radius) {
        let domElement = document.getElementById(this.tableTitle);
        let div = document.createElement("div");
        div.className = "title";
        while (domElement.firstChild) {
            domElement.firstChild.remove();
        }
        let p1 = document.createElement("p");
        let p2 = document.createElement("p");
        let p3 = document.createElement("p");
        p3.id = this.dlWarning;
        p3.className = this.dlWarning;
        let a = document.createElement("a");
        a.href = this._getNedWebUrl(RA, DEC, radius);
        a.target = "_blank";
        a.innerHTML = "Data from NED";
        p1.appendChild(a);
        p2.innerHTML = `Flat Universe with <span title="Hubble constant">H<sub>0</sub></span>=${this.Hnot}, 
                        <span title="Mass density">&Omega;<sub>M</sub></span>=${this.OmegaM}`;
        div.appendChild(p1);
        div.appendChild(p2);
        div.appendChild(p3);
        domElement.appendChild(div);
    }

    /**
     * Displays a message in the dedicated area in title div
     * @param {string} message displayed message
     */
    _setDlWarning(message){
        document.getElementById(this.dlWarning).innerText = message;
    }

    /**
     * Returns a TD DOM element
     * @param {string} data 
     */
    _getDataCell(data) {
        let td = document.createElement("td");
        td.innerHTML = data;
        return td;
    }

    /**
     * Returns a TD DOM element containing a hyperlink
     * @param {string} link 
     * @param {string} text 
     */
    _getHyperlinkCell(link, text) {
        let a = document.createElement("a");
        a.innerHTML = text;
        a.href = link;
        a.target = "_blank";
        let td = document.createElement("td");
        td.appendChild(a);
        return td;
    }

    /**
     * Returns a TH DOM element
     * @param {string} content 
     */
    _getTableHeader(content) {
        let th = document.createElement("th");
        th.innerHTML = content;
        return th;
    }

    /**
     * Add a SPAN element with a text message inside the DOM element containing the results
     * @param {string} text 
     */
    _writeInfoMessage(text) {
        let domElement = document.getElementById(this.tableTarget);
        let span = document.createElement("span");
        span.textContent = text;
        domElement.appendChild(span);
    }

    /**
     * Shows a spinner image indicating that the UI is busy
     * 
     */
    _showSpinner() {
        let domElement = document.getElementById(this.tableTarget);
        let div = document.createElement("div");
        div.classList.add("spinner-border");
        div.role = "status";
        let span = document.createElement("span");
        span.classList.add("sr-only");
        div.appendChild(span);
        domElement.appendChild(div);
    }

    /**
     * Calls all the registered listeners
     * @param event contains values of redshift, velocity, ra, dec
     */
    _executeListeners(event) {
        for (let l of this.selectionListeners) {
            l.sourceTableCall(event);
        }
    }

    /**
     * Adds a new listener to the list
     * @param {object} listener 
     */
    addListener(listener) {
        this.selectionListeners.push(listener);
    }

    /**
     * Removes a listener
     * @param {object} listener 
     */
    removeListener(listener) {
        for (let i = 0; i < this.selectionListeners.length; i++) {
            if (this.selectionListeners[i] === listener) {
                this.selectionListeners.splice(i, 1);
                //return true;
            }
        }
    }

    /**
     * Event triggered when a TR element is clicked
     * It is propagated to all entries in this.selectionListeners
     * 
     * @param {*} event 
     */
    _onClick(event) {
        let row = event.target.parentNode.getAttribute("data-row");
        // if row is null, user clicked on link to ned page
        if(row !== null){
            if (this.previousSelection != null) {
                this.previousSelection.className = '';
            }
            event.target.parentNode.className = "table-active";
            //const velocity = redshift2Velocity(this.data[row]['redshift']);
            this.previousSelection = event.target.parentNode;

            this.spectroUI.setRedshift(this.data[row]['redshift']);
            this.spectroUI.setVelocity(0);


            if(document.getElementById(this._dlInfo) !== null){
                this.spectroUI.setDl(this.data[row]['dl']);
                cleanContainer(document.getElementById(this._dlInfo));
                if(this.data[row]['redshift'] < Constants.MIN_REDSHIFT_FOR_CALC ){
                    this._setDlWarning(`z < ${Constants.MIN_REDSHIFT_FOR_CALC}, DL value must be set manually to compute a Line Luminosity`);
                    document.getElementById(this._dlInfo).appendChild(this._buildDLInfo(this.data[row]['object']));
                }else{
                    this._setDlWarning("");
                }
            }

            //last selected source
            if(document.getElementById(this._sourceInfo) !== null){
                cleanContainer(document.getElementById(this._sourceInfo));
                document.getElementById(this._sourceInfo).appendChild(this._buildSourceInfo(this.data[row]['object']));
                //reset redshift to last selected source value
                DOMAccessor.getResetZButton().classList.remove("hidden");
            }

            let clickevent = EventFactory.getEvent(EventFactory.EVENT_TYPES.custom,  {
                detail: {
                    redshift: this.data[row]['redshift'],
                    //velocity: velocity,
                    ra: this.data[row]['ra'],
                    dec: this.data[row]['dec'],
                    object: this.data[row]['object']
                }
            })
            this._lastZ = this.data[row]['redshift'];
            this._executeListeners(clickevent);
        }
    }

    /**
     * Returns a HTML table element with all its rows in tbody
     * 
     * @param {*} rows 
     * @returns {domElement}
     */
    _getTable(rows) {
        let table = document.createElement("table");
        table.id = this._tableId;
        table.className = "table table-hover ned-table";
        let thead = document.createElement("thead");
        let tr = document.createElement("tr");
        tr.appendChild(this._getTableHeader("Object Name"));
        tr.appendChild(this._getTableHeader("RA"));
        tr.appendChild(this._getTableHeader("Dec"));
        tr.appendChild(this._getTableHeader("Type"));
        tr.appendChild(this._getTableHeader("Redshift"));
        tr.appendChild(this._getTableHeader("Separation (arcsec)"));
        tr.appendChild(this._getTableHeader("Separation (kpc)"));
        tr.appendChild(this._getTableHeader("Scale (kpc/‘’)"));
        tr.appendChild(this._getTableHeader("DA (Mpc)"));
        tr.appendChild(this._getTableHeader("Z age (Gyr)"));
        tr.appendChild(this._getTableHeader("DL (Mpc)"));
        tr.appendChild(this._getTableHeader("References"));
        tr.appendChild(this._getTableHeader("Link to NED"));
        thead.appendChild(tr);
        table.appendChild(thead);
        let tbody = document.createElement("tbody");

        for (let row of rows) {
            tbody.appendChild(row);
        }
        table.appendChild(tbody);
        return table;
    }

    /**
     * Displays the result table or a warning message if no result has been found
     * 
     * @param {float} RA
     * @param {float} DEC
     */
    _showResult(RA, DEC) {
        // clear content of component containing the table
        cleanContainer(document.getElementById(this.tableTarget));

        let table = this._getDataTable(RA, DEC, this.radius);
        let rows = table["table"];
        this.data = table["data"];

        // build results table
        if (rows.length > 0) {
            let domElement = document.getElementById(this.tableTarget);
            domElement.appendChild(this._getTable(rows));
            //table sortable and searchable
            $('#' + this._tableId).DataTable({
                order: [[11, 'desc']]
            });
        }
        // no redshift data available, show message
        else {
            console.log("no redshift found, writeMessage");
            this._writeInfoMessage("No redshift information available");
        }
    }

    /**
     * Returns rows of HTML table and the metadata associated to each row.
     * Additional metadata are calculated if z > Constants.MIN_REDSHIFT_FOR_CALC 
     * (scale in kpc, distance in Mpc, * z age in Gyr, luminosity distance in Mpc) 
     * 
     * @returns {object} {"table" : [], "data" : []}
     */
    _getDataTable(RA, DEC) {
        let rows = [];
        let rows_data = [];
        for (let source of this.nedResult) {
            let data = source.data;
            const sep = separation(data["ra"], data["dec"], RA, DEC);
            const distances = computeDistanceQuantities(this.Hnot, this.OmegaM, data["z"]);
            let row = document.createElement("tr");
            row.setAttribute("data-row", rows.length);
            let link = this._getHyperlinkCell(Constants.NED_SERVICE_URL + encodeURIComponent(data["prefname"]), data["prefname"]);
            row.appendChild(this._getDataCell("<strong>"+data["prefname"]+"</strong>"));
            row.appendChild(this._getDataCell(DecDeg2HMS(parseFloat(data["ra"]))));
            row.appendChild(this._getDataCell(DecDeg2DMS(parseFloat(data["dec"]))));
            row.appendChild(this._getDataCell(data["pretype"]));
            row.appendChild(this._getDataCell(data["z"]));
            row.appendChild(this._getDataCell(sep.toFixed(3)));

            if (data["z"] > Constants.MIN_REDSHIFT_FOR_CALC) {
                row.appendChild(this._getDataCell((sep * distances["scale_kpc"]).toFixed(3)));
                row.appendChild(this._getDataCell(distances["scale_kpc"].toFixed(3)));
                row.appendChild(this._getDataCell(distances["distance_Mpc"].toFixed(3)));
                row.appendChild(this._getDataCell(distances["z_age_Gyr"].toFixed(3)));
                row.appendChild(this._getDataCell(distances["luminosity_distance_Mpc"].toFixed(3)));
            } else {
                row.appendChild(this._getDataCell("N/A"));
                row.appendChild(this._getDataCell("N/A"));
                row.appendChild(this._getDataCell("N/A"));
                row.appendChild(this._getDataCell("N/A"));
                row.appendChild(this._getDataCell("N/A"));
            }

            let dl = parseFloat(distances["luminosity_distance_Mpc"].toFixed(3));
            if(data["z"] <= Constants.MIN_REDSHIFT_FOR_CALC){
                dl = null;
            }

            row.appendChild(this._getDataCell(data["n_crosref"]));
            row.addEventListener('click', this._onClick);
            row.appendChild(this._getHyperlinkCell(Constants.NED_SERVICE_URL + encodeURIComponent(data["prefname"]), "Link"));
            rows_data.push({
                "object": data["prefname"].trim(),
                "redshift": parseFloat(data["z"]),
                "velocity": "",
                "dl": dl,
                "ra": parseFloat(data["ra"]),
                "dec": parseFloat(data["dec"])
            });
            rows.push(row);
        }

        return { "table": rows, "data": rows_data };
    }

    /**
     * Gets sources at RA/DEC/radius from NED catalog
     * @param {*} RA
     * @param {*} DEC
     * @param {*} radius in degrees
     */
    getSources(RA, DEC, radius, Hnot, OmegaM) {
        cleanContainer(document.getElementById(this.tableTarget));
        this.radius = radius;
        this.Hnot = Hnot;
        this.OmegaM = OmegaM;
        this._setTitle(RA, DEC, radius);

       
        //this._writeInfoMessage("Please wait, NED request in progress.");
        this._showSpinner();
        this.previousRadius = radius;

        const url = this._getNedTapUrl(RA, DEC, radius);
        // NED request through Aladin
        // This is a vulnerability of our service, if Aladin is down NED is unreachable
        A.catalogFromURL(url, { onClick: 'showTable' }, (result) => {
            if (result.length > 0) {
                this.nedResult = result;
                this._showResult(RA, DEC);
            } else {
                cleanContainer(document.getElementById(this.tableTarget));
                this._writeInfoMessage("Nothing found, take a larger radius.");
            }
        });
    }
}


export {
    SourceTable, computeDistanceQuantities, computeDl
}