Source: visualize/CsysConverter.js

/*
 * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
 */
import CoordinateSys from './CoordSys.js';
import {get} from 'lodash';
import VisUtil from './VisUtil.js';
import {makeRoughGuesser} from './ImageBoundsData.js';
import Point, {makeImageWorkSpacePt, makeImagePt,
               makeScreenPt, makeWorldPt, makeDevicePt, isValidPoint} from './Point.js';
import {Matrix} from 'transformation-matrix-js';
import {getPixScaleDeg, isHiPS} from './WebPlot.js';
import {makeFitsImagePt, makeZeroBasedImagePt} from './Point';


function convertToCorrect(wp) {
    if (!wp) return null;
    const csys= wp.getCoordSys();
    if (csys===CoordinateSys.SCREEN_PIXEL) {
        return makeScreenPt(wp.x, wp.y);
    }
    else if (csys===CoordinateSys.PIXEL) {
        return makeImagePt(wp.x, wp.y);
    }
    return wp;
}


const MAX_CACHE_ENTRIES = 38000; // set to never allows the cache array over 48000 with a 80% load factor


/**
 * This class is for conversion
 * Some include a Image coordinate system, a world coordinate system, and a screen
 * coordinate system.
 * <ul>
 * <li>The image coordinate system is the coordinate system of the data. 
 * <li>The world coordinate system is the system that the data represents
 *        (i.e. the coordinate system of the sky)
 * <li>Screen coordinates are the pixel values of the screen.
 * </ul>
 * @public
 */
export class CysConverter {

    /**
     * @param {object} plot
     * @param {object} [altAffTrans] an alternate transform to use
     * @param {String} [whichWCS] choose the wcs (must be defined) the default WCS is the empty string
     * @public
     */
    constructor(plot, altAffTrans, whichWCS='')  {
        this.plotImageId= plot.plotImageId;
        this.plotId = plot.plotId;
        this.plotState= plot.plotState;
        this.dataWidth= plot.dataWidth;
        this.dataHeight= plot.dataHeight;
        this.projection= plot.allWCSMap[whichWCS];
        this.zoomFactor= plot.zoomFactor;
        this.imageCoordSys= plot.imageCoordSys;
        this.inPlotRoughGuess= null;
        this.conversionCache= plot.conversionCache;
        this.screenSize= plot.screenSize;
        this.affTrans= altAffTrans || plot.affTrans;
        this.viewDim= plot.viewDim;
        this.plotType= plot.plotType;
        this.header= plot.header;
    }

    // isRotated() { return !Matrix.from(this.affTrans).isIdentity(); }

    /**
     *
     * @param {WorldPt} wp world point
     * @param {ImagePt} imp Image Point
     */
    putInConversionCache(wp, imp) {
        if (this.conversionCache.size<MAX_CACHE_ENTRIES) {
            this.conversionCache.set(wp.toString(), imp);
        }
    }

//========================================================================================
//----------------------------- pointIn Methods  -----------------------------------------
//========================================================================================

    /**
     * Determine if a world point is in data Area of the plot and is not null
     * @param {WorldPt} iwPt the point to test.
     * @returns {boolean} true if it is in the data boundaries, false if not.
     */
    imagePointInData(iwPt) {
        let retval= false;
        if (iwPt && this.pointInPlot(iwPt)) {
            const ipt= this.getImageCoords(iwPt);
            const x= ipt.x;
            const y= ipt.y;
            retval= (x >= 0 && x <= this.dataWidth && y >= 0 && y <= this.dataHeight );
        }
        return retval;
    }


    /**
     * Determine if a image point is in the plot boundaries and is not null.
     * @param {Point} pt the point to test.
     * @returns {boolean} true if it is in the boundaries, false if not.
     */
    pointInData(pt) {
        if (!isValidPoint(pt)) return false;
        return this.imagePointInData(this.getImageWorkSpaceCoords(pt));
    }


    /**
     * This method returns false it the point is definitely not in plot.  It returns true if the point might be in the plot.
     * Used for tossing out points that we know that are not in plot without having to do all the math.  It is much faster.
     * @param {WorldPt} wp
     * @return {boolean} true in we guess it might be in the bounds, false if we know that it is not in the bounds
     */
    pointInPlotRoughGuess(wp) {

        if (!this.projection.isWrappingProjection()) {
            if (!this.inPlotRoughGuess) this.inPlotRoughGuess= makeRoughGuesser( this);
            return this.inPlotRoughGuess(wp);
        }
        else {
            return true;
        }
    }


