Source: visualize/WebPlot.js

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



import {get,isEmpty,isArray} from 'lodash';
import {RequestType} from './RequestType.js';
import {clone} from '../util/WebUtil.js';
import CoordinateSys from './CoordSys.js';
import {makeProjection, makeProjectionNew} from './projection/Projection.js';
import PlotState from './PlotState.js';
import BandState from './BandState.js';
import {makeWorldPt, makeScreenPt} from './Point.js';
import {changeProjectionCenter} from './HiPSUtil.js';
import {CysConverter} from './CsysConverter.js';
import {makeImagePt} from './Point';
import {convert} from './VisUtil.js';
import {parseSpacialHeaderInfo, makeDirectFileAccessData} from './projection/ProjectionHeaderParser.js';
import {UNSPECIFIED, UNRECOGNIZED } from './projection/Projection.js';
import {getImageCubeIdx} from './PlotViewUtil.js';
import {parseWavelengthHeaderInfo} from './projection/WavelengthHeaderParser.js';
import {geAtlProjectionIDs} from './FitsHeaderUtil.js';
import {TAB} from './projection/Wavelength';


export const RDConst= {
    IMAGE_OVERLAY: 'IMAGE_OVERLAY',
    IMAGE_MASK: 'IMAGE_MASK',
    TABLE: 'TABLE',
    WAVELENGTH_TABLE: 'WAVELENGTH_TABLE',
    SUPPORTED_DATATYPES: ['IMAGE_MASK', 'TABLE']
};

const HIPS_DATA_WIDTH=  10000000000;
const HIPS_DATA_HEIGHT= 10000000000;



export const getHiPsTitleFromProperties= (hipsProperties) => hipsProperties.obs_title || hipsProperties.label || 'HiPS';

/**
 * FITS headers keys
 * todo: add more headers
 */

export const PlotAttribute= {

    MOVING_TARGET_CTX_ATTR:   'MOVING_TARGET_CTX_ATTR',

    /**
     * This will probably be a WebMouseReadoutHandler class
     * @see WebMouseReadoutHandler
     */
    READOUT_ATTR: 'READOUT_ATTR',

    READOUT_ROW_PARAMS: 'READOUT_ROW_PARAMS',

    /**
     * This will probably be a WorldPt
     * Used to overlay a target associated with this image
     */
    FIXED_TARGET: 'FIXED_TARGET',

    /**
     *
     */
    INIT_CENTER: 'INIT_CENTER',

    /**
     * This will probably be a double with the requested size of the plot
     */
    REQUESTED_SIZE: 'REQUESTED_SIZE',


    /**
     * This will probably an object represent a rectangle {pt0: point,pt1: point}
     * @See ./Point.js
     */
    SELECTION: 'SELECTION',

    IMAGE_BOUNDS_SELECTION: 'IMAGE_BOUNDS_SELECTION',

    /**
     * setting for outline image, bounds (for FootprintObj) or drawObj, text, textLoc,
     */
    OUTLINEIMAGE_BOUNDS: 'OUTLINEIMAGE_BOUNDS',
    OUTLINEIMAGE_TITLE: 'OUTLINEIMAGE_TITLE',
    OUTLINEIMAGE_TITLELOC: 'OUTLINEIMAGE_TITLELOC',
    OUTLINEIMAGE_DRAWOBJ: 'OUTLINE_OBJ',
    /**
     * This will probably an object to represent a line {pt0: point,pt1: point}
     * @See ./Point.js
     */
    ACTIVE_DISTANCE: 'ACTIVE_DISTANCE',

    SHOW_COMPASS: 'SHOW_COMPASS',

    /**
     * This will probably an object {pt: point}
     * @See ./Point.js
     */
    ACTIVE_POINT: 'ACTIVE_POINT',


    /**
     * This is a String describing why this plot can't be rotated.  If it is defined then
     * rotating is disabled.
     */
    DISABLE_ROTATE_REASON: 'DISABLE_ROTATE_HINT',

    /**
     * what should happen when multi-fits images are changed.  If set the zoom is set to the same level
     * eg 1x, 2x ect.  If not set then flipping should attempt to make the image the same arcsec/screen pixel.
     */
    FLIP_ZOOM_BY_LEVEL: 'FLIP_ZOOM_BY_LEVEL',

    /**
     * what should happen when multi-fits images are changed.  If set the zoom is set to the same level
     * eg 1x, 2x ect.  If not set then flipping should attempt to make the image the same arcsec/screen pixel.
     */
    FLIP_ZOOM_TO_FILL: 'FLIP_ZOOM_TO_FILL',

    /**
     * if set, when expanded the image will be zoom to no bigger than this level,
     * this should be a subclass of Number
     */
    MAX_EXPANDED_ZOOM_LEVEL : 'MAX_EXPANDED_ZOOM_LEVEL',

    /**
     * if set, this should be the last expanded single image zoom level.
     * this should be a subclass of Number
     */
    LAST_EXPANDED_ZOOM_LEVEL : 'LAST_EXPANDED_ZOOM_LEVEL',

    /**
     * if set, must be one of the string values defined by the enum ZoomUtil.FullType
     * currently is is ONLY_WIDTH, WIDTH_HEIGHT, ONLY_HEIGHT
     */
    EXPANDED_TO_FIT_TYPE : 'MAX_EXPANDED_ZOOM_LEVEL',

    /**
     * if true, the readout will be very small
     */
    MINIMAL_READOUT : 'MINIMAL_READOUT',

    TABLE_ROW : 'TABLE_ROW',
    TABLE_ID : 'TABLE_ID',


    UNIQUE_KEY : 'UNIQUE_KEY'

};


