Source: visualize/projection/Wavelength.js

/**
 *  References:
 *  1. "Representations of spectral coordinates in FITS", by E. W. Greisen1,M.R.Calabretta2, F.G.Valdes3,
 *     and S. L. Allen. A&A Volume 446, Number 2, February I 2006
 *  2. "Representations of world coordinates in FITS" A&A 395, 1061-1075 (2002) DOI: 10.1051/0004-6361:20021326
 *     E. W. Greisen1 - M. R. Calabretta2
 *  3. https://www.aanda.org/articles/aa/full/2002/45/aah3859/aah3859.html,
 *
 */

export const PLANE  = 'PLANE';
export const LINEAR = 'LINEAR';
export const LOG = 'LOG';
export const F2W = 'F2W';
export const V2W = 'V2W';
export const TAB = 'TAB';


export const WAVE= 'WAVE';
export const AWAV= 'AWAV';


/**
 * @global
 * @public
 * @typedef {Object} WaveLengthData
 *
 * @prop {string} algorithm
 * @prop {string} wlType
 * @prop {Number} N
 * @prop {Array.<number>} r_j
 * @prop {Array.<number>} pc_3j
 * @prop {Number} s_3
 * @prop {Number} lambda_r
 * @prop {number} restWAV
 * @prop {number} crpix
 * @prop {number} cdelt
 * @prop {number} crval
 */



const wlTypes= {
    [PLANE] : {
        getWaveLength : getWaveLengthPlane,
        implemented : true,
    },
    [LINEAR] : {
        getWaveLength : getWaveLengthLinear,
        implemented : true,
    },
    [LOG] : {
        getWaveLength : getWaveLengthLog,
        implemented : true,
    },
    [F2W] : {
        getWaveLength : getWaveLengthNonLinear,
        implemented : true,
    },
    [V2W] : {
        getWaveLength : getWaveLengthNonLinear,
        implemented : true,
    },
    [TAB] : {
        getWaveLength : getWaveLengthTable,
        implemented : true,
    }
};


export function getWavelength(pt, cubeIdx, wlData) {
    const {algorithm, wlType}= wlData;
    if (wlTypes[algorithm] && wlTypes[algorithm].implemented && wlTypes[algorithm].getWaveLength) {
        const wl=  wlTypes[algorithm].getWaveLength(pt,cubeIdx,wlData);
        if (wlType===WAVE) return wl;
        if (wlType===AWAV) return convertAirToVacuum(wl);
        return wl;
    }
}
//TODO check here to see the cubeIdx??
function getWaveLengthPlane(ipt, cubeIdx, wlData) {
    const {crpix, crval, cdelt} = wlData;
    //pixel count starts from 1 to naxisn
    const wl = crval + ( cubeIdx + 1  - crpix ) * cdelt;
    return wl;
}

export function isWLAlgorithmImplemented(wlData) {
    const {algorithm}= wlData;
    return wlTypes[algorithm].implemented;
}

/**
 *  calculate the air wavelength and then convert to vacuum wavelength
 *  @param {number} wl
 *  @return {number}
 */
const convertAirToVacuum= (wl) => 1 + 10**-6 * ( 287.6155 + 1.62887/wl**2 + 0.01360/wl**4);


/**
 *
 * The algorithm is based on the fact that the spectral data is stored in the naxis3.  The naxis1, ctype1:ra
 * naxis2, ctype2: dec, naxis3, ctype3 : wavelength
 * Thus, the k value referenced in the paper is 3 here
 *
 *  omega  = x_3 = s_3* Sum [ m3_j * (p_j - r_j) ]
 *
 *  lamda = lambda_r + omega
 *
 *  lambda_r : is the reference value,  given by CRVAL3
 *
 *
 *
 * @param {ImagePt} ipt
 * @param {number} cubeIdx
 * @param {Object} wlData
 * @param {Number} wlData.N
 * @param {Array.<number>} wlData.r_j
 * @param {Array.<number>} wlData.pc_3j
 * @param {Number} wlData.s_3
 * @param wlData.lambda_r
 * @return {number}
 */
function getWaveLengthLinear(ipt, cubeIdx, wlData) {
    const {N,  r_j, pc_3j, s_3,  lambda_r}= wlData;
    return lambda_r + getOmega(getPixelCoords(ipt,cubeIdx),  N, r_j, pc_3j, s_3);
}

/**
 *
 * The algorithm is based on the fact that the spectral data is stored in the naxis3.  The naxis1, ctype1:ra
 * naxis2, ctype2: dec, naxis3, ctype3 : wavelength
 * Thus, the k value referenced in the paper is 3 here
 *
 *  omega  = x_3 = s_3* Sum [ m3_j * (p_j - r_j) ]
 *
 *  lamda = lambda_r * exp (omega/s_3)
 *
 *  lambda_r : is the reference value,  given by CRVAL3
 *
 * @param {ImagePt} ipt
 * @param {number} cubeIdx
 * @param {WaveLengthData} wlData
 * @param {Number} wlData.N
 * @param {Array.<number>} wlData.r_j
 * @param {Array.<number>} wlData.pc_3j
 * @param {Number} wlData.s_3
 * @param {Number} wlData.lambda_r
 * @return {number}
 */
