Source: visualize/saga/MouseReadoutWatch.js

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

import {race,call} from 'redux-saga/effects';
import {get} from 'lodash';
import {visRoot} from '../ImagePlotCntlr.js';
import {clone} from '../../util/WebUtil.js';
import {readoutRoot, makeValueReadoutItem, makePointReadoutItem,
        makeDescriptionItem, isLockByClick, STANDARD_READOUT, HIPS_STANDARD_READOUT} from '../MouseReadoutCntlr.js';
import {callGetFileFlux} from '../../rpc/PlotServicesJson.js';
import {Band} from '../Band.js';
import {MouseState} from '../VisMouseSync.js';
import {primePlot, getPlotStateAry, getPlotViewById} from '../PlotViewUtil.js';
import {CysConverter} from '../CsysConverter.js';
import {mouseUpdatePromise} from '../VisMouseSync.js';
import {getPixScaleArcSec, getScreenPixScaleArcSec, isImage, isHiPS, getFluxUnits} from '../WebPlot.js';
import {getPlotTilePixelAngSize} from '../HiPSUtil.js';
import {fireMouseReadoutChange} from '../VisMouseSync';
import {
    failedToParseWavelenthInfo,
    getImageCubeIdx,
    getPtWavelength, getWavelengthParseFailReason,
    getWaveLengthUnits, hasPixelLevelWLInfo, hasPlaneOnlyWLInfo,
    hasWLInfo,
    isImageCube, wavelengthInfoParsedSuccessfully
} from '../PlotViewUtil';


const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const PAUSE_DELAY= 200;


/**
 * Readout watcher defined the algorythm to drive the mouse readout. It does the following:
 *   - waits for a promise of a mouse event
 *   - Has two modes lockByClick, on or off
 *   - lockByClick off:
 *       - on mouse move compute and fire a mouse readout object
 *       - if mouse pauses (200 ms) and readout uses async, call the server for more data (the image pixel flux value)
 *       - if mouse still pause, fire a updated mouse readout object
 *   - lockByClick on:
 *       - on mouse click compute and fire a mouse readout object
 *       - if readout uses async, call the server for more data
 *       - if the position has not changed, fire a updated mouse readout object
 *
 */
export function* watchReadout() {

    let mouseCtx;
    yield call(mouseUpdatePromise);

    mouseCtx = yield call(mouseUpdatePromise);

    while (true) {

        let getNextWithWithAsync= false;
        const lockByClick= isLockByClick(readoutRoot());
        const {plotId,worldPt,screenPt,imagePt,mouseState, healpixPixel, norder}= mouseCtx;

        const plotView= getPlotViewById(visRoot(), plotId);
        const plot= primePlot(plotView);
        const threeColor= plot.plotState.threeColor;
        if (usePayload(mouseState,lockByClick)) {
            if (plot) {
                const readoutItems= makeImmediateReadout(plot, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder);
                // dispatchReadoutData({plotId, readoutItems, threeColor, readoutKey:getReadoutKey(plot)});
                fireMouseReadoutChange({plotId, readoutItems, threeColor, readoutType:getReadoutKey(plot)});
                getNextWithWithAsync= hasAsyncReadout(plot);
            }
        }
        else if (!lockByClick) {
            // dispatchReadoutData({plotId, readoutItems:{}, readoutKey:getReadoutKey(plot)});
            fireMouseReadoutChange({plotId, readoutItems:{}, readoutType:getReadoutKey(plot)});
        }

        if (getNextWithWithAsync) { // get the next mouse event or the flux
            mouseCtx= lockByClick ? yield call(processAsyncDataImmediate,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder) :
                                    yield call(processAsyncDataDelayed,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder);
        }
        else { // get the next mouse event
            mouseCtx = yield call(mouseUpdatePromise);
        }

    }
}

function* processAsyncDataImmediate(plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder) {
    try {
        const readoutItems= yield call(makeAsyncReadout,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder);
        if (readoutItems) {
            const plot= primePlot(plotView);
            // dispatchReadoutData({plotId:plotView.plotId,readoutItems, threeColor, readoutKey:getReadoutKey(plot)});
            fireMouseReadoutChange({plotId:plotView.plotId,readoutItems, threeColor, readoutType:getReadoutKey(plot)});
            const mouseCtx = yield call(mouseUpdatePromise);
            return mouseCtx;
        }
    }
    catch(error) {
        const mouseCtx = yield call(mouseUpdatePromise);
        return mouseCtx;
    }
    return null;
}