/**
 * @global
 * @public
 * @typedef {Object} Dimension
 *
 * @prop {number} width
 * @prop {number} height
 *
 */


/**
 * @global
 * @public
 * @typedef {Object} WebPlot
 *
 * @summary This class contains plot information.
 * Publicly this class operations in many coordinate system.
 * Some include a Image coordinate system, a world coordinate system, and a screen
 * coordinate system.
 *
 * @prop {String} plotId - plot id, id of the plotView, immutable
 * @prop {String} plotImageId,  - plot image id, id of this WebPlot , immutable
 * @prop {Object} serverImage, immutable
 * @prop {String} title - the title
 * @prop {{cubePlane,cubeHeaderAry}} cubeCtx
 * @prop {PlotState} plotState - the plot state, immutable
 * @prop {number} dataWidth - the width of the image data
 * @prop {number} dataHeight - the height of the image data
 * @prop {number} zoomFactor - the zoom factor
 * @prop {string} title - title of the plot
 * @prop {object} webFitsData -  needs documentation
 * @prop {ImageTileData} tileData -  object contains the image tile information
 * @prop {CoordinateSys} imageCoordSys - the image coordinate system
 * @prop {Dimension} screenSize - width/height in screen pixels
 * @prop {Projection} projection - projection routines for this projections
 * @prop {Object} wlData - data object to wave length conversions, if defined then this conversion is available
 * @prop {Object} affTrans - the affine transform
 * @prop {{width:number, height:number}} viewDim  size of viewable area  (div size: offsetWidth & offsetHeight)
 * @prop {Array.<Object>} directFileAccessDataAry - object of parameters to get flux from the FITS file
 *
 * @see PlotView
 */



/**
 * @global
 * @public
 * @typedef {Object} RelatedData
 * @summary overlay data that is associated with the image data
 *
 * @prop {string} relatedDataId - a globally unique id made from the plotId and the dataKey - this is added by the client and does
 * not come from the server
 * @prop {string} dataKey - should be a unique string key an array of plot of RelatedData, that is all
 * RelatedData array entries for a plot should have a unqiue dataKey
 * @prop {string} dataType - one of 'IMAGE_OVERLAY', 'IMAGE_MASK', 'TABLE'
 * @prop {string} desc - user description of the data
 * @prop {Object.<string, string>} searchParams - map of search parameters to get the related data
 * @prop {Object.<string, string>} availableMask - only used for masks- key is the bit number, value is the description
 *
 */



/**
 * @global
 * @public
 * @typedef {Object} ThumbnailImage
 * @summary the thumbnail information
 *
 * @prop {number} width - width of thumbnail
 * @prop {number} height - height of thumbnail
 * @prop {string} url - file key to use in the service to retrieve this tile
 *
 */

/**
 * @global
 * @public
 * @typedef {Object} ImageTile
 * @summary a single image tile
 *
 * @prop {number} width - width of this tile
 * @prop {number} height - height of this tile
 * @prop {number} index - index of this tile
 * @prop {string} url - file key to use in the service to retrieve this tile
 * @prop {number} x - pixel offset of this tile
 * @prop {number} y - pixel offset of this tile
 *
 */