function getWaveLengthLog(ipt, cubeIdx, wlData) {
    const {N,  r_j, pc_3j, s_3,  lambda_r}= wlData;
    const omega= getOmega(getPixelCoords(ipt,cubeIdx),  N, r_j, pc_3j, s_3);
    return lambda_r* Math.exp(omega/lambda_r);
}

/**
 *
 * The algorithm is based on the fact that the spectral data is stored in the naxis3.  The naxis1, ctype1:ra
 * naxis2, ctype2: dec, naxis3, ctype3 : wavelength
 * Thus, the k value referenced in the paper is 3 here
 *
 *  omega  = x_3 = s_3* Sum [ m3_j * (p_j - r_j) ]
 *
 *  lamda = lambda_r * exp (omega/s_3)
 *
 *  lambda_r : is the reference value,  given by CRVAL3
 *
 * @param {ImagePt} ipt
 * @param {number} cubeIdx
 * @param {WaveLengthData} wlData
 * @param {Number} wlData.N
 * @param {Array.<number>} wlData.r_j
 * @param {Array.<number>} wlData.pc_3j
 * @param {Number} wlData.s_3
 * @param {Number} wlData.lambda_r
 * @param {string} wlData.algorithm
 * @param {number} wlData.restWAV
 * @return {number}
 */
function getWaveLengthNonLinear(ipt, cubeIdx, wlData) {

    const {N,  r_j, pc_3j, s_3,  lambda_r, algorithm= 'F2W', restWAV=0}= wlData;
    let lamda= NaN;
    const omega= getOmega(getPixelCoords(ipt,cubeIdx),  N, r_j, pc_3j, s_3);


    switch (algorithm){
        case 'F2W':
            lamda =lambda_r *  lambda_r/(lambda_r - omega);
            break;
        case 'V2W':
            const lamda_0 = restWAV;
            const b_lamda = ( Math.pow(lambda_r, 4) - Math.pow(lamda_0, 4) + 4 *Math.pow(lamda_0, 2)*lambda_r*omega )/
            Math.pow((Math.pow(lamda_0, 2) + Math.pow(lambda_r, 2) ), 2);
            lamda = lamda_0 - Math.sqrt( (1 + b_lamda)/(1-b_lamda) );
            break;
    }
    return lamda;
}

const getArrayDataFromTable= (table, rowIdx, columnName) => undefined;  // todo implement


/**
 *
 * The pre-requirement is that the FITS has three axies, ra (1), dec(2) and wavelength (3).
 * Therefore the keyword for i referenced in the paper is 3
 * @param {ImagePt} ipt
 * @param {number} cubeIdx
 * @param {WaveLengthData} wlData
 * @param {Number} wlData.N
 * @param {Array.<number>} wlData.r_j
 * @param {Array.<number>} wlData.pc_3j
 * @param {Number} wlData.s_3
 * @param {Number} wlData.lambda_r
 * @return {number}
 */
function getWaveLengthTable(ipt, cubeIdx, wlData) {


    const {N, r_j, pc_3j, s_3, cdelt, lambda_r, wlTable, ps3_1, ps3_2}= wlData;

    const omega= getOmega(getPixelCoords(ipt,cubeIdx),  N, r_j, pc_3j, s_3);
    const psi_m = !isNaN(cdelt)? lambda_r + cdelt* omega : lambda_r + omega;
    if (!wlTable) return NaN;


    //read the cell coordinate from the FITS table and save to two one dimensional arrays
    const coordData = getArrayDataFromTable(wlTable,0, ps3_1 || 'COORDS');
    if (!coordData) return NaN;

    const indexData =  getArrayDataFromTable(wlTable,0, ps3_2 || 'INDEX');
    if (!indexData) return coordData[psi_m];

    const psiIndex = searchIndex(indexData, psi_m);
    if (isNaN(psiIndex) || psiIndex===-1) return NaN; // No index found in the index array, gamma is undefined, so is coordinate

    const gamma_m = calculateGamma_m(indexData, psi_m, psiIndex);
    if (isNaN(gamma_m)) return NaN;

    return  coordData[psiIndex] + (gamma_m - psiIndex) * (coordData[psiIndex+1] - coordData[psiIndex]);
}


/**
 *
 * @param {Array.<number>} indexVec
 * @param {number} psi
 * @param {number} idx
 * @return {*}
 */
function calculateGamma_m(indexVec, psi, idx) {
    /* Scan the indexing vector, (Ψ1, Ψ2,...), sequen-tially starting from the first element, Ψ1,
     * until a successive pair of index values is found that encompass ψm (i.e. such that Ψk ≤ ψm ≤ Ψk+1
     * for monotonically increasing index values or Ψk ≥ ψm ≥ Ψk+1 for monotonically decreasing index values
     * for some k). Then, when Ψk  Ψk+1, interpolate linearly on the indices
     */


    if (idx!==-1 && indexVec[idx-1]!==indexVec[idx]){
        // Υm = k + (ψm − Ψk) / (Ψk+1− Ψk)
        return  idx + (psi-indexVec[idx-1])/(indexVec[idx]-indexVec[idx-1]);
    }
    else {
        return NaN; // No index found in the index array, gamma is undefined, so is coordinate
    }

}





