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