Source: visualize/projection/Projection.js

/*
 * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
 */

import {get} from 'lodash';
import {clone} from '../../util/WebUtil.js';
import {makeImagePt, makeProjectionPt, makeWorldPt} from '../Point.js';
import {computeDistance} from '../VisUtil.js';
import {CoordinateSys} from '../CoordSys.js';
import {AitoffProjection} from './AitoffProjection.js';
import {NCPProjection} from './NCPProjection.js';
import {ARCProjection} from './ARCProjection.js';
import {GnomonicProjection} from './GnomonicProjection.js';
import {SansonFlamsteedProjection } from './SansonFlamsteedProjection.js';
import {LinearProjection} from './LinearProjection.js';
import {CartesianProjection} from './CartesianProjection.js';
import {OrthographicProjection} from './OrthographicProjection.js';
import {CylindricalProjection} from './CylindricalProjection.js';
import {PlateProjection} from './PlateProjection.js';
import {TpvProjection} from './TpvProjection.js';
import {Projection as AladinProjection} from '../../externalSource/aladinProj/AladinProjections.js';



const unspecifiedProject= () => null;
const unimplementedProject= () => null;

export const GNOMONIC     = 1001; // TESTED
export const ORTHOGRAPHIC = 1002; // TESTED
export const NCP          = 1003;
export const AITOFF       = 1004; // TESTED
export const CAR          = 1005;
export const LINEAR       = 1006;
export const PLATE        = 1007; // TESTED
export const ARC          = 1008;
export const SFL          = 1009; // TESTED
export const CEA          = 1010;
export const TPV          = 1011;
export const UNSPECIFIED  = 1998; // TESTED
export const UNRECOGNIZED = 1999; // TESTED



export const ALADIN_SIN     = 5;



const projTypes= {
	[GNOMONIC] : {
		name: 'GNOMONIC',
		fwdProject: GnomonicProjection.fwdProject,
		revProject: GnomonicProjection.revProject,
		implemented : true,
		wrapping : false
	},
    [TPV] : {
        name: 'TPV',
        fwdProject: TpvProjection.fwdProject,
        revProject: TpvProjection.revProject,
        implemented : true,
        wrapping : false
    },
	[ORTHOGRAPHIC] : {
		name: 'ORTHOGRAPHIC',
		fwdProject: OrthographicProjection.fwdProject,
		revProject: OrthographicProjection.revProject,
		implemented : true,
		wrapping : false
	},
	[NCP] : {
		name: 'NCP',
		fwdProject: NCPProjection.fwdProject,
		revProject: NCPProjection.revProject,
		implemented : true,
		wrapping : false
	},
	[ARC] : {
		name: 'ARC',
		fwdProject: ARCProjection.fwdProject,
		revProject: ARCProjection.revProject,
		implemented : true,
		wrapping : false
	},
	[AITOFF] : {
		name: 'AITOFF',
		fwdProject: AitoffProjection.fwdProject,
		revProject: AitoffProjection.revProject,
		implemented : true,
		wrapping : true
	},
	[CAR] : {
		name: 'CAR',
		fwdProject: CartesianProjection.fwdProject,
		revProject: CartesianProjection.revProject,
		implemented : true,
		wrapping : false
	},

	[CEA] : {
		name: 'CEA',
		fwdProject: CylindricalProjection.fwdProject,
		revProject: CylindricalProjection.revProject,
		implemented : true,
		wrapping : false
	},
	[SFL] : {
		name: 'SFL',
		fwdProject: SansonFlamsteedProjection.fwdProject,
		revProject: SansonFlamsteedProjection.revProject,
        implemented : true,
        wrapping : true
	},
	[LINEAR] : {
		name: 'LINEAR',
		fwdProject: LinearProjection.fwdProject,
		revProject: LinearProjection.revProject,
		implemented : true,
		wrapping : false
	},
	[PLATE] : {
		name: 'PLATE',
		fwdProject: PlateProjection.fwdProject,
		revProject: PlateProjection.revProject,
		implemented : true,
		wrapping : false
	},
	[UNSPECIFIED] : {
		name: 'UNSPECIFIED',
		fwdProject: unspecifiedProject,
		revProject: unspecifiedProject,
		implemented : false,
		wrapping : false
	},
	[UNRECOGNIZED] : {
		name: 'UNRECOGNIZED',
		fwdProject: unimplementedProject,
		revProject: unimplementedProject,
		implemented : false,
		wrapping : false
	},

    [ALADIN_SIN] : {
        name: 'ALADIN_SIN',
        fwdProject : fwdAladinSinProject,
        revProject : revAladinSinProject,
        implemented : true,
        wrapping : false,
    },


};


const translateProjectionName= (maptype) => get(projTypes, [maptype,'name'],'UNRECOGNIZED');
const isImplemented= (header) => get(projTypes, [header.maptype, 'implemented'],false);
const isWrappingProjection= (header) => get(projTypes, [header.maptype, 'wrapping'],false);




/**
 * Convert 'ProjectionPt' coordinates to World coordinates
 *    'ProjectionPt' coordinates have 0,0 in center of lower left pixel
 *    (same as 'Skyview Screen' coordinates)
 * @param x double precision in 'ProjectionPt' coordinates
 * @param y double precision in 'ProjectionPt' coordinates
 * @param header the header info
 * @param coordSys used for the worldPt
 */