function* processAsyncDataDelayed(plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder) {
    const mousePausedRaceWinner = yield race({ mouseCtx: call(mouseUpdatePromise), timer: call(delay, PAUSE_DELAY) });

    if (mousePausedRaceWinner.mouseCtx) return mousePausedRaceWinner.mouseCtx;

    try {
        const mouseMoveRaceWinner = yield race({
            mouseCtx: call(mouseUpdatePromise),
            readoutItems: call(makeAsyncReadout,plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder)
        });
        if (mouseMoveRaceWinner.mouseCtx) return mouseMoveRaceWinner.mouseCtx;

        if (mouseMoveRaceWinner.readoutItems) {
            const plot= primePlot(plotView);
            // dispatchReadoutData({plotId:plotView.plotId,readoutItems:mouseMoveRaceWinner.readoutItems,
            //                  threeColor, readoutKey:getReadoutKey(plot)});
            fireMouseReadoutChange({plotId:plotView.plotId,readoutItems:mouseMoveRaceWinner.readoutItems,
                threeColor, readoutType:getReadoutKey(plot)});
            const mouseCtx = yield call(mouseUpdatePromise);
            return mouseCtx;
        }
    }
    catch(error) {
        console.log('flux error= just ignore');
        const mouseCtx = yield call(mouseUpdatePromise);
        return mouseCtx;
    }
}


function usePayload(mouseState, lockByClick) {
    if (lockByClick) {
        return mouseState===MouseState.CLICK;
    }
    else {
        return mouseState!==MouseState.EXIT;
    }
}



//-------------------------------------------------------------------------------
//------- Interface Functions between the readout watcher and the factory
//-------------------------------------------------------------------------------

const getReadoutType= (plot) => readoutTypes.find( (r) => r.matches(plot));

function makeImmediateReadout(plot,worldPt,screenPt,imagePt, threeColor, healpixPixel, norder) {
    const rt= getReadoutType(plot);
    return rt && rt.createImmediateReadout(plot,worldPt,screenPt,imagePt, threeColor, healpixPixel, norder);
}

function makeAsyncReadout(plotView,worldPt,screenPt,imagePt, threeColor) {
    const rt= getReadoutType(primePlot(plotView));
    return Promise.resolve(rt && rt.createAsyncReadout(plotView,worldPt,screenPt,imagePt, threeColor));
}

function hasAsyncReadout(plot) {
    const rt= getReadoutType(plot);
    return rt && rt.hasAsyncReadout(plot);
}

function getReadoutKey(plot) {
    const rt= getReadoutType(plot);
    return rt && rt.readoutKey;
}


//-------------------------------------
//------- Factory ---------------------
//-------------------------------------

/**
 * @global
 * @public
 * @typedef {Object} MouseReadoutType
 *
 * @prop {String} readoutKey: unique key represent this readout type
 *
 * @prop {function(WebPlot:plot):boolean} matches: function to test if this readout should be used form: tableMatches(WebPlot): boolean
 *
 * @prop {function(WebPlot:plot, WorldPt:worldPt, ScreenPt:screenPt,
 * ImagePt:imagePt, Boolean:threeColor, number:healpixPixel, number:norder):Object} createImmediateReadout:
 *            function to create a readout object
 *
 * @prop {function(WebPlot:plot, WorldPt:worldPt, ScreenPt:screenPt, ImagePt:imagePt, Boolean:threeColor):Promise<Object>} createAsyncReadout:
 *            function to create a readout by calling the server to get data, returns a promise with a readout
 *
 * @prop {function(WebPlot:plot):boolean} hasAsyncReadout: function to test readout should be used make async calls: hasAsyncReadout(WebPlot): boolean
 *
 */





/**
 * @type {Array.<MouseReadoutType>}
 */
const readoutTypes= [
    {  // this first one is not used yet.
        readoutKey: 'spectral-cube',
        matches: (plot) => false,
        createImmediateReadout: () => {throw Error('not implemented');},
        createAsyncReadout: () => {throw Error('not implemented');},
        hasAsyncReadout: (plot) => true,
    },
    {
        readoutKey: STANDARD_READOUT,
        matches: (plot) => isImage(plot),
        createImmediateReadout: makeImagePlotImmediateReadout,
        createAsyncReadout: makeImagePlotAsyncReadout,
        hasAsyncReadout: (plot) => true,
    },
    {
        readoutKey: HIPS_STANDARD_READOUT,
        matches: (plot) => isHiPS(plot),
        createImmediateReadout: makeHiPSReadout,
        createAsyncReadout: () => {throw Error('HiPS should not do async');},
        hasAsyncReadout: (plot) => false,

    },
];




