Source: public/javascript/modules/utils.js

import { SPEED_OF_LIGHT, VELOCITY_SHIFT_TYPE, REDSHIFT_SHIFT_TYPE } from "./constants.js";

/*
 ** A set of classes and function definitions utilized by the
 ** differents flavours of OLQV viewers.
 **
 ** Author : M. Caillat
 ** Date : 06th December 2018
 **        07th April 2021
 */

/*
 ** A class to convert a right ascension expressed in decimal degree into an integer value expressing a pixel index.
 */
class RADDtoPixelConverter {
    static enter(what) {
        console.group(this.name + "." + what);
    }

    static exit() {
        console.groupEnd();
    }

    constructor(radd0, radd1, rapix0, rapix1) {
        RADDtoPixelConverter.enter(this.constructor.name);
        this.radd0 = radd0;
        this.rapix0 = rapix0;
        this.slope = (rapix1 - rapix0) / (radd1 - radd0);
        RADDtoPixelConverter.exit();
    }

    convert(radd) {
        RADDtoPixelConverter.enter(this.convert.name);
        var result = this.rapix0 + (radd - this.radd0) * this.slope;
        RADDtoPixelConverter.exit();
        return result;
    }
}

/*
 ** A class to convert a declination expressed in decimal degree into an integer value expressing a pixel index.
 */
class DECDDtoPixelConverter {
    static enter(what) {
        console.group(this.name + "." + what);
    }

    static exit() {
        console.groupEnd();
    }

    constructor(decdd0, decdd1, decpix0, decpix1) {
        DECDDtoPixelConverter.enter(this.constructor.name);
        this.decdd0 = decdd0;
        this.decpix0 = decpix0;
        this.slope = (decpix1 - decpix0) / (decdd1 - decdd0);
        DECDDtoPixelConverter.exit();
    }

    convert(decdd) {
        DECDDtoPixelConverter.enter(this.convert.name);
        var result = this.decpix0 + (decdd - this.decdd0) * this.slope;
        DECDDtoPixelConverter.exit();
        return result;
    }
};

class PixelToAbsConverter {
    static enter(what) {
        console.group(this.name + "." + what);
    }

    static exit() {
        console.groupEnd();
    }

    /**
     * 
     * @param {integer} crpix1 the reference pixel along the first axis ( supposedly RA )
     * @param {float} cdeltInDeg1 the coordinate increment in degree at the reference point along the first axis ( supposedly RA )
     * @param {integer} crpix2 the reference pixel along the second axis ( supposedly DEC )
     * @param {float} cdeltInDeg2 the coordinate increment in degree at the reference point along the second axis ( supposedly DEC )
     * @param {Projection} projection the instance of Projection deduced from the FITS header.
     */
    constructor(crpix1, cdeltInDeg1, crpix2, cdeltInDeg2, projection) {
        this.crpix1 = crpix1;
        this.cdeltInRad1 = cdeltInDeg1 * Math.PI / 180.;
        this.crpix2 = crpix2;
        this.cdeltInRad2 = cdeltInDeg2 * Math.PI / 180.;
        this.projection = projection;
    }

    /**
     * 
     * @param {integer} i an integer expressing the first pixel coordinate on the pixels grid ( supposedly RA )  
     * @param {integer} j an integer expressing the second pixel coordinate on the pixels grid ( supposedly DEC )
     * @returns {float[2]} [<right-ascension-in-degree>, <declination-in-degree>]
     */
    convert(i, j) {
        let aux = this.projection.relToAbs([(i - this.crpix1) * this.cdeltInRad1], [(j - this.crpix2) * this.cdeltInRad2]);
        return ([aux['ra'][0] * 180 / Math.PI, aux['dec'][0] * 180 / Math.PI]);
    }

