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