function isSorted(intArray) {

    const end = intArray.length-1;
    let counterAsc = 0;
    let counterDesc = 0;

    for (let i = 0; i < end; i++) {
        if (intArray[i] < intArray[i+1]) counterAsc++;
        else if(intArray[i] > intArray[i+1]) counterDesc++;
    }
    if(counterDesc===0) return 1;
    else if (counterAsc===0) return -1;
    else return 0;
}




function searchIndex(indexVec, psi) {
    /*Scan the indexing vector, (Ψ1, Ψ2,...), sequen-tially starting from the first element, Ψ1,
      until a successive pair of index values is found that encompass ψm (i.e. such that Ψk ≤ ψm ≤ Ψk+1
      for monotonically increasing index values or Ψk ≥ ψm ≥ Ψk+1 for monotonically decreasing index values
      for some k). Then, when Ψk  Ψk+1, interpolate linearly on the indices
     */

    const sort = isSorted(indexVec); //1:ascending, -1: descending, 0: not sorted
    if (sort===0){
        return NaN; //The vector index array has to be either ascending or descending
    }


    for (let i=1; i<indexVec.length; i++){
        if (sort===1 && indexVec[i-1]<=psi  && psi<=indexVec[i]){
            return i;
        }
        if (sort===-1 && indexVec[i-1]>=psi && psi>=indexVec[i]){
            return i;

        }
    }
    return -1;
}





/**
 *
 * The algorithm is based on the fact that the spectral data is stored in the naxis3.  The naxis1, ctype1:ra
 * naxis2, ctype2: dec, naxis3, ctype3 : wavelength
 * Thus, the k value referenced in the paper is 3 here
 *
 *                      N
 *  omega  = x_3 = s_3* ∑ [ m3_j * (p_j - r_j) ]
 *                      j=1
 *
 *  lamda = lambda_r + omega
 *
 *  lambda_r : is the reference value,  given by CRVAL3
 *
 * Get pixel at given "ImagePt" coordinates
 *   "ImagePt" coordinates have 0,0 lower left corner of lower left pixel
 *   (from the reference paper above) Note that integer pixel numbers refer to the center of the pixel in each axis,
 *   so that, for example, the first pixel runs from pixel number 0.5 to pixel number 1.5 on every axis.
 *   Note also that the reference point location need not be integer nor need it even occur within the image.
 *   The original FITS paper (Wells et al. 1981) defined the pixel numbers to be counted from 1 to NAXIS j ($ \geq $1)
 *   on each axis in a Fortran-like order as presented in the FITS image[*].
 *
 *   This method get the coordinates for given image point (p1, p2, p3) where imagePt=(p1, p2)
 *   If it is only one plan, the p3=1 since the axis is count staring from 1.
 *
 * @param {ImagePt} ipt
 * @param {number} cubeIdx
 * @return {Array.<number>}
 */
function getPixelCoords(ipt, cubeIdx) {

    //As noted above, the pixel is counting from 1 to naxis j where (j=1, 2,... naxis).  Since the p = Math.round(ipt.x - 0.5)
    //starts from 0.  Thus, 1 is added here.
    //pixel numbers refer to the center of the pixel, so we subtract 0.5, see notes above
    const p0 = Math.round(ipt.x - 0.5) + 1 ;
    const p1 = Math.round(ipt.y - 0.5) + 1 ;
    return [p0, p1, cubeIdx+1]; //since cubeIdx starts from 0
}

/**
 *
 * The algorithm is based on the fact that the spectral data is stored in the naxis3.  The naxis1, ctype1:ra
 * naxis2, ctype2: dec, naxis3, ctype3 : wavelength
 * Thus, the k value referenced in the paper is 3 here
 *                      N
 *  omega  = x_3 = s_3* ∑ [ m3_j * (p_j - r_j) ]
 *                      j=1
 *
 *  N: is the dimensionality of the WCS representation given by NAXIS or WCSAXES
 *  p_j: is the pixel coordinates
 *  r_j: is the pixel coordinate of the reference point given by CRPIXJ where j=1... N, where N=3 in our case
 *  s_3: is a scaling factor given by CDELT3
 *  m3_j: is a linear transformation matrix given either by PC3_j or CD3_j
 *
 *
 * @param pixCoords
 * @param {number} N
 * @param {Array.<number>} r_j
 * @param {Array.<number>} pc_3j
 * @param {Number} s_3
 * @return number
 */
function getOmega(pixCoords, N,  r_j, pc_3j, s_3){
    //if (!pc_3j || !r_j ) return s_3*(pixCoords[0]+pixCoords[1]);

    let omega =0.0;
    for (let i=0; i<N; i++){
        omega += s_3 * pc_3j[i] * (pixCoords[i]-r_j[i]);
    }
    return omega;
}