Source: public/javascript/modules/fitsheader.js

import { degToArcsec, degToRad, unitRescale } from "./utils.js";
import { header, product } from "./init.js";
import { Constants } from "./constants.js";

/**
 *  Class representing a FITS header.
 * @typedef {Object} FitsHeader
 */
class FitsHeader {
    /**
     * Note : during initialization, cunit3 is set to M/S if this is a GILDAS spectrum and CTYPE3 is VRAD
     * 
     * @param {object} header   FITS file header
     * @param {string} product  Name of product
     */
    constructor(header, product) {
        this.product = product;
        this.object = header.OBJECT;

        // number of axis
        this.naxis = parseInt(header.NAXIS);

        // number of points of nth axis
        this.naxis1 = parseInt(header.NAXIS1);
        this.naxis2 = parseInt(header.NAXIS2);
        this.naxis3 = parseInt(header.NAXIS3);
        this.naxis4 = "NAXIS4" in header ? parseInt(header.NAXIS4) : undefined;

        // array location of the reference point in pixels of nth axis
        this.crpix1 = parseFloat(header.CRPIX1);
        this.crpix2 = parseFloat(header.CRPIX2);
        this.crpix3 = parseFloat(header.CRPIX3);

        this.crpix = [parseFloat(header.CRPIX1), parseFloat(header.CRPIX2)];

        // coordinate value at reference point of nth axis
        this.crval1 = parseFloat(header.CRVAL1);
        this.crval2 = parseFloat(header.CRVAL2);
        this.crval3 = parseFloat(header.CRVAL3);

        // name of nth axis
        this.ctype1 = "CTYPE1" in header ? header.CTYPE1.toUpperCase().trim() : undefined;
        this.ctype2 = "CTYPE2" in header ? header.CTYPE2.toUpperCase().trim() : undefined;

        this.ctype3 = "CTYPE3" in header ? header.CTYPE3.toUpperCase().trim() : undefined;
        if(this.ctype3 === 'FREQUENCY' || this.ctype3 === "FREQ-W2F")
            this.ctype3 = 'FREQ';

        if(this.ctype1 !== undefined){
            this.projectionType = this.ctype1.slice(-3);
            console.log(this.projectionType);
        }
        else{
            this.projectionType = undefined;
        }

        this.cunit1 = "CUNIT1" in header ? header.CUNIT1.toUpperCase().trim() : undefined;
        this.cunit2 = "CUNIT2" in header ? header.CUNIT2.toUpperCase().trim() : undefined;

        // coordinate increment at reference point in degrees on axis 1/2/3
        this.cdelt1 = "CDELT1" in header ? parseFloat(header.CDELT1) : undefined;
        this.cdelt2 = "CDELT2" in header ? parseFloat(header.CDELT2) : undefined;
        this.cdelt3 = "CDELT3" in header ? parseFloat(header.CDELT3) : undefined;

        // Image units (K, Jy/beam, etc)  
        this.bunit = header.BUNIT;

        this.instrume = "INSTRUME" in header ? header.INSTRUME : undefined;
        this.origin = "ORIGIN" in header ? header.ORIGIN : undefined;

        this.specsys = "SPECSYS" in header ? header.SPECSYS : undefined;

        // Major axis of the clean beam  
        this.bmaj = "BMAJ" in header ? parseFloat(header.BMAJ) : undefined;
        // Minor axis of the clean beam  
        this.bmin = "BMIN" in header ? parseFloat(header.BMIN) : undefined;
        // Position angle of the clean beam 
        this.bpa = "BPA" in header ? parseFloat(header.BPA) : undefined;

        this.restfreq = "RESTFRQ" in header ? header.RESTFRQ : undefined;
        this.line = "LINE" in header ? header.LINE : undefined;

        this.pc1_1 = 'PC1_1' in header ? header.PC1_1 : undefined;
        this.pc2_1 = 'PC2_1' in header ? header.PC2_1 : undefined;

        this.velolsr = "VELO-LSR" in header ? header["VELO-LSR"] : undefined;

        // Rotation angle
        this.crota2 = "CROTA2" in header ? parseFloat(header.CROTA2) : undefined;

        if ((this.isGILDAS()) && (this.ctype3 === "VRAD")) {
            this.cunit3 = "M/S";
        } else {
            this.cunit3 = "CUNIT3" in header ? header.CUNIT3.toUpperCase().trim() : undefined;
        }

        this.cdelt3prim = this.getCdelt3prim();

        this.width = this.naxis1;
        this.height = this.naxis2;
        this._setSquaredDimensions();
    }