    /**
     * @param {integer} i an integer expressing the first pixel coordinate on the pixels grid ( supposedly RA )
     * @param {integer} j an integer expressing the second pixel coordinate on the pixels grid ( supposedly DEC )
     * @returns {String[2]} [<string-HMS>, <string-DMS>]
     */
    label(i, j) {
        let [raInDeg, decInDeg] = this.convert(i, j);
        return [DecDeg2HMS(raInDeg), DecDeg2DMS(decInDeg)];
    }
}

class AbsToPixelConverter {
    static enter(what) {
        console.group(this.name + "." + what);
    }

    static exit() {
        console.groupEnd();
    }

    /**
     * 
     * @param {integer} crpix1 the reference pixel along the first axis ( supposedly RA )
     * @param {float} cdelt1 the coordinate increment in radians at the reference point along the first axis ( supposedly RA )
     * @param {integer} crpix2 the reference pixel along the second axis ( supposedly DEC )
     * @param {float} cdelt2 the coordinate increment in radians at the reference point along the second axis ( supposedly DEC )
     * @param {Projection} projection the instance of Projection deduced from the FITS header.
     */
    constructor(crpix1, cdelt1, crpix2, cdelt2, projection) {
        this.crpix1 = crpix1;
        //degrees to radians
        this.cdelt1 = degToRad(cdelt1);
        this.crpix2 = crpix2;
        // degrees to radians
        this.cdelt2 = degToRad(cdelt2);
        this.projection = projection;
    }

    /**
     * 
     * @param {float} x right ascension in radian 
     * @param {float} y right ascention in radian
     * @returns {integer[2]} [<pixel-first-coordinate>, <pixel-second-coordinate>]
     */
    convert(x, y) {
        let aux = this.projection.absToRel([x], [y]);
        let i = this.crpix1 + ((aux["x"][0] / this.cdelt1) - 1);
        let j = this.crpix2 + ((aux["y"][0] / this.cdelt2) - 1);
        return ([Math.round(i), Math.round(j)]);
    }
}

class PixelToDECConverter {
    static enter(what) {
        console.group(this.name + "." + what);
    }

    static exit() {
            console.groupEnd();
        }
        /**
         * 
         * @param {*} refpos        CRVAL3
         * @param {*} refchannel    CRPIX3
         * @param {*} angularoffset CDELT3
         */
    constructor(refpos, refchannel, angularoffset) {
        this.crval3 = refpos;
        this.crpix3 = refchannel;
        this.cdelt3 = angularoffset;
    }

    /**
     * 
     * @param {*} pix 
     */
    convert(pix) {
        var result = this.crval3 + (pix - this.crpix3) * this.cdelt3;
        return result;
    }
};

class PixelToRAConverter {
    static enter(what) {
        console.group(this.name + "." + what);
    }

    static exit() {
            console.groupEnd();
        }
        /**
         * 
         * @param {*} refpos        CRVAL1
         * @param {*} refchannel    CRPIX1
         * @param {*} angularoffset CDELT1
         * @param {*} dec           DEC in degree
         */
    constructor(refpos, refchannel, angularoffset, dec) {
        this.crval1 = refpos;
        this.crpix1 = refchannel;
        this.cdelt1 = angularoffset;
    }

    /**
     *   static enter(what) {
      console.group(this.name + "." + what);
    }

    static exit() {
      console.groupEnd();
    }
     * @param {*} pix 
     */
    convert(pix, dec) {
        let dec_inrad = (dec * Math.PI) / 180;
        var result = this.crval1 + (pix - this.crpix1) * this.cdelt1 / Math.cos(dec_inrad);
        return result;
    }
};

/*
 ** Converts a decimal number expected to represent an angle in degree
 ** into a string expressing a right ascension ( H:M:S)
 */
var DecDeg2HMS = function(deg, sep = ':') {
    //if(any(deg< 0 | deg>360)){stop('All deg values should be 0<=d<=360')}
    //if (deg < 0)
    //deg[deg < 0] = deg[deg < 0] + 360
    const HRS = Math.floor(deg / 15);
    const MIN = Math.floor((deg / 15 - HRS) * 60);
    let SEC = (deg / 15 - HRS - MIN / 60) * 3600;
    SEC = Math.floor(SEC * 1000) / 1000.;
    return HRS + sep + MIN + sep + SEC.toFixed(3);
};