    /**
     * Determine if a image point is in the plot boundaries and is not null.
     * @param {ImagePt} ipt the point to test.
     * @returns {boolean} true if it is in the boundaries, false if not.
     */
    imageWorkSpacePtInPlot(ipt) {
        if (!ipt) return false;
        const {x,y}= ipt;
        return (x >= 0 && x <= this.dataWidth && y >= 0 && y <= this.dataHeight );
    }

    /**
     * Determine if a image point is in the plot boundaries and is not null
     * @param {Point} pt the point to test.
     * @return {boolean} true if it is in the boundaries, false if not.
     */
    pointInPlot(pt) {
        let retval= false;
        if (!isValidPoint(pt)) {
            return false;
        }
        else if (pt.type===Point.W_PT) {
            retval= this.pointInPlotRoughGuess(pt);
            if (retval) {
                retval= this.imageWorkSpacePtInPlot(this.getImageWorkSpaceCoords(pt));
            }
        }
        else  {
            retval= this.imageWorkSpacePtInPlot(this.getImageWorkSpaceCoords(pt));
        }
        return retval;
    }


    pointOnDisplay(pt) {
        if (!isValidPoint(pt)) return false;
        const devPt= this.getDeviceCoords(pt);
        if (!devPt) return false;
        const {x,y}= devPt;
        const {width,height}= this.viewDim;
        let retval= (x>=0 && y>=0 && x<=width && y<=height);
        if (retval) {
            const minSize= Math.min(width,height);
            const {width:sWidth,height:sHeight}= this.screenSize;
            if (sWidth< minSize || sHeight< minSize) {
               retval= this.pointInPlot(pt);
            }
        }
        return retval;
    }

    /* check if the point within display range for either regular image or hips */
    pointInView(pt) {
        if (!pt) return false;
        if (isHiPS(this)) {
            const inViewDim = (pt) => {
                const devPt = this.getDeviceCoords(pt);

                return (devPt) && (devPt.x >= 0 && devPt.x < this.viewDim.width) &&
                    (devPt.y >= 0 && devPt.y < this.viewDim.height);
            };

            return inViewDim(pt);
        } else {
            return this.pointInData(pt);
        }

    };

    /* check it the point is viewable by checking if the point is within range of viewDim for
        hips image or if the point is within plot range for fits image
     */
    isPointViewable(pt) {
        return isHiPS(this) ? this.pointInView(pt) : this.pointOnDisplay(pt);
    };

//========================================================================================
//----------------------------- End pointIn Methods  -------------------------------------
//========================================================================================

//========================================================================================
//========================================================================================
//========================================================================================
//----------------------------- Conversion Methods Begin --------------------------------
//========================================================================================
//========================================================================================

//========================================================================================
//----------------------------- Conversion to ImageWorkSpacePt Methods  ------------------
//========================================================================================

    /**
     * Return the ImageWorkSpacePt coordinates for a given Pt
     * @param {object} pt the point to translate
     * @param {number} [altZoomLevel] only use this parameter it you want to compute the point for a zoom level that
     *                 if different than what the plotted zoom level
     * @return ImageWorkSpacePt the image workspace coordinates
     */
    getImageWorkSpaceCoords(pt, altZoomLevel) {
        if (!isValidPoint(pt)) return null;


        let retval= null;
        switch (pt.type) {
            case Point.IM_WS_PT:
                retval= pt;
                break;
            case Point.FITS_IM_PT:
            case Point.ZERO_BASED_IM_PT:
                const imPt= this.getImageCoords(pt);
                retval= makeImageWorkSpacePt(imPt.x, imPt.y);
                break;
            case Point.IM_PT:
                retval= makeImageWorkSpacePt(pt.x, pt.y);
                break;
            case Point.SPT:
                retval= this.makeIWPtFromSPt(pt,altZoomLevel);
                break;
            case Point.DEV_PT:
                retval= this.makeIWPtFromSPt(this.getScreenCoords(pt),altZoomLevel);
                break;
            case Point.W_PT:
                const checkedPt= convertToCorrect(pt);
                if (checkedPt.type===Point.W_PT) {
                    retval= this.getImageWorkSpaceCoords(this.getImageCoords(checkedPt));
                }
                else {
                    retval= this.getImageWorkSpaceCoords(checkedPt, altZoomLevel);
                }
                break;
        }
        return retval;
    }