/**
 * @global
 * @public
 * @typedef {Object} HiPSTile
 * @summary a single hips image tile
 *  url computed by: NorderK/DirD/NpixN{.ext}
 *  where
 *  K= nOrder
 *  N= tileNumber
 *  D=(N/10000)*10000 (integer division)
 *
 * @prop {Array.<WorldPt>} corners (maybe) in worldPt
 * @prop {Array.<devpt>} devPtCorners  (maybe) in screenPt (keep here?)
 * @prop {string} url - root url (maybe, don't  know if necessary)
 * @props {number} nOrder (K)
 * @props {number} tileNumber (N)
 *
 */




/**
 * @global
 * @public
 * @typedef {Object} ImageTileData
 * @summary The information about all the image tiles
 *
 * @prop {Array.<ImageTile>} images
 * @prop {number} screenWidth - width of all the tiles
 * @prop {number} screenHeight - height of all the tiles
 * @prop {String} templateName - template name (not used)
 * @prop {number} zfact - zoom factor
 * @prop {ThumbnailImage} thumbnailImage - information about the thumbnail
 *
 */


const relatedIdRoot= '-Related-';

export const isHiPS= (plot) => Boolean(plot && plot.plotType==='hips');
export const isImage= (plot) => Boolean(plot && plot.plotType==='image');
export const isKnownType= (plot) => Boolean(plot && (plot.plotType==='image' || plot.plotType==='hips'));

/**
 *
 * @param plotId
 * @param plotType
 * @param asOverlay
 * @param imageCoordSys
 * @return {WebPlot}
 */
function makePlotTemplate(plotId, plotType, asOverlay, imageCoordSys) {
    return {
        plotId,
        plotType,
        imageCoordSys,
        asOverlay,
        plotImageId     : plotId+'---NEEDS___INIT',
        tileData    : undefined,
        relatedData     : undefined,
        plotState : undefined,
        projection: undefined,
        dataWidth : undefined,
        dataHeight : undefined,
        title : '',
        plotDesc        : '',
        dataDesc        : '',
        webFitsData     : undefined,
        //=== Mutable =====================
        screenSize: {width:0, height:0},
        zoomFactor: 1,
        affTrans : undefined,
        viewDim  : undefined,
        attributes: undefined,

        // a note about conversionCache - the caches (using a map) calls to convert WorldPt to ImagePt
        // have this here breaks the redux paradigm, however it still seems to be the best place. The cache
        // is completely transient. If we start serializing the store there should not be much of an issue.
        conversionCache: new Map(),
        //=== End Mutable =====================
    };
}


function processAllSpacialAltWcs(header) {

    const availableAry= geAtlProjectionIDs(header);
    if (isEmpty(availableAry)) return {};

    return availableAry.reduce( (obj, altChar) => {
        const processHeader= parseSpacialHeaderInfo(header, altChar);
        const {maptype}= processHeader;
        if (!maptype || maptype===UNSPECIFIED ||  maptype===UNRECOGNIZED) {
            //todo did not find a spacial, do some other type of wcs computation
        }
        if (processHeader.headerType==='spacial') {
            obj[altChar]= makeProjectionNew(processHeader, processHeader.imageCoordSys);
        }
        else {
            obj[altChar]= undefined;
        }
        return obj;
    }, {});
}

function processAllWavelengthAltWcs(header,wlTable) {
    const availableAry= geAtlProjectionIDs(header);
    if (isEmpty(availableAry)) return {};

    return availableAry.reduce( (obj, altChar) => {
        const wlData= parseWavelengthHeaderInfo(header, altChar, undefined, wlTable);
        if (wlData) obj[altChar]= wlData;
        return obj;
    }, {});
}


/**
 *
 */