/**
 * Converts a H:M:S value into decimal degrees
 * @param {*} value 
 * @returns 
 */
var HMS2DecDeg = function(value){
    const parts = value.split(":");
    if(parts.length !== 3){
        throw("Invalid format for H:M:S value");
    }else{
        const a = parseInt(parts[0]);
        const b = parseFloat(parts[1])*(1/60);
        const c = parseFloat(parts[2]) * (1/3600);
        return (a + b + c)*15;
    }
};

/*
 ** Converts a decimal number expected to represent an angle in degree
 ** into a string expressing a declination ( D:M:S)
 */
var DecDeg2DMS = function(deg, sep = ':') {
    var sign = deg < 0 ? '-' : '+';
    deg = Math.abs(deg);
    var DEG = Math.floor(deg);
    var MIN = Math.floor((deg - DEG) * 60);
    var SEC = (deg - DEG - MIN / 60) * 3600;
    SEC = Math.floor(SEC * 1000.) / 1000.;
    if (SEC < 0.) SEC = 0.;
    if (SEC > 60) SEC = 60.;

    return (sign + DEG + sep + MIN + sep + SEC.toFixed(3));
};

/**
 * Converts a declination D:M:S into decimal degrees
 * @param {*} value 
 * @returns result in degrees
 */
var DMS2DecDeg = function(value, sep=":"){
    let sign = 1;

    if(value[0] == "-"){
        sign = -1;
    }

    const parts = value.split(sep);

    if(parts.length !== 3){
        throw("Invalid format for D:M:S value");
    }else{
        const a = parseFloat(parts[0]);
        const b = parseFloat(parts[1]) * (1/60);
        const c = parseFloat(parts[2]) * (1/3600);
        
        return (a + b + c)*sign;
    }
};

/*
 ** A class to convert pixels into a string expressing a right ascension.
 **
 ** The constructor establishes the transformation pixels -> HMS
 ** with the given parameter ra0pix, ra1pix ( interval in pixels )
 ** and ra0, ra1 ( the same interval in decimal degrees)
 */
class RaLabelFormatter {
    static enter(what) {
        console.group(this.name + "." + what);
    }

    static exit() {
        console.groupEnd();
    }

    // calculer les vraies coordonnées dans le bon système
    // (sans interpolation linéaire)
    constructor(ra0pix, ra1pix, ra0, ra1) {
        RaLabelFormatter.enter(this.constructor.name);
        this.ra0pix = ra0pix;
        this.ra1pix = ra1pix;
        this.ra0 = ra0;
        this.ra1 = ra1;
        this.slope = ((this.ra1 - this.ra0) / (this.ra1pix - this.ra0pix));
        this.format = this.format.bind(this);
        RaLabelFormatter.exit();
    }

    /*
     ** Returns the string representation of a RA in HMS given its input value in pixels.
     */
    format(rapix) {
        //RaLabelFormatter.enter(this.format.name);
        var res = this.ra0 + (rapix - this.ra0pix) * this.slope;
        //RaLabelFormatter.exit();
        return DecDeg2HMS(res);
    }
};

/*
 ** A class to convert pixels into a string expressing a declination.
 **
 ** The constructor establishes the transformation pixels -> DMS
 ** with the given parameter dec0pix, dec1pix ( interval in pixels )
 ** and dec0, dec1 ( the same interval in decimal degrees)
 */
class DecLabelFormatter {
    static enter(what) {
        console.group(this.name + "." + what);
    }

    static exit() {
        console.groupEnd();
    }