    /**
     * @description  Return a ImageWorkspacePt from the screen point.
     * @param {ScreenPt} screenPt
     * @param {number} [altZoomLevel]
     */
    makeIWPtFromSPt(screenPt, altZoomLevel) {
        if (!screenPt) return null;
        const zoom= altZoomLevel || this.zoomFactor;
        return makeImageWorkSpacePt(screenPt.x / zoom, this.dataHeight-screenPt.y/zoom);
    }

//========================================================================================
//----------------------------- Conversion to ImageSpacePt Methods  ----------------------
//========================================================================================


    getFitsStandardImagePtFromInternal(pt) {
        const imPt= this.getImageCoords(pt);
        return makeFitsImagePt(imPt.x+.5, imPt.y+.5);
    }

    getZeroBasedImagePtFromInternal(pt) {
        const {ltv1,ltv2}= CysConverter.getLtv(this.header);
        const imPt= this.getImageCoords(pt);
        return makeZeroBasedImagePt(imPt.x-.5-ltv1, imPt.y-.5-ltv2);
    }

    static convertFitsStandardImagePtToInternalImage(pt) {
        return makeImagePt(pt.x-.5, pt.y-.5);
    }

    static getLtv(header) {
        const {LTV1,LTV2, CRVAL1A,CRVAL2A}= header;
        let ltv1, ltv2;
        if (!isNaN(Number(get(LTV1,'value'))) && !isNaN(Number(get(LTV2,'value')))) {
            ltv1= Number(get(LTV1,'value'));
            ltv2= Number(get(LTV2,'value'));
        }
        else if (!isNaN(Number(get(CRVAL1A,'value'))) && !isNaN(Number(get(CRVAL2A,'value')))) {
            ltv1= -Number(get(CRVAL1A,'value'));
            ltv2= -Number(get(CRVAL2A,'value'));
        }
        else {
            ltv1= 0;
            ltv2= 0;
        }
        return {ltv1,ltv2};
    }

    /**
     * Return the ImagePt coordinates given Pt
     * @param {object} pt the point to translate
     * @return {ImagePt} the image coordinates
     */
    getImageCoords(pt) {
        if (!isValidPoint(pt)) return null;

        let retval = null;

        switch (pt.type) {
            case Point.IM_WS_PT:
                retval= CysConverter.makeIPtFromIWPt(pt);
                break;
            case Point.SPT:
            case Point.DEV_PT:
                retval= CysConverter.makeIPtFromIWPt(this.getImageWorkSpaceCoords(pt));
                break;
            case Point.IM_PT:
                retval = pt;
                break;
            case Point.FITS_IM_PT:
                retval= CysConverter.makeIPtFromFitsImPt(pt);
                break;
            case Point.ZERO_BASED_IM_PT:
                retval= this.makeIPtFromxZeroImPt(pt);
                break;
            case Point.W_PT:
                retval = this.getImageCoordsFromWorldPt(pt);
                break;
        }
        return retval;
    }


    /**
     * return a ImagePoint from a ImageWorkspace point
     * @param {ImageWpt} iwPt
     * returns {ImagePt}
     */
    static makeIPtFromIWPt(iwPt) {
        return iwPt ? makeImagePt(iwPt.x, iwPt.y) : null;
    }

    static makeIPtFromFitsImPt(pt) {
        return pt ? makeImagePt(pt.x-.5, pt.y-.5) : null;
    }

    makeIPtFromxZeroImPt(pt) {
        if (!pt) return null;
        const {ltv1,ltv2}= CysConverter.getLtv(this.header);
        return makeImagePt(pt.x+.5+ltv1, pt.y+.5+ltv2);
    }