export const WebPlot= {

    /**
     *
     * @param {string} plotId
     * @param wpInit init data returned from server
     * @param {object} attributes any attributes to initialize
     * @param {boolean} asOverlay
     * @param {{cubePlane,cubeHeaderAry,relatedData,dataWidth,dataHeight,imageCoordSys}} cubeCtx
     * @return {WebPlot} the plot
     */
    makeWebPlotData(plotId, wpInit, attributes= {}, asOverlay= false, cubeCtx) {

        const relatedData = cubeCtx ? cubeCtx.relatedData : wpInit.relatedData;
        const plotState= PlotState.makePlotStateWithJson(wpInit.plotState);
        const headerAry= !cubeCtx ? wpInit.headerAry : [cubeCtx.cubeHeaderAry[0]];
        const header= headerAry[plotState.firstBand().value];
        const zeroHeader= wpInit.zeroHeaderAry[0];
        const processHeader= parseSpacialHeaderInfo(header,'',zeroHeader);
        const projection= makeProjectionNew(processHeader, processHeader.imageCoordSys);
        const processHeaderAry= !plotState.isThreeColor() ?
                                   [processHeader] :
                                    headerAry.map( (h,idx) => parseSpacialHeaderInfo(h,'',wpInit.zeroHeaderAry[idx]));
        const fluxUnitAry= processHeaderAry.map( (p) => p.fluxUnits);


        const wlRelated= relatedData && relatedData.find( (r) => r.dataType==='WAVELENGTH_TABLE_RESOLVED');

        let wlData= parseWavelengthHeaderInfo(header, '', zeroHeader, get(wlRelated,'table'));
        const allWCSMap= processAllSpacialAltWcs(header);
        const allWlMap= processAllWavelengthAltWcs(header, get(wlRelated,'table'));
        // if (!wlData && Object.values(allWlMap)>0) {
        allWlMap['']= wlData;
        allWCSMap['']= projection;
        if (Object.values(allWlMap).length>0 && get(wlData, 'algorithm')!==TAB) {
            wlData= Object.values(allWlMap)[0];
        }
        const zf= plotState.getZoomLevel();

        for(let i= 0; (i<3); i++) {
            if (headerAry[i]) plotState.get(i).directFileAccessData= makeDirectFileAccessData(headerAry[i], cubeCtx?cubeCtx.cubePlane:-1);
        }

        //original plot state come with header information for getting flux.
        // this is only need for one call, so most time we strip it out.
        // keeping clientFitsHeaderAry allows a way to put back the original
        //todo: i think is could be cached on the server side so we don't need to be send it back and forth
        const directFileAccessDataAry= plotState.getBands().map( (b) => plotState.getDirectFileAccessData(b));

        const imageCoordSys= cubeCtx ? cubeCtx.imageCoordSys : wpInit.imageCoordSys;
        let plot= makePlotTemplate(plotId,'image',asOverlay, CoordinateSys.parse(imageCoordSys));

        const imagePlot= {
            tileData    : wpInit.initImages,
            relatedData     : null,
            header,
            headerAry,
            zeroHeader,
            fluxUnitAry,
            cubeCtx,
            plotState,
            projection,
            wlData,
            allWCSMap,
            allWlMap,
            dataWidth       : cubeCtx ? cubeCtx.dataWidth : wpInit.dataWidth,
            dataHeight      : cubeCtx ? cubeCtx.dataHeight : wpInit.dataHeight,
            title : '',
            plotDesc        : wpInit.desc,
            dataDesc        : wpInit.dataDesc,
            webFitsData     : isArray(wpInit.fitsData) ? wpInit.fitsData : [wpInit.fitsData],
            //=== Mutable =====================
            screenSize: {width:wpInit.dataWidth*zf, height:wpInit.dataHeight*zf},
            zoomFactor: zf,
            attributes,
            directFileAccessDataAry
            //=== End Mutable =====================
        };
        plot= clone(plot, imagePlot);
        plot.cubeIdx= getImageCubeIdx(plot);
        if (relatedData) {
            plot.relatedData= relatedData.map( (d) => clone(d,{relatedDataId: plotId+relatedIdRoot+d.dataKey}));
        }

        if ((!cubeCtx || cubeCtx.cubePlane===0) && wlData && wlData.failWarning)  {
            console.warn(`ImagePlot (${plotId}): Wavelength projection parse error: ${wlData.failWarning}`);
        }

        return plot;
    },

    /**
     *
     * @param plotId
     * @param wpRequest
     * @param hipsProperties
     * @param desc
     * @param zoomFactor
     * @param attributes
     * @param asOverlay
     * @return {WebPlot} the new WebPlot object for HiPS
     */
    makeWebPlotDataHIPS(plotId, wpRequest, hipsProperties, desc, zoomFactor=1, attributes= {}, asOverlay= false) {

        const plotState= PlotState.makePlotState();

        const bandState= BandState.makeBandState();

        bandState.plotRequestTmp= wpRequest;
        bandState.rangeValuesSerialize = null; // todo
        bandState.rangeValues= null; //todo
        plotState.bandStateAry= [bandState,null,null];
        plotState.ctxStr=null;
        plotState.zoomLevel= 1;
        plotState.threeColor= false;
        plotState.colorTableId= 0;

        const hipsCoordSys= getHiPSCoordSysFromProperties(hipsProperties);
        const lon= Number(hipsProperties.hips_initial_ra) || 0;
        const lat= Number(hipsProperties.hips_initial_dec) || 0;
        const projection= makeHiPSProjection(hipsCoordSys, lon,lat);

        const plot= makePlotTemplate(plotId,'hips',asOverlay, hipsCoordSys);

        const hipsPlot= {
            //HiPS specific
            nside: 3,
            hipsUrlRoot: wpRequest.getHipsRootUrl(),
            dataCoordSys : hipsCoordSys,
            hipsProperties,

            /// other
            plotState,
            projection,
            allWCSMap: {'':projection},
            dataWidth: HIPS_DATA_WIDTH,
            dataHeight: HIPS_DATA_HEIGHT,

            title : getHiPsTitleFromProperties(hipsProperties),
            plotDesc        : desc,
            dataDesc        : hipsProperties.label || 'HiPS',
            //=== Mutable =====================
            screenSize: {width:HIPS_DATA_WIDTH*zoomFactor, height:HIPS_DATA_HEIGHT*zoomFactor},
            cubeDepth: Number(get(hipsProperties, 'hips_cube_depth')) || 1,
            cubeIdx: Number(get(hipsProperties, 'hips_cube_firstframe')) || 0,
            zoomFactor,
            attributes,

            //=== End Mutable =====================

        };

        return clone(plot, hipsPlot);
    },


    /**
     *
     * @param {WebPlot} plot
     * @param {object} stateJson
     * @param {ImageTileData} tileData
     * @return {*}
     */
    setPlotState(plot,stateJson,tileData) {
        const plotState= PlotState.makePlotStateWithJson(stateJson);
        const zf= plotState.getZoomLevel();
        const screenSize= {width:plot.dataWidth*zf, height:plot.dataHeight*zf};

        //keep the plotState populated with the fitsHeader information, this is only used with get flux calls
        //todo: i think is could be cached on the server side so we don't need to be send it back and forth
        const {bandStateAry}= plotState;
        for(let i=0; (i<bandStateAry.length);i++) {
            if (bandStateAry[i] && isEmpty(bandStateAry[i].directFileAccessData)) {
                bandStateAry[i].directFileAccessData= plot.directFileAccessDataAry[i];
            }
        }

        plot= {...plot,...{plotState, zoomFactor:zf,screenSize}};
        if (tileData) plot.tileData= tileData;
        return plot;
    },


};


