/* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ /** * @public * @summary Build the interface to remotely communicate to the firefly viewer */ import {take} from 'redux-saga/effects'; import {isArray, get} from 'lodash'; import Enum from 'enum'; import {WSCH} from '../core/History.js'; import {debug} from './ApiUtil.js'; import {getRootURL} from '../util/BrowserUtil.js'; import {dispatchRemoteAction} from '../core/JsonUtils.js'; import {dispatchPlotImage, dispatchPlotHiPS} from '../visualize/ImagePlotCntlr.js'; import {RequestType} from '../visualize/RequestType.js'; import {clone, logError, hashCode} from '../util/WebUtil.js'; import {confirmPlotRequest,findInvalidWPRKeys} from '../visualize/WebPlotRequest.js'; import {dispatchTableSearch, dispatchTableFetch} from '../tables/TablesCntlr.js'; import {dispatchChartAdd} from '../charts/ChartsCntlr.js'; import {makeFileRequest} from '../tables/TableRequestUtil.js'; import {uniqueChartId} from '../charts/ChartUtil.js'; import {getWsChannel, getWsConnId} from '../core/AppDataCntlr.js'; import {getConnectionCount, WS_CONN_UPDATED, GRAB_WINDOW_FOCUS} from '../core/AppDataCntlr.js'; import {dispatchAddCell, dispatchEnableSpecialViewer, LO_VIEW} from '../core/LayoutCntlr.js'; import {dispatchAddSaga} from '../core/MasterSaga.js'; import {modifyURLToFull} from '../util/BrowserUtil.js'; import {DEFAULT_FITS_VIEWER_ID, DEFAULT_PLOT2D_VIEWER_ID} from '../visualize/MultiViewCntlr.js'; import {REINIT_APP} from '../core/AppDataCntlr.js'; export const ViewerType= new Enum([ 'TriView', // use what it in the title 'Grid' // use the plot description key ], { ignoreCase: true }); const VIEWER_ID = '__viewer'; let viewerWindow; let defaultViewerFile=''; let defaultViewerType=ViewerType.TriView; /** * @returns {{getViewer: function, getExternalViewer: function}} * @ignore */ export function buildViewerApi() { return {getViewer,setViewerConfig,ViewerType}; } /** * * @param {Object} viewerType must be either ViewerType.TriView or ViewerType.Grid * @param {string} [htmlFile] */ export function setViewerConfig(viewerType, htmlFile= '') { if (!htmlFile) { if (viewerType===ViewerType.Grid) { htmlFile= 'slate.html'; } } defaultViewerType= viewerType; defaultViewerFile= htmlFile; } /** * wrapper function to return the API's remote Viewer object. This allow one firefly app to * gain access to another app's API. * To use this function, it should be loaded first. @see loadRemoteApi * It cannot be implemented as async because the common use case for getViewer is to launch a new Tab/Window. * This will be blocked by browser's popup blocker if it's done asynchronously. * @param {String} [channel] the channel id string, default to current connected channel. * @param {String} [file] url path, or the html file to load. Defaults to blank(index of the app). * @param {String} [scriptUrl] url of the script to load. When scriptUrl is not given, return the Viewer of the loaded app. * @returns {Object} the API's Viewer interface @link{firefly.ApiViewer} * @public * @memberof firefly */ export function getViewer(channel, file=defaultViewerFile, scriptUrl) { if (scriptUrl) { // requesting for a viewer that's different from the currently loaded app const getViewer = get(loadRemoteApi(scriptUrl), 'getViewer'); return getViewer && getViewer(channel, file); } else { // return currently loaded app's Viewer channel = (channel || getWsChannel()) + VIEWER_ID; const dispatch= (action) => dispatchRemoteAction(channel,action); const reinitViewer= () => dispatch({ type: REINIT_APP, payload: {}}); /** * The interface to remotely communicate to the firefly viewer. * @public * @namespace firefly.ApiViewer */ const viewer= Object.assign({dispatch, reinitViewer, channel}, buildImagePart(channel,file,dispatch), buildTablePart(channel,file,dispatch), buildChartPart(channel,file,dispatch), buildUtilPart(channel,file), ); switch (defaultViewerType) { // add any additional API case ViewerType.TriView: return viewer; case ViewerType.Grid: return Object.assign({}, viewer, buildSlateControl(channel,file,dispatch)); default: debug('Unknown viewer type: ${defaultViewerType}, returning TriView'); return viewer; } } } /** * wrapper function to return the API's remote Viewer object. This allow one firefly app to * gain access to another app's API. * To use this function, it should be loaded first. @see loadRemoteApi * It cannot be implemented as async because the common use case for getViewer is to launch a new Tab/Window. * This will be blocked by browser's popup blocker if it's done asynchronously. * @param {object} p parameter * @param {String} [p.channel] default to current connected channel. * @param {String} [p.file] url path, or the html file to load. Defaults to blank(index of the app). * @param {String} [p.scriptUrl] url of the script to load. When scriptUrl is not given, return the Viewer of the loaded app. * @returns {Object} the API's getViewer object */ function getRemoteViewer({channel, file, scriptUrl}) { if (!scriptUrl) return getViewer(channel, file); const getViewer = get(loadRemoteApi(scriptUrl), 'getViewer'); return getViewer && getViewer(channel, file); } export function loadRemoteApi(scriptUrl) { const frameId = 'id_' + hashCode(scriptUrl); let apiFrame = document.getElementById(frameId); console.log('apiFrame:' + apiFrame); if (!apiFrame) { apiFrame = document.createElement('iframe'); apiFrame.id = frameId; apiFrame.style.display = 'none'; apiFrame.style.width = '0px'; apiFrame.style.height = '0px'; document.body.appendChild(apiFrame); // apiFrame.onload = () => {} this event is not fired in Safari when src is blank. it's treated as synchronous. const myscript = apiFrame.contentDocument.createElement('script'); myscript.type = 'text/javascript'; myscript.src = scriptUrl; const headEl = apiFrame.contentDocument.getElementsByTagName('head')[0]; headEl.appendChild(myscript); } return get(apiFrame, 'contentWindow.firefly'); } function buildUtilPart(channel, file, dispatcher) { const openViewer= () => { doViewerOperation(channel,file); } return {openViewer}; } function buildSlateControl(channel,file,dispatcher) { /** * * @param {number} row * @param {number} col * @param {number} width * @param {number} height * @param {LO_VIEW} type must be 'tables', 'images' or 'xyPlots' (use 'xyPlots' for histograms) * @param {string} cellId */ const addCell= (row, col, width, height, type, cellId) => { doViewerOperation(channel,file, () => { if (LO_VIEW.get(type)===LO_VIEW.tables) { if (cellId!=='main') debug('for tables type is force to be "main"'); cellId= 'main'; } dispatchAddCell({row,col,width,height,type, cellId,dispatcher}); }); }; // const enableSpecialViewer= (viewerType, cellId= undefined, tableGroup= 'main') => // dispatchEnableSpecialViewer({viewerType, cellId:(cellId || `${viewerType}-${tableGroup}`), dispatcher}); /** * * @param {string} cellId cell id to add to * @param {string} [tableGroup] tableGroup to connect to, currently only main supported */ const showCoverage = (cellId, tableGroup= 'main') => { doViewerOperation(channel,file, () => { dispatchEnableSpecialViewer({viewerType:LO_VIEW.coverageImage, cellId:(cellId || `${LO_VIEW.coverageImage}-${tableGroup}`), dispatcher}); }); }; /** * * @param {string} cellId cell id to add to * @param {string} [tableGroup] tableGroup to connect to, currently only main supported */ const showImageMetaDataViewer = (cellId, tableGroup= 'main') => { doViewerOperation(channel,file, () => { dispatchEnableSpecialViewer({ viewerType: LO_VIEW.tableImageMeta, cellId: (cellId || `${LO_VIEW.tableImageMeta}-${tableGroup}`), dispatcher }); }); }; return {addCell, showCoverage, showImageMetaDataViewer}; } function buildImagePart(channel,file,dispatch) { let defP= {}; /** * @summary set the default params the will be add to image plot request * @param params * @memberof firefly.ApiViewer * @public */ const setDefaultParams= (params)=> defP= params; /** * @summary show a image in the firefly viewer in another tab * @param {WebPlotParams|WebPlotRequest} request The object contains parameters for web plot request * @param {String} viewerId * @memberof firefly.ApiViewer * @public */ const showImage= (request, viewerId) => { doViewerOperation(channel,file, () => { if (isArray(request)) { request= request.map( (r) => clone(r,defP)); } else { request= clone(request,defP); } plotRemoteImage(request,viewerId, dispatch); }); }; /** * @summary show a HiPS in the firefly viewer in another tab * @param {WebPlotParams|WebPlotRequest} request The object contains parameters for web plot request on HiPS type * @param {String} viewerId * @memberof firefly.ApiViewer * @public */ const showHiPS= (request, viewerId) => { doViewerOperation(channel,file, () => { request= clone(request,defP); plotRemoteHiPS(request,viewerId, dispatch); }); }; /** * @summary show a image in the firefly viewer in another tab, the file first then the url * @param file a file on the server * @param url a url to a fits file * @memberof firefly.ApiViewer * @public */ const showImageFileOrUrl= (file,url) => showImage({file, url, Type : RequestType.TRY_FILE_THEN_URL}); //------- deprecated part --------------------- const doDepPlot= (request, oldCall, newCall) => { debug(`${oldCall} call it deprecated, use ${newCall} instead`); showImage(request); }; const plot= (request) => doDepPlot(request,'plot','showImage'); const plotURL= (url) => doDepPlot({url}, 'plotURL', `showImage({url: ${url} })`); const plotFile= (file) => doDepPlot({file}, 'plotFile', `showImage({url: ${file} })`); const plotFileOrURL= (file,url) => doDepPlot({file, url, Type : RequestType.TRY_FILE_THEN_URL}, 'plotFileOrURL', 'showImageFileOrUrl'); //------- End deprecated part --------------------- return {showImage,showHiPS, showImageFileOrUrl,setDefaultParams, plot,plotURL,plotFile, plotFileOrURL}; } function buildTablePart(channel,file,dispatch) { /** * * @param {Object} request * @param {TblOptions} options * @memberof firefly.ApiViewer * @public */ const showTable= (request, options) => { doViewerOperation(channel,file, () => { dispatchTableSearch(request, options, dispatch); }); }; const fetchTable= (request, hlRowIdx) => { doViewerOperation(channel,file, () => { dispatchTableFetch(request, hlRowIdx, undefined, dispatch); }); }; return {showTable, fetchTable}; } function buildChartPart(channel,file,dispatch) { /** * @summary Show a chart * @param {{chartId: string, data: array.object, layout: object}} options * @param {string} viewerId * @memberof firefly.ApiViewer * @public */ const showChart= (options, viewerId) => { doViewerOperation(channel, file, () => { plotRemoteChart(options, viewerId, dispatch); }); }; /** * @summary Show XY Plot * @param {XYPlotOptions} xyPlotOptions * @param {string} viewerId * @memberof firefly.ApiViewer * @public */ const showXYPlot= (xyPlotOptions, viewerId) => { doViewerOperation(channel, file, () => { plotRemoteXYPlot(xyPlotOptions, viewerId, dispatch); }); }; /** * @summary Show Histogram * @param {HistogramOptions} histogramOptions * @param {string} viewerId * @memberof firefly.ApiViewer * @public */ const showHistogram= (histogramOptions, viewerId) => { doViewerOperation(channel, file, () => { plotRemoteHistogram(histogramOptions, viewerId, dispatch); }); }; return {showChart, showXYPlot, showHistogram}; } function doViewerOperation(channel,file,f) { const cnt = getConnectionCount(channel); if (cnt > 0) { if (viewerWindow){ viewerWindow.focus(); } else { dispatchRemoteAction(channel, {type:GRAB_WINDOW_FOCUS}); } f && f(); } else { dispatchAddSaga(doOnWindowConnected, {channel, f}); // const url= `${getRootURL()}${file}?__wsch=${channel}`; const url= `${modifyURLToFull(file,getRootURL())}?${WSCH}=${channel}`; viewerWindow = window.open(url, channel); } } export function* doOnWindowConnected({channel, f}) { let isLoaded = false; while (!isLoaded) { const action = yield take([WS_CONN_UPDATED]); const cnt = get(action, ['payload', channel, 'length'], 0); isLoaded = cnt > 0; } // Added a half second delay before ready to combat a race condition // TODO: loi is going to look it it to determine if application it truly ready setTimeout(() => f && f(), 500); } //================================================================ //---------- Private Chart functions //================================================================ /** * @param {{chartId: string, data: array.object, layout: object}} params - chart parameters * @param {string} viewerId * @param {Function} dispatch - dispatch function */ function plotRemoteChart(params, viewerId, dispatch) { const dispatchParams= clone({ groupId: viewerId || 'default', viewerId:viewerId || DEFAULT_PLOT2D_VIEWER_ID, chartId: params.chartId || uniqueChartId(), chartType: 'plot.ly', deletable: true, dispatcher: dispatch }, params); dispatchChartAdd(dispatchParams); } /** * @param {XYPlotOptions} params - XY plot parameters * @param {string} viewerId * @param {Function} dispatch - dispatch function */ function plotRemoteXYPlot(params, viewerId, dispatch) { let tblId = params.tbl_id; if (!tblId) { const source = params.source; if (source) { const searchRequest = makeFileRequest( params.chartTitle||'', // title params.source, // source null, // alt_source {pageSize: 0} // table options ); tblId = searchRequest.tbl_id; dispatchTableFetch(searchRequest, 0, undefined, dispatch); params = Object.assign({}, params, {tbl_id: tblId}); } else { logError('Either tbl_id or source must be specified in the parameters'); return; } } // SCATTER dispatchChartAdd({ groupId: viewerId || 'default', viewerId:viewerId || DEFAULT_PLOT2D_VIEWER_ID, chartId: params.chartId || uniqueChartId(), chartType: 'scatter', params, deletable: true, dispatcher: dispatch}); } /** * @param {HistogramOptions} params - histogram parameters * @param {string} viewerId * @param {Function} dispatch - dispatch function */ function plotRemoteHistogram(params, viewerId, dispatch) { let tblId = params.tbl_id; if (!tblId) { const source = params.source; if (source) { const searchRequest = makeFileRequest( params.chartTitle||'', // title params.source, // source null, // alt_source {pageSize: 0} // table options ); tblId = searchRequest.tbl_id; dispatchTableFetch(searchRequest, 0, undefined, dispatch); params = Object.assign({}, params, {tbl_id: tblId}); } else { logError('Either tbl_id or source must be specified in the parameters'); return; } } // HISTOGRAM dispatchChartAdd({ groupId: viewerId || 'default', viewerId:viewerId || DEFAULT_PLOT2D_VIEWER_ID, chartId: params.chartId || uniqueChartId(), chartType: 'histogram', params, deletable: true, dispatcher: dispatch}); } //================================================================ //---------- Private Table functions //================================================================ //================================================================ //---------- Private Image functions //================================================================ function plotRemoteImage(request, viewerId, dispatch) { const testR= Array.isArray(request) ? request : [request]; testR.forEach( (r) => { const badList= findInvalidWPRKeys(r); if (badList.length) debug(`plot request has the following bad keys: ${badList}`); }); request= confirmPlotRequest(request,{},'remoteGroup',makePlotId); dispatchPlotImage({wpRequest:request, viewerId:viewerId || DEFAULT_FITS_VIEWER_ID, dispatcher:dispatch}); } function plotRemoteHiPS(request, viewerId, dispatch) { const badList= findInvalidWPRKeys(request); if (badList.length) debug(`HiPS request has the following bad keys: ${badList}`); request= confirmPlotRequest(request,{Type:'HiPS'},'remoteGroup',makePlotId); dispatchPlotHiPS({plotId:request.plotId, wpRequest:request, viewerId:viewerId || DEFAULT_FITS_VIEWER_ID, dispatcher:dispatch}); } let plotCnt= 0; function makePlotId() { plotCnt++; return `apiPlot-${getWsConnId()}-${plotCnt}`; }