    // calculer les vraies coordonnées dans le bon système
    // (sans interpolation linéaire)
    constructor(dec0pix, dec1pix, dec0, dec1) {
        DecLabelFormatter.enter(this.constructor.name);
        this.dec0pix = dec0pix;
        this.dec1pix = dec1pix;
        this.dec0 = dec0;
        this.dec1 = dec1;
        this.slope = ((this.dec1 - this.dec0) / (this.dec1pix - this.dec0pix));
        this.format = this.format.bind(this);
        DecLabelFormatter.exit();
    }

    format(decpix) {
        //DecLabelFormatter.enter(this.format.name);
        var res = this.dec0 + (decpix - this.dec0pix) * this.slope;
        //DecLabelFormatter.exit();
        return DecDeg2DMS(res);
    }
};

/*
 ** A function which returns the units of a spectrum resulting
 ** from the summation of the pixels values on each plane of a range of RA-DEC planes in a FITS cube.
 */
/*
function summedPixelsSpectrumUnit(unit) {
  switch (unit) {
    case "Jy/beam":
      return "Jy";
      break;

    case "erg/s/cm^2/A/arcsec^2":
      return "erg/s/cm^2/A";
      break;

    default:
      return "";
  }
}
*/

/*
 ** A function which returns the units of a 2D array resulting from
 ** the summation of a range of RA-DEC planes in a FITS cube.
 */
function summedPixelsUnit(unit) {
    switch (unit) {
        case "Jy/beam":
            return "Jy/beam * km/s";


        case "erg/s/cm^2/A/arcsec^2":
            return "erg/s/cm^2/arcsec^2";

        case "K (Ta*)":
            return "K.km/s";

        default:
            return "";
    }
}

/*
 ** A function which returns a factor
 ** for the display of physical quantity
 ** The returned values are based on experience rather
 ** than on a purely rational approach
 */
function unitRescale(unit) {
    switch (unit) {
        case "K (Ta*)":
            return 1.0;

        case "Jy/beam":
            return 1.0;

        case "erg/s/cm^2/A/arcsec^2":
            //     return 1e18;
            return 1.;

        case "erg/s/cm^2/A":
            //      return 1e12;
            return 1.;

        default:
            return 1.0;

    }
}

/*
 ** A function which sums the values of a array
 ** between two indices i0 (included ) and i1 ( excluded )
 ** and returns the sum multiplied by a coefficient coeff.
 */
function sumArr(arr, i0, i1, coeff) {
    console.trace();
    console.log(arr);
    

    i0 = Math.max(0, i0);
    i1 = Math.min(arr.length - 1, i1);
    console.log(i0 +  "   " + i1);
    console.log(arr.slice(i0, i1 + 1))
    if (i0 > i1)[i0, i1] = [i1, i0];

    try{
        return coeff * arr.slice(i0, i1 + 1).reduceRight(function(a, b) { return a + b; });
    }catch(exception){
        if(arr.slice(i0, i1 + 1).length === 0)
            alert(`Selected interval [${i0}, ${i1+1}] is out of bound`);
        else
            alert(exception);
    }
        
}

/**
 * Utility.
 * A function which parses a string into an array of floats. The string is expected
 * to be a comma separated list of textual representation of decimal numbers.
 * A range [min, max[ can be provided,  then the values are considered valif if and only if they lie in that range.
 *
 * @param {string} s a comma separated list of textual numbers representations
 * @param {number[2]} range if provided values are checked to lie in the range
 * 
 * @returns {number[]|undefined} an array of float numbers or undefined.
*/
function str2FloatArray(s, range = false) {
    let x = s.split(",");
    let w = undefined;
    let result = undefined;

    let y = x.map(function(z) { return parseFloat(z) });
    if (range) {
        w = y.map(function(t) { return (!isNaN(t) && (range[0] <= t) && (t < range[1])) });
    } else {
        w = y.map(function(t) { return true; });
    }

    if (w.reduce(function(accum, u) { return accum && u }, true)) {
        result = y;
    }

    return result;
}

/*

 ** A function which creates a document fragment out of an HTML string and appends it to the content of an existing element.
 ** The HTML string is assumed to describe a single element ( e.g. one signle div, p, etc. ).
 ** Returns the created element.
 */