    /*
     *  Set the same values for width and height
     */
    _setSquaredDimensions() {
        if (this.width > this.height) {
            this.height = this.width;
        } else if (this.width < this.height) {
            this.width = this.height;
        }
    }

    getCentralPixelPosition(){
        return [Math.round(this.naxis1 / 2), Math.round(this.naxis2 / 2)];
    }

    /**
     * 
     * @returns velolsr in m/s
     */
    getVeloLsr(){
        return this.velolsr; //* unitRescale("m/s");
    }

    /**
     * Returns number of jansky per kelvins from bmin, bmaj, restfreq and crval3 values
     * @returns {float}
     */
    janskyPerKelvin() {
        let kb = 1.380649e-23;
        let bmin = degToRad(this.bmin);
        let bmaj = degToRad(this.bmaj);

        let lambda = null;

        if (this.isGILDAS()) {
            lambda = Constants.SPEED_OF_LIGHT / this.restfreq;
        } else {
            lambda = Constants.SPEED_OF_LIGHT / this.crval3;
        }

        let omega = Math.PI * bmin * bmaj / 4 / Math.log(2);
        let step1 = 2 * kb * omega / (lambda * lambda);
        let result = step1 / 1e-26;

        return result;
    }

    /**
     * Returns the dimension of the fits file
     * @returns {int}
     */
    dimensionality() {
        let result;
        if (this.naxis >= 3 && this.naxis1 > 1 && this.naxis2 > 1 && this.naxis3 >= 1) {
            result = 3;
        } else if (this.naxis == 2 && this.naxis1 > 1 && this.naxis2 > 1) {
            result = 2;
        }
        return result;
    }

    /**
     * Returns number of kelvin per jansky ( inverse value of jansky per kelvins)
     * @returns {float}
     */
    kelvinPerJansky() {
        let jperk = this.janskyPerKelvin();
        let result = 1.0 / jperk;
        return result;
    }

    /**
     * Returns a title to be displayed above the spectrum
     * @returns {string} HTML formatted text
     */
    getSpectrumTitle() {
        let title = "<span>" +
            Number(degToArcsec(this.bmin)).toFixed(2) + "\" x " + "</span>" +
            "<span>" + Number(degToArcsec(this.bmaj)).toFixed(2) + "\", " + "</span>" +
            "<span>" + "PA " + Number(this.bpa).toFixed(1) + "°" + "</span>";

        if (!this.isSpectrumInK()) {
            title = title + "<span>, " + Number(this.janskyPerKelvin()).toExponential(2) + " Jy/K</span>";
        }
        return title;
    }

    /**
     * Returns a velocity centered to 0 if ctype3 is frequence or radial velocity
     * or else returns crval3
     * @returns {float}
     */
    getVCenter() {
        let vcenter = this.crval3;
        if (this.ctype3 === "FREQ" || this.ctype3 === "VRAD") {
            vcenter = 0;
        }
        return vcenter;
    }