    /**
     * @desc Return the image coordinates given a WorldPt class.
     * @param {WorldPt} wpt the class containing the point in sky coordinates
     * @returns {ImagePt} the translated coordinates
     */
    getImageCoordsFromWorldPt(wpt) {
        if (!wpt) return null;

        let retval;
        const checkedPt= convertToCorrect(wpt);
        if (checkedPt.type===Point.W_PT) {
            const originalWp= wpt;
            retval= this.conversionCache.get(checkedPt.toString() );
            if (!retval) {
                if (this.imageCoordSys!==wpt.getCoordSys()) {
                    wpt= VisUtil.convert(wpt,this.imageCoordSys);
                }
                const projPt= this.projection.getImageCoords(wpt.getLon(),wpt.getLat());
                retval= projPt ? makeImagePt( projPt.x+ 0.5 ,  projPt.y+ 0.5) : null;
                this.putInConversionCache(originalWp,retval);
            }
        }
        else {
            retval= this.getImageCoords(checkedPt);
        }
        return retval;
    }

//========================================================================================
//----------------------------- Conversion to DevicePt Methods  --------------------------
//========================================================================================

    /**
     * Return the device coordinates given a Pt
     * @param pt the point to translate
     * @param {number} [altZoomLevel] only use this parameter it you want to compute the point for a zoom level that
     *                 if different than what the plotted zoom level
     * @param {object} [altTransform] an alternate affine transform to use
     * @return {DevicePt} the device coordinates
     */
    getDeviceCoords(pt, altZoomLevel, altTransform) {
        if (!isValidPoint(pt)) return null;

        let retval = null;

        switch (pt.type) {
            case Point.DEV_PT:
                retval= pt;
                break;
            case Point.FITS_IM_PT:
            case Point.ZERO_BASED_IM_PT:
            case Point.IM_WS_PT:
            case Point.IM_PT:
            case Point.W_PT:
                if (!altZoomLevel && !altTransform) {
                    retval= this.makeDevicePtFromSp(this.getScreenCoords(pt)); // normal case
                }
                else {
                    // special case, used with thumbnail: ignore Viewport
                    retval= this.makeDevicePtFromSp(this.getScreenCoords(pt, altZoomLevel), altTransform);
                }

                break;
            case Point.SPT:
                retval= this.makeDevicePtFromSp(pt, altTransform);
                break;
                break;
        }
        return retval;
    }


    getDevicePtCoordsOptimize(wpt, retPt) {
        const success= this.getScreenCoordsOptimize(wpt,retPt);
        if (!success  || !this.affTrans) return false;
        const {x,y}= Matrix.from(this.affTrans).applyToPoint(retPt.x,retPt.y);
        retPt.x= x;
        retPt.y= y;
        retPt.type= Point.DEV_PT;
        return true;
    }

    /**
     *
     * @param {Object} sp ScreenPt
     * @param {Object} altTransform
     * @return {DevicePt}
     */
     makeDevicePtFromSp(sp, altTransform) {
        if (!sp || !this.affTrans) return null;
        const {x,y}= Matrix.from(altTransform || this.affTrans).applyToPoint(sp.x,sp.y);
        return makeDevicePt(x,y);
    }

//========================================================================================
//----------------------------- Conversion to ScreenPt Methods  --------------------------
//========================================================================================

    /**
     * Return the screen coordinates given Pt
     * @param pt the point to translate
     * @param {number} [altZoomLevel] only use this parameter it you want to compute the point for a zoom level that
     *                 if different than what the plotted zoom level
     * @return ScreenPt the screen coordinates
     */
    getScreenCoords(pt, altZoomLevel) {
        if (!isValidPoint(pt)) return null;

        let retval = null;

        switch (pt.type) {
            case Point.IM_WS_PT:
                retval= this.makeSPtFromIWPt(pt, altZoomLevel);
                break;
            case Point.SPT:
                retval= pt;
                break;
            case Point.FITS_IM_PT:
            case Point.ZERO_BASED_IM_PT:
                const imPt= this.getImageCoords(pt);
                retval= this.makeSPtFromIWPt(this.getImageWorkSpaceCoords(imPt), altZoomLevel);
                break;
            case Point.IM_PT:
                retval= this.makeSPtFromIWPt(this.getImageWorkSpaceCoords(pt), altZoomLevel);
                break;
            case Point.DEV_PT:
                retval = this.makeSpFromDevPt(pt);
                break;
            case Point.W_PT:
                const checkedPt= convertToCorrect(pt);
                if (checkedPt) {
                    if (checkedPt.type===Point.W_PT) {
                        retval= this.makeSPtFromIWPt(this.getImageWorkSpaceCoords(checkedPt),altZoomLevel);
                    }
                    else {
                        retval= this.getScreenCoords(checkedPt, altZoomLevel);
                    }
                }
                break;
        }
        return retval;
    }