function createAndAppendFromHTML(html, element) {
    var template = document.createElement('template');
    template.innerHTML = html.trim();
    $(element).append(template.content.cloneNode(true));
    return element.lastChild;
}

function redshift2Velocity(z) {
    // SPEED_OF_LIGHT in m/s
    return z * (SPEED_OF_LIGHT / 1000);
}

function velocity2Redshift(v) {
    // SPEED_OF_LIGHT in m/s
    return v / (SPEED_OF_LIGHT / 1000);
}

/*
 ** Two functions to log when a function is entered and exited
 */
function ENTER() {
    var caller = ENTER.caller;
    if (caller == null) {
        result = "_TOP_";
    } else {
        result = caller.name + ": entering";
    }
    console.log(result + ": entering");
}

function EXIT() {
    var caller = EXIT.caller;
    if (caller == null) {
        result = "_TOP_";
    } else {
        result = caller.name + ": exiting";
    }
    console.log(result + ": exiting");
}

function inRange(x, xmin, xmax) {
    return ((x - xmin) * (x - xmax) <= 0);
}

/*
 
  ** Frequency <-> Velocity derivations.
  **
  ** From https://www.iram.fr/IRAMFR/ARN/may95/node4.html
  */

/*
  ** No verification is done on the values of restfreq and frequency. Both are expected to have realistic values.
  ** frequency and restfreq are expected to by in the same unit (i.e. both in HZ or both in GHZ)
  ** The result is in m/s.
  
 var f2v = function (frequency, restfreq, crval3) {
  return (SPEED_OF_LIGHT * (restfreq - frequency) / restfreq) + crval3;
}*/

var f2v = function(frequency, restfreq, vcenter) {
    return (SPEED_OF_LIGHT * (restfreq - frequency) / restfreq) + vcenter;
}


/*
 ** No verification is done on the values of restfreq and velocity. Both are expected to have realistic values.
 */
var v2f = function(velocity, restfreq, vcenter) {
    return restfreq * (1 - (velocity - vcenter) / SPEED_OF_LIGHT)
}

/*
 ** Frequency to wavelength
 */
var f2lambda = function(frequency) {
    return SPEED_OF_LIGHT / frequency;
}

/*
 ** Revert 1D array.
 */
var revertArray = function(a) {
    var result = [];
    for (let i = 0; i < a.length; i++) {
        result.push(a[a.length - i - 1]);
    }
    return result;
}

/*
 ** Round *original* number to *round* numbers after 0.
 */
var round = function(original, round) {
    var i = 0;
    var r = 1;
    while (i < round) {
        ++i;
        r *= 10;
    }
    return Math.round(original * r) / r;
};

/*
 ** Coordinate tabulators
 **
 **  - crval : value at reference point
 **  - cdelt : increment of abscissa
 **  - crpix : coordinate of reference point
 **  - n : tabulate at point of coordinate n
 */
var linearTabulator = function(crval, cdelt, crpix, n) {
    return crval + (n - crpix) * cdelt;
}

/**
 * Generates a random string 
 * (from https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript)
 * @param {*} s 
 * @returns 
 */
var hashCode = function(s) {
    return s.split("").reduce(function(a, b) { a = ((a << 5) - a) + b.charCodeAt(0); return a & a }, 0);
}

var degToArcsec = function(deg) {
    return deg * 3600;
}

function degToRad(deg) {
    return deg * (Math.PI / 180);
}

/**
 * get the rest value for a frequency according to a redshift or a velocity
 * returns a deep-copy of the array with shifted line values
 * @param {*} frequency 
 * @param {*} value  value used to calculate shift 
 * @param {*} type  redshift or velocity
 */
function unshift(frequency, value, type) {
    let result = null;
    if (type == REDSHIFT_SHIFT_TYPE) {
        result = frequency * (1 + value);
    } else if (type == VELOCITY_SHIFT_TYPE) {
        result = frequency * (1 + (value / (SPEED_OF_LIGHT / 10 ** 3)));
    } else {
        result = frequency;
    }
    return result;
}