    /**
     * Returns a summary of opened fits file (product name and list of axis)
     * @param {boolean} withFrequency if true, frequency is displayed
     * @returns {string} HTML formatted text
     */
    getFitsSummary(withFrequency) {
        let FITSSummary = `<strong> ${this.product} </strong> - 
            OBJECT = <strong> ${this.object} </strong> - 
            NAXIS = <strong> ${this.naxis} </strong> - 
            NAXIS1 = <strong> ${this.naxis1} </strong> - 
            NAXIS2 = <strong> ${this.naxis2} </strong> - 
            NAXIS3 = <strong> ${this.naxis3} </strong>`;


        if(this.cdelt3 !== undefined && this.cunit3 !== undefined){
            let value = (this.cdelt3*unitRescale(this.cunit3)).toFixed(3);
            let unit =  "";
            if(this.cunit3 === "KM/S"){
                unit = this.cunit3;
            } else if(this.cunit3 === "M/S"){
                unit = "km/s";
            } else if(this.cunit3 === "HZ"){
                unit = "MHz";
            }else if(this.cunit3 === "MHZ"){
                unit = "MHz";
            }

            FITSSummary += `- CDELT3  =  <strong>${value} ${unit}</strong>`;
        }


        if (this.line !== undefined) {
            FITSSummary += `- LINE  =  <strong>${this.line} GHz</strong>`;
        }

        if (withFrequency && this.restfreq !== undefined) {
            FITSSummary += `- RESTFRQ  =  <strong>${(this.restfreq / 1e9).toFixed(3)} GHz</strong>`;
        }

        return FITSSummary;
    }

    getCdelt3prim() {
        let result = 0.0;
        if ((this.isGILDAS()) && (this.ctype3 === "VRAD")) {
            result = Math.abs(this.cdelt3) * Constants.UNIT_FACTOR[this.cunit3] / Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT[this.ctype3]];
        } else if (this.isCASA()) {
            if (this.ctype3 === "FREQ") {
                //ALTRVAL
                result = Constants.SPEED_OF_LIGHT / 1000.0 * Math.abs(this.cdelt3) / this.crval3;
            } else {
                result = Math.abs(this.cdelt3) * Constants.UNIT_FACTOR[this.cunit3] / Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT[this.ctype3]];
            }
        } else if (this.isSITELLE()) {
            if (this.cdelt1 && this.cdelt2) {
                result = this.cdelt3 / Math.abs(this.cdelt1 * this.cdelt2 * 3600.0 * 3600.0);
            } else {
                result = this.cdelt3;
            }
        } else if (this.isMUSE()) {
            result = 1;
        }
        return result;
    }

    /**
     * Returns true if spectrum is in Kelvin
     * @returns {boolean}
     */
    isSpectrumInK() {
        if (Constants.KELVIN_UNITS.has(this.bunit) || this.bunit.startsWith("K"))
            return true;
        /*else {
            if (this.bunit.startsWith("K")) {
                let error = "bunit is probably Kelvin but not recognized as such.";
                alert(error);
                throw new Error(error);
            }
        }*/
        return false;
    }

    /**
     * Returns true if field instrume is SITELLE
     * @returns {boolean}
     */
    isSITELLE() {
        if (this.instrume === "SITELLE")
            return true;
        return false;
    }

    /**
     * Returns true if field instrume is MUSE
     * @returns {boolean}
     */
    isMUSE() {
        if (this.instrume === "MUSE")
            return true;
        return false;
    }

    /**
     * Returns true if field origin starts with CASA
     * @returns {boolean}
     */
    isCASA() {
        if (this.origin.startsWith("CASA"))
            return true;
        return false;
    }

    /**
     * Returns true if instrument is MEERKAT
     * @returns {boolean}
     */
     isMEERKAT() {
        if (this.instrume.toUpperCase() === "MEERKAT")
            return true;
        return false;
    }

    /**
     * Returns true if instrument is MIRI
     * @returns {boolean}
     */
    isMIRI() {
        if (this.instrume.toUpperCase() === "MIRI")
            return true;
        return false;
    }

    /**
     * Returns true if field origin starts with GILDAS
     * @returns {boolean}
     */
    isGILDAS() {
        //this is the general method to check that a file comes from GILDAS
        if (this.origin.startsWith("GILDAS"))
            return true;
        return false;
    }

    /**
     * Returns true if field origin starts with "Miriad fits"
     * @returns {boolean}
     */
    isMIRIAD() {
        //this is the general method to check that a file comes from Miriad
        if (this.origin.startsWith("Miriad fits"))
            return true;
        return false;
    }

    isNENUFAR(){
        if (this.instrume.toUpperCase() === "NENUFAR")
            return true;
        return false;
    }


}

let FITS_HEADER = new FitsHeader(header, product);
export { FITS_HEADER };