//-------------------------------------
//------- Standard Image Plot and routines to get flux
//-------------------------------------

function makeImagePlotImmediateReadout(plot, worldPt, screenPt, imagePt, threeColor) {
    return makeReadoutWithFlux(makeReadout(plot,worldPt,screenPt,imagePt), plot, null, threeColor);
}

function makeImagePlotAsyncReadout(plotView, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder) {

    const plot= primePlot(plotView);
    const readoutItems= makeImmediateReadout(plot, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder);
    return doFluxCall(plotView,imagePt).then( (fluxResult) => {
        if (fluxResult) {
            const plot= primePlot(plotView);
            return makeReadoutWithFlux(readoutItems,plot, fluxResult, threeColor);
        }
    });
}


/**
 *
 * @param readout
 * @param {WebPlot} plot
 * @param fluxResult
 * @param threeColor
 * @return {*}
 */
function makeReadoutWithFlux(readout, plot, fluxResult,threeColor) {
    readout= clone(readout);
    const fluxData= fluxResult ? getFlux(fluxResult,plot) : null;
    const labels= getFluxLabels(plot);
    if (threeColor) {
        const bands = plot.plotState.getBands();
        bands.forEach( (b,idx) => readout[b.key+'Flux']=
            makeValueReadoutItem(labels[idx], get(fluxData,[idx,'value']),get(fluxData,[idx,'unit']), 6 ));
    }
    else {
        readout.nobandFlux= makeValueReadoutItem(labels[0], get(fluxData,[0,'value']),get(fluxData,[0,'unit']), 6);
    }
    if (fluxData) {
        const oIdx= fluxData.findIndex( (d) => d.imageOverlay);
        if (oIdx>-1) {
            readout.imageOverlay= makeValueReadoutItem('mask', fluxData[oIdx].value, fluxData[oIdx].unit, 0);
        }
    }
    return readout;

}

function doFluxCall(plotView,iPt) {
    const plot= primePlot(plotView);
    if (CysConverter.make(plot).pointInPlot(iPt)) {
        const plotStateAry= getPlotStateAry(plotView);
        return callGetFileFlux(plotStateAry, iPt)
            .then((result) => {
                return result;
            })
            .catch((e) => {
                console.log(`flux error: ${plotView.plotId}`, e);
                return ['', '', ''];
            });
    }
    else {
        return Promise.resolve(['', '', '']);
    }
}


function getFlux(result, plot) {
    const fluxArray = [];
    if (result.NO_BAND) {
        const fluxUnitStr = getFluxUnits(plot,Band.NO_BAND);
        const fValue = parseFloat(result.NO_BAND);
        fluxArray[0]= {value: fValue, unit: fluxUnitStr};
    }
    else {
        const bands = plot.plotState.getBands();
        let bandName;
        for (let i = 0; i < bands.length; i++) {
            switch (bands[i].key) {
                case 'RED':
                    bandName = 'Red';
                    break;
                case 'GREEN':
                    bandName = 'Green';
                    break;
                case 'BLUE':
                    bandName = 'Blue';
                    break;
            }
            const unitStr = getFluxUnits(plot,bands[i]);
            const fnum = parseFloat(result[bandName]);
            fluxArray[i]= {bandName, value:fnum, unit:unitStr};
        }
    }
    Object.keys(result)
        .filter((k) => k.startsWith('overlay'))
        .forEach( (k) => {fluxArray.push({ imageOverlay : true, value : parseFloat(result[k]), unit : 'mask' });});
    return fluxArray;
}

function getFluxLabels(plot) {

    if (!plot) return '';
    const bands = plot.plotState.getBands();
    const fluxLabels = ['', '', ''];
    for (let i = 0; i < bands.length; i++) {
        fluxLabels[i] = showSingleBandFluxLabel(plot, bands[i]);
    }
    return fluxLabels;

}

function showSingleBandFluxLabel(plot, band) {

    if (!plot || !band) return '';
    const fluxUnits= getFluxUnits(plot,band);

    let start;
    switch (band) {
        case Band.RED :
            start = 'Red ';
            break;
        case Band.GREEN :
            start = 'Green ';
            break;
        case Band.BLUE :
            start = 'Blue ';
            break;
        case Band.NO_BAND :
            start = '';
            break;
        default :
            start = '';
            break;
    }
    const valStr = start.length > 0 ? 'Val: ' : 'Value: ';

    const fluxUnitInUpperCase = fluxUnits.toUpperCase();
    if (fluxUnitInUpperCase === 'DN' || fluxUnitInUpperCase === 'FRAMES' || fluxUnitInUpperCase === '') {
        return start + valStr;
    }
    else {
        return start + 'Flux: ';
    }

}