/**
 * calculate the shifted value of all the lines in the given array
 * returns a deep-copy of the array with shifted line values
 * @param {*} lines  array of lines
 * @param {*} value  value used to calculate shift 
 * @param {*} type  redshift or velocity
 */
function shift(frequency, value, type) {
    let result = null;
    if (type == REDSHIFT_SHIFT_TYPE) {
        result = frequency / (1 + value);
    } else if (type == VELOCITY_SHIFT_TYPE) {
        result = frequency / (1 + (value / (SPEED_OF_LIGHT / 10 ** 3)));;
    } else {
        alert("Unknown data type");
    }
    return result;
}

/**
 * Compute the search radius in degrees from coordinates
 * to get matching sources in NED
 * 
 * if radius_value is FOV : Returns the max value between abs(RAmax-RAmin) and abs(Decmax-Decmin)
 * else : return radius_value
 * 
 * radius_value : radius in arcmin or "fov"
 * fits_header : FITS_HEADER object
 */
function getRadiusInDegrees(radius_value, fits_header) {
    if (radius_value === "fov") {
        // compute dec min and max in degrees
        let pix2dec = new PixelToDECConverter(fits_header.crval2, fits_header.crpix2, fits_header.cdelt2);
        let dec_min = pix2dec.convert(0);
        let dec_max = pix2dec.convert(fits_header.naxis2);

        //compute ra min and max in degrees
        let pix2ra = new PixelToRAConverter(fits_header.crval1, fits_header.crpix1, fits_header.cdelt1);
        let ra_min = pix2ra.convert(0, dec_min);
        let ra_max = pix2ra.convert(fits_header.naxis1, dec_max);

        // search radius
        let dist_in_ra = Math.abs(ra_min - ra_max);
        let dist_in_dec = Math.abs(ra_min - ra_max);

        return Math.max([dist_in_ra], dist_in_dec)
    } else {
        return radius_value;
    }
}

/**
 * Returns value of L'_Line in K.km/s.pc2, then Mgas is a function of L'_Line
 * @param {*} sline    surface selected in summed pixel spectrum in Jy.km/s
 * @param {*} nu_obs   line observed frequency in GHz
 * @param {*} dl       distance in Mpc (from NED)
 * @param {*} z        redshift
 * @returns 
 */
function getLpLine(sline, nu_obs, dl, z){
    return 3.25e7 * sline * (nu_obs**-2) * (dl**2) * ((1+z)**-3);
}

/**
*Converts K in cm-1 (for energy values)
*  @param {*} energy_in_K
*
*/
var KToCm = function(energy_in_K){
    return energy_in_K * 0.695;
  }
  
  /**
  *Converts cm-1 in K (for energy values)
*  @param {*} energy_in_cm
  */
  var cmToK = function(energy_in_cm){
    return energy_in_cm / 0.695;
  }

export {
    hashCode,
    degToRad,
    degToArcsec,
    linearTabulator,
    round,
    revertArray,
    /*f2lambda,*/ v2f,
    f2v,
    inRange,
    velocity2Redshift,
    redshift2Velocity,
    /*ENTER, EXIT, createAndAppendFromHTML, str2FloatArray,*/ sumArr,
    createAndAppendFromHTML,
    str2FloatArray,
    unitRescale,
    summedPixelsUnit,
    DecLabelFormatter,
    RaLabelFormatter,
    DecDeg2DMS,
    DecDeg2HMS,
    HMS2DecDeg,
    DMS2DecDeg,
    PixelToRAConverter,
    PixelToDECConverter,
    AbsToPixelConverter,
    PixelToAbsConverter,
    DECDDtoPixelConverter,
    RADDtoPixelConverter,
    getRadiusInDegrees,
    shift, 
    unshift,
    getLpLine,
    KToCm,
    cmToK
}