/**
 *
 * @param {CoordinateSys} coordinateSys
 * @param lon
 * @param lat
 * @return {Projection}
 */
function makeHiPSProjection(coordinateSys, lon=0, lat=0) {
    const header= {
        cdelt1: 180/HIPS_DATA_WIDTH,
        cdelt2: 180/HIPS_DATA_HEIGHT,
        maptype: 5,
        crpix1: HIPS_DATA_WIDTH*.5,
        crpix2: HIPS_DATA_HEIGHT*.5,
        crval1: lon,
        crval2: lat

    };
    return makeProjection({header, coorindateSys:coordinateSys.toString()});
}


function getHiPSCoordSysFromProperties(hipsProperties) {
    switch (hipsProperties.hips_frame) {
        case 'equatorial' : return CoordinateSys.EQ_J2000;
        case 'galactic' :   return CoordinateSys.GALACTIC;
        case 'ecliptic' :   return CoordinateSys.ECL_B1950;
    }
    if (!hipsProperties.hips_frame) {
        switch (hipsProperties.coordsys) { // fallback using old style
            case 'C' : return CoordinateSys.EQ_J2000;
            case 'G' : return CoordinateSys.GALACTIC;
            case 'E' : return CoordinateSys.ECL_B1950;
        }
    }
    return CoordinateSys.GALACTIC;
}

function makeHiPSProjectionUsingProperties(hipsProperties, lon=0, lat=0) {
    return makeHiPSProjection(getHiPSCoordSysFromProperties(hipsProperties), lon,lat);
}


/**
 * replace the hips projection if the coordinate system changes
 * @param {WebPlot} plot
 * @param hipsProperties
 * @param {WorldPt} wp
 */
