/*
* 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 {};
}
}