function makeWLResult(plot,imagePt= undefined) {
               // do wavelength readout if it has pixel level wl or if it is a not a cube image with plane wl info
    if ((hasPixelLevelWLInfo(plot) || (hasPlaneOnlyWLInfo(plot) && !isImageCube(plot)))) {
        if (wavelengthInfoParsedSuccessfully(plot)) {
            if (!imagePt) return;
            const cubeIdx= (isImageCube(plot) && getImageCubeIdx(plot)) || 0;
            const wlValue= getPtWavelength(plot, imagePt, cubeIdx);
            return makeValueReadoutItem('Wavelength', wlValue, getWaveLengthUnits(plot), 4);
        }
        else {
            const item=  makeValueReadoutItem('Wavelength', 'Failed', '', 4);
            item.failReason= getWavelengthParseFailReason(plot);
            return item;
        }
    }
    else {
        return;
    }
}



/**
 *
 * @param {WebPlot} plot
 * @param {WorldPt} worldPt
 * @param {ScreenPt} screenPt
 * @param {ImagePt} imagePt
 * @return {Object}
 */
function makeReadout(plot, worldPt, screenPt, imagePt) {
    const csys= CysConverter.make(plot);
    if (csys.pointInPlot(imagePt)) {
        return {
            worldPt: makePointReadoutItem('World Point', worldPt),
            screenPt: makePointReadoutItem('Screen Point', screenPt),
            imagePt: makePointReadoutItem('Image Point', imagePt),
            fitsImagePt: makePointReadoutItem('FITS Standard Image Point', csys.getFitsStandardImagePtFromInternal(imagePt)),
            zeroBasedImagePt: makePointReadoutItem('FITS Standard Image Point', csys.getZeroBasedImagePtFromInternal(imagePt)),
            title: makeDescriptionItem(plot.title),
            pixel: makeValueReadoutItem('Pixel Size',getPixScaleArcSec(plot),'arcsec', 3),
            screenPixel:makeValueReadoutItem('Screen Pixel Size',getScreenPixScaleArcSec(plot),'arcsec', 3),
            wl: makeWLResult(plot,imagePt)
        };
    }
    else {
        return {
            wl: makeWLResult(plot)
        };
    }

}

//-------------------------------------
//------- HiPS Image Plot
//-------------------------------------


function makeHiPSPixelReadoutItem(plot) {
    const pixDeg= getPlotTilePixelAngSize(plot);
    let unit= 'degree', value= pixDeg;
    if (pixDeg*3600 < 60) {
        unit= 'arcsec';
        value= pixDeg*3600;
    }
    else if (pixDeg*60 < 60) {
        unit= 'arcmin';
        value= pixDeg*60;
    }
    return makeValueReadoutItem('Pixel Size',value, unit, 3);
}

/**
 *
 * @param {WebPlot} plot
 * @param {WorldPt} worldPt
 * @param {ScreenPt} screenPt
 * @param {ImagePt} imagePt
 * @param {boolean} threeColor
 * @param {number} [healpixPixel] the healpix pixel for the current tile, only passed with HiPS
 * @param {number} [norder] the healpix pixel norder
 * @return {Object}
 */
function makeHiPSReadout(plot, worldPt, screenPt, imagePt, threeColor, healpixPixel, norder) {
    const csys= CysConverter.make(plot);
    if (csys.pointInPlot(imagePt)) {
            return {
                worldPt: makePointReadoutItem('World Point', worldPt),
                screenPt: makePointReadoutItem('Screen Point', screenPt),
                imagePt: makePointReadoutItem('Image Point', imagePt),
                fitsImagePt: makePointReadoutItem('FITS Standard Image Point', csys.getFitsStandardImagePtFromInternal(imagePt)),
                title: makeDescriptionItem(plot.title),
                pixel: makeHiPSPixelReadoutItem(plot),
                screenPixel:makeValueReadoutItem('Screen Pixel Size',getScreenPixScaleArcSec(plot),'arcsec', 3),
                healpixPixel:makeValueReadoutItem('Healpix Pixel', healpixPixel, 'pixel', 0),
                healpixNorder:makeValueReadoutItem('Healpix norder', norder,'norder', 0),
            };
    }
    else {
        return {};
    }

}