export function replaceHiPSProjectionUsingProperties(plot, hipsProperties, wp= makeWorldPt(0,0)) {
    const projection= makeHiPSProjectionUsingProperties(hipsProperties, wp.x, wp.y);
    const retPlot= clone(plot);
    retPlot.imageCoordSys= projection.coordSys;
    retPlot.dataCoordSys= projection.coordSys;
    retPlot.projection= projection;
    retPlot.allWCSMap= {'':projection};
    return retPlot;
}

/**
 * replace the hips projection if the coordinate system changes
 * @param {WebPlot} plot
 * @param coordinateSys
 * @param {WorldPt} wp
 */
export function replaceHiPSProjection(plot, coordinateSys, wp= makeWorldPt(0,0)) {
    const newWp= convert(wp, coordinateSys);
    const projection= makeHiPSProjection(coordinateSys, newWp.x, newWp.y);
    const retPlot= clone(plot);
    retPlot.imageCoordSys= projection.coordSys;
    //note- the dataCoordSys stays the same
    retPlot.projection= projection;
    retPlot.allWCSMap= {'':projection};
    return retPlot;
}


/**
 * replace the header in the transform of the plot object
 * @param {WebPlot} plot
 * @param {Object} header
 * @return {WebPlot}
 */
export function replaceHeader(plot, header) {
    const retPlot= clone(plot);
    retPlot.conversionCache= new Map();
    retPlot.projection= makeProjection({header:clone(header), coorindateSys:plot.projection.coordSys.toString()});
    retPlot.allWCSMap= {'':retPlot.projection};
    return retPlot;
}

/**
 * Return true if this is a WebPlot obj
 * @param obj
 * @return boolean
 */
export function isPlot(obj) {
    return Boolean(obj && obj.plotType && obj.plotId && obj.plotImageId && obj.conversionCache);
}




/**
 * Check if the plot is is a blank image
 * @param {WebPlot} plot - the plot
 * @return {boolean}
 */
export function isBlankImage(plot) {
    if (plot.plotState.isThreeColor()) return false;
    const req= plot.plotState.getWebPlotRequest();
    return (req && req.getRequestType()===RequestType.BLANK);
}

/**
 *
 * @param {WebPlot} plot
 * @param {number} zoomFactor
 * @return {WebPlot}
 */
export function clonePlotWithZoom(plot,zoomFactor) {
    if (!plot) return null;
    const screenSize= {width:plot.dataWidth*zoomFactor, height:plot.dataHeight*zoomFactor};
    return Object.assign({},plot,{zoomFactor,screenSize});
}


/**
 *
 * @param {WebPlot|CysConverter} plot
 * @return {number}
 */
export function getScreenPixScaleArcSec(plot) {
    if (!plot || !plot.projection || !isKnownType(plot)) return 0;
    if (isImage(plot)) {
        return plot.projection.getPixelScaleArcSec() / plot.zoomFactor;
    }
    else if (isHiPS(plot)) {
        const pt00= makeWorldPt(0,0, plot.imageCoordSys);
        const tmpPlot= changeProjectionCenter(plot, pt00);
        const cc= CysConverter.make(tmpPlot);
        const scrP= cc.getScreenCoords( pt00);
        const pt2= cc.getWorldCoords( makeScreenPt(scrP.x-1, scrP.y), plot.imageCoordSys);
        return Math.abs(0-pt2.x)*3600; // note have to use angular distance formula here, because of the location of the point
    }
    return 0;
}


export function getFluxUnits(plot,band) {
    if (!plot || !band || !isImage(plot)) return '';
    return plot.fluxUnitAry[band.value];
}


/**
 *
 * @param {WebPlot|CysConverter} plot
 * @return {number}
 */
export function getPixScaleArcSec(plot) {
    return getPixScaleDeg(plot)*3600;
}

/**
 *
 * @param {WebPlot|CysConverter} plot
 * @return {number}
 */
export function getPixScaleDeg(plot) {
    if (!plot || !plot.projection || !isKnownType(plot) ) return 0;
    if (!plot || !plot.projection) return 0;
    if (isImage(plot)) {
        return plot.projection.getPixelScaleDegree();
    }
    else if (isHiPS(plot)) {
        const pt00= makeWorldPt(0,0, plot.imageCoordSys);
        const tmpPlot= changeProjectionCenter(plot, pt00);
        const cc= CysConverter.make(tmpPlot);
        const imP= cc.getImageCoords( pt00);
        const pt2= cc.getWorldCoords( makeImagePt(imP.x-1, imP.y), plot.imageCoordSys);
        return Math.abs(0-pt2.x);
    }
    return 0;
}