    makeSpFromDevPt(devPt) {
         if (!devPt || !this.affTrans) return null;
         const {x,y}= Matrix.from(this.affTrans).inverse().applyToPoint(devPt.x,devPt.y);
         return makeScreenPt(x,y);
     }




    /**
     * @desc An optimized conversion of WorldPt to Screen point.
     * @param {WorldPt} wpt a world pt
     * @param {ScreenPt} retPt mutable returned Screen Point, this object will be written to
     * @return {boolean} success or failure
     */
    getScreenCoordsOptimize(wpt, retPt) {
        let success= false;

        let imagePt= this.conversionCache.get(wpt.toString());

        if (!imagePt) {
            const csys= wpt.getCoordSys();
            if (csys===CoordinateSys.SCREEN_PIXEL) {
                retPt.x= wpt.x;
                retPt.y= wpt.y;
                success= true;
            }
            else if (csys===CoordinateSys.PIXEL) {
                this.makeScreenPtFromImPtOptimized(wpt,retPt);
            }
            else {
                const originalWp= wpt;
                if (this.imageCoordSys!==wpt.getCoordSys()) {
                    wpt= VisUtil.convert(wpt,this.imageCoordSys);
                }

                const  proj_pt= this.projection.getImageCoords(wpt.getLon(),wpt.getLat());
                if (proj_pt) {
                    const imageX= proj_pt.x  + 0.5;
                    const imageY= proj_pt.y  + 0.5;

                    imagePt= makeImagePt(imageX,imageY);
                    this.putInConversionCache(originalWp, imagePt);

                    this.makeScreenPtFromImPtOptimized(imagePt,retPt);
                    success= true;
                }
            }
        }
        else {
            this.makeScreenPtFromImPtOptimized(imagePt,retPt);
            success= true;
        }

        return success;
    }


    makeScreenPtFromImPtOptimized(imagePt,retPt) {
        if (!imagePt) return null;
        // convert image to image workspace
        const imageWorkspaceX= imagePt.x;
        const imageWorkspaceY= imagePt.y;

        // convert image workspace to screen
        const zFact= this.zoomFactor;
        retPt.x= imageWorkspaceX*zFact;
        retPt.y= (this.dataHeight - imageWorkspaceY) *zFact;
    }



    /**
     *
     * @param {ImageWorkspacePt} iwpt ImageWorkspacePt
     * @param {number} [altZoomLevel]
     */
    makeSPtFromIWPt(iwpt, altZoomLevel) {
        if (!iwpt) return null;
        const zoom= altZoomLevel || this.zoomFactor;
        return makeScreenPt(iwpt.x*zoom,
                            (this.dataHeight - iwpt.y) *zoom );
    }


//========================================================================================
//----------------------------- Conversion to WorldPt Methods  ---------------------------
//========================================================================================


    /**
     * @desc Return the sky coordinates given a image x (fsamp) and  y (fline)
     * @param {PointPt} pt  the point to convert
     * @param  {CoordinateSys} [outputCoordSys] (optional) The coordinate system to return, default to coordinate system of image
     * @returns {WorldPt} the translated coordinates
     */
    getWorldCoords(pt, outputCoordSys= undefined) {
        if (!isValidPoint(pt)) return null;

        let retval = null;

        switch (pt.type) {
            case Point.IM_PT:
                retval= this.makeWorldPtFromIPt(pt,outputCoordSys);
                break;
            case Point.FITS_IM_PT:
            case Point.ZERO_BASED_IM_PT:
            case Point.IM_WS_PT:
            case Point.SPT:
            case Point.DEV_PT:
                retval= this.makeWorldPtFromIPt(this.getImageCoords(pt),outputCoordSys);
                break;
            case Point.W_PT:
                retval=  (outputCoordSys===pt.getCoordSys()) ? pt : VisUtil.convert(pt, outputCoordSys);
                break;
        }
        return retval;
    }