function getWorldCoordsInternal(x, y, header, coordSys)  {
	if (!projTypes[header.maptype]) return null;
	const pt = projTypes[header.maptype].fwdProject( x, y, header);
	return pt ? makeWorldPt(pt.x, pt.y, coordSys) : null;
}

/**
 *   Convert World coordinates to 'Skyview Screen' coordinates
 *    'Skyview Screen' coordinates have 0,0 in center of lower left pixel
 * @param ra double precision
 * @param dec double precision
 * @param header the header info
 * @return ImagePt with X,Y in 'Skyview Screen' coordinates
 */
function getImageCoordsInternal(ra, dec, header) {
	if (!projTypes[header.maptype]) return null;
	const image_pt = projTypes[header.maptype].revProject( ra, dec, header);
	return image_pt;
}




export class Projection {

	/**
	 * @summary Contains data about the state of a plot.
	 * This object is never created directly if is always instantiated from the json sent from the server.
	 * @param {Object} header data from the fits file
	 * @param {CoordinateSys} coordSys
	 * Note - constructor should never be call directly
	 *
     *
	 * @prop {object} header
	 * @prop {number} scale1
	 * @prop {number} scale2
	 * @prop {number} pixelScaleArcSec
	 * @prop {CoordinateSys} coordSys
	 * @public
	 */
    constructor(header, coordSys)  {
        this.header= clone(header);
        this.coordSys= coordSys;
        const {crpix1,crpix2, cdelt1}= header;
        if (!cdelt1 && crpix1 && crpix2) {
            const projCenter = this.getWorldCoords(crpix1 - 1, crpix2 - 1);
            const oneToRight = this.getWorldCoords(crpix1, crpix2 - 1);
            const oneUp = this.getWorldCoords(crpix1 - 1, crpix2);
            if (oneToRight && oneUp) {
                this.header.cdelt1 = - computeDistance( projCenter, oneToRight);
                this.header.cdelt2 = computeDistance( projCenter, oneUp);
            }

        }
        this.pixelScaleDeg = Math.abs(this.header.cdelt1);
        this.pixelScaleArcSec = this.pixelScaleDeg * 3600.0;
    }

	/**
	 *
	 * @return {number}
	 * @public
	 */
	getPixelScaleDegree() { return this.pixelScaleDeg; }


	/**
	 * @summary the scale of an image pixel in arcsec
	 * @return {number}
	 * @public
	 */
	getPixelScaleArcSec() { return this.pixelScaleArcSec; }


	/**
	 * @summary convert from a world point to a image point
	 * @param ra
	 * @param dec
	 * @return {ImagePt}
	 * @public
	 */
    getImageCoords(ra, dec) { return getImageCoordsInternal(ra, dec, this.header, false); }

	/**
	 * @summary convert from a image point to a world point
	 * @param x
	 * @param y
	 * @return {WorldPt}
	 * @public
	 */
	getWorldCoords( x, y) { return getWorldCoordsInternal(x, y, this.header, this.coordSys, false); }

	/**
	 * @return {boolean} true, if this projection is implemented
	 * @public
	 */
	isImplemented() { return isImplemented(this.header); }

	/**
	 * @return {boolean} true, if this projection is specified
	 * @public
	 */
	isSpecified() { return this.header.maptype!==UNSPECIFIED; }

	/**
	 * @return {boolean} true, if this projection is wrapping, e.g. AITOFF
	 * @public
	 */
	isWrappingProjection() { return isWrappingProjection(this.header); }

	/**
	 * @return true, if this projection is wrapping, e.g. AITOFF
	 * @public
	 */
    getProjectionName() { return translateProjectionName(this.header.maptype); }
}

export function makeProjection(projJSON) {
	return new Projection(projJSON.header, CoordinateSys.parse(projJSON.coorindateSys));
}

export function makeProjectionNew(header, csys) {
    return new Projection(header, csys);
}


function fwdAladinSinProject(x, y, header) {

    const widthHalf = header.crpix1;
    const heightHalf = header.crpix2;
    const yshift = 20;

    const height = header.crpix2 * 2;
    const yFlip = height - y;


    let pX = (x - widthHalf) / widthHalf;
    if (pX === -1) pX = -.99;
    else if (pX === 1) pX = .99;
    const pY = (yFlip - heightHalf) / (heightHalf + yshift);


    const p = new AladinProjection(header.crval1, header.crval2);
    p.setProjection(AladinProjection.PROJ_SIN);
    try {
        const pt = p.unproject(pX, pY);
        if (!pt) return null;
        const retPt = makeProjectionPt(pt.ra, pt.dec);
        return retPt;
    } catch (e) {
        return null;
    }
}

function revAladinSinProject(ra, dec,  header) {
        const widthHalf= header.crpix1;
        const heightHalf= header.crpix2;
        const width= header.crpix1*2;
        const height= header.crpix2*2;
        const yshift= 20;

        const p= new AladinProjection(header.crval1, header.crval2);
        p.setProjection(AladinProjection.PROJ_SIN);
        const pt= p.project(ra,dec);
        if (!pt) return null;
        const x= pt.X;
        const y= pt.Y;

        const imX= x*widthHalf +widthHalf;
        const imY= y*(heightHalf+yshift) + heightHalf;
        return makeImagePt(  imX, height - imY);
}