    makeWorldPtFromIPt( ipt, outputCoordSys) {
        if (!ipt) return null;
        let wpt = this.projection.getWorldCoords(ipt.x - .5 ,ipt.y - .5);
        if (wpt && outputCoordSys!==wpt.getCoordSys()) {
            wpt= VisUtil.convert(wpt, outputCoordSys);
        }
        return wpt;
    }



//========================================================================================
//========================================================================================
//----------------------------- Conversion Methods End -----------------------------------
//========================================================================================
//========================================================================================
//========================================================================================
//========================================================================================
//========================================================================================
//========================================================================================

    coordsWrap(wp1, wp2) {
        if (!wp1 || !wp2) return false;

        let retval= false;
        if (this.projection.isWrappingProjection()) {
            const  worldDist= VisUtil.computeDistance(wp1, wp2);
            const pix= getPixScaleDeg(this);
            const value1= worldDist/pix;

            const ip1= this.getImageWorkSpaceCoords(wp1);
            const ip2= this.getImageWorkSpaceCoords(wp2);
            if (ip1 && ip2) {
                const xDiff= ip1.x-ip2.x;
                const yDiff= ip1.y-ip2.y;
                const imageDist= Math.sqrt(xDiff*xDiff + yDiff*yDiff);
                retval= ((imageDist / value1) > 3);
            }
            else {
                retval= false;
            }
        }
        return retval;
    }

    /**
     *
     * @param {Object} plot - the image
     * @param {object} [altAffTrans] an alternate transform to use
     * @returns {CysConverter}
     */
    static make(plot, altAffTrans) {
        return plot ? new CysConverter(plot, altAffTrans) : null;
    }
} //end of class definition



/**
 *
 * @param pt
 * @return {*}
 * @public
 * @memberof firefly.util.image.CCUtil
 *
 */
function getWorldPtRepresentation(pt) {
    if (!isValidPoint(pt)) return null;

    if (pt.type===Point.W_PT)           return pt;
    else if (pt.type=== Point.IM_WS_PT) return makeWorldPt(pt.x,pt.y, CoordinateSys.PIXEL);
    else if (pt.type=== Point.IM_PT)    return makeWorldPt(pt.x,pt.y, CoordinateSys.PIXEL);
    else if (pt.type=== Point.SPT)      return makeWorldPt(pt.x,pt.y, CoordinateSys.SCREEN_PIXEL);
}

/** part of lowLevelApi
 * @namespace firefly.util.image.CCUtil
 * @public
 */
export const CCUtil = {
    /**
     * Convert to ImageWorkSpace Point
     * @param {WebPlot} plot - the image
     * @param pt
     * @func  getImageWorkSpaceCoords
     * @memberof   firefly.util.image.CCUtil
     * @public
     */
    getImageWorkSpaceCoords : (plot,pt) => CysConverter.make(plot).getImageWorkSpaceCoords(pt),
    
    /**
     *
     * Convert to Image Point
     * @param {WebPlot} plot - the image
     * @param {object} pt - the point to convert
     * @return {ImagePt}
     * @function getImageCoords
     * @memberof  firefly.util.image.CCUtil
     * @public
     */
    getImageCoords: (plot,pt) => CysConverter.make(plot).getImageCoords(pt),
    
    /**
     *
     * Convert to Device point
     */
    /**
     * @param {WebPlot} plot - the image
     * @param {object} pt - the point to convert
     * @public
     * @memberof firefly.util.image.CCUtil
     */
    getDeviceCoords: (plot,pt) => CysConverter.make(plot).getDeviceCoords(pt),
    
    /*
     *
     * Convert to Screen Point
     * */
    /**
     * @param {WebPlot} plot - the image
     * @param {object} pt - the point to convert
     * @function  getScreenCoords
     * @memberof  firefly.util.image.CCUtil
     */
    getScreenCoords: (plot,pt) => CysConverter.make(plot).getScreenCoords(pt),
    
    /**
     *
     * Convert to World Point
     * @param {WebPlot} plot - the image
     * @param  pt - the point to convert
     * @return {WorldPt}
     * @function getWorldCoords
     * @memberof  firefly.util.image.CCUtil
     * @public
     */
    getWorldCoords: (plot,pt) => CysConverter.make(plot).getWorldCoords(pt),

    /**
     *
     * @ignore
     *
     */
    getWorldPtRepresentation
};


export default CysConverter;