/* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ import {without,union,difference, get, has} from 'lodash'; import {race,call} from 'redux-saga/effects'; import {take} from 'redux-saga/effects'; import {dispatchAddSaga} from '../core/MasterSaga.js'; import {flux} from '../Firefly.js'; import {clone} from '../util/WebUtil.js'; import ImagePlotCntlr, {dispatchRecenter, visRoot, ExpandType, WcsMatchType} from './ImagePlotCntlr.js'; import {primePlot, getPlotViewById} from './PlotViewUtil.js'; import Enum from 'enum'; import {REINIT_APP} from '../core/AppDataCntlr.js'; export const META_VIEWER_ID = 'triViewImageMetaData'; export const IMAGE_MULTI_VIEW_KEY= 'imageMultiView'; export const IMAGE_MULTI_VIEW_PREFIX= 'MultiViewCntlr'; export const ADD_VIEWER= `${IMAGE_MULTI_VIEW_PREFIX}.AddViewer`; export const REMOVE_VIEWER= `${IMAGE_MULTI_VIEW_PREFIX}.RemoveViewer`; export const VIEWER_MOUNTED= `${IMAGE_MULTI_VIEW_PREFIX}.viewMounted`; export const VIEWER_UNMOUNTED= `${IMAGE_MULTI_VIEW_PREFIX}.viewUnmounted`; export const ADD_VIEWER_ITEMS= `${IMAGE_MULTI_VIEW_PREFIX}.addViewerItems`; export const REMOVE_VIEWER_ITEMS= `${IMAGE_MULTI_VIEW_PREFIX}.removeViewerItems`; export const REPLACE_VIEWER_ITEMS= `${IMAGE_MULTI_VIEW_PREFIX}.replaceViewerItems`; export const CHANGE_VIEWER_LAYOUT= `${IMAGE_MULTI_VIEW_PREFIX}.changeViewerLayout`; export const UPDATE_VIEWER_CUSTOM_DATA= `${IMAGE_MULTI_VIEW_PREFIX}.updateViewerCustomData`; export const ADD_TO_AUTO_RECEIVER = `${IMAGE_MULTI_VIEW_PREFIX}.addToAutoReceiver`; function reducers() { return { [IMAGE_MULTI_VIEW_KEY]: reducer, }; } function actionCreators() { return { [CHANGE_VIEWER_LAYOUT]: changeViewerLayoutActionCreator, }; } export function getMultiViewRoot() { return flux.getState()[IMAGE_MULTI_VIEW_KEY]; } export default { reducers, actionCreators, ADD_VIEWER, REMOVE_VIEWER, ADD_VIEWER_ITEMS, REMOVE_VIEWER_ITEMS, REPLACE_VIEWER_ITEMS, VIEWER_MOUNTED, VIEWER_UNMOUNTED, UPDATE_VIEWER_CUSTOM_DATA, CHANGE_VIEWER_LAYOUT, reducer }; export const SINGLE='single'; export const GRID='grid'; export const IMAGE='image'; export const PLOT2D='plot2d'; export const WRAPPER='wrapper'; export const DEFAULT_FITS_VIEWER_ID= 'DEFAULT_FITS_VIEWER_ID'; export const DEFAULT_PLOT2D_VIEWER_ID= 'DEFAULT_PLOT2D_VIEWER_ID'; export const EXPANDED_MODE_RESERVED= 'EXPANDED_MODE_RESERVED'; export const GRID_RELATED='gridRelated'; export const GRID_FULL='gridFull'; export const NewPlotMode = new Enum(['create_replace', 'replace_only', 'none']); function initState() { /** * * @typedef {Object} Viewer * @prop {string} viewerId:EXPANDED_MODE_RESERVED, * @prop {string[]} itemIdAry * @prop {string} layout must be 'single' or 'grid' * @prop {boolean} canReceiveNewPlots - NewPlotMode.create_replace.key, * @prop {boolean} reservedContainer * @prop {string} containerType - one of 'image', 'plot2d', 'wrapper' * @prop {boolean} mounted - if the react component using the store is mounted * @prop {Object|String} layoutDetail - may be any object, string, etc- Hint for the UI, can be any string but with 2 reserved GRID_RELATED, GRID_FULL * @prop {object} customData: {} * * @global * @public */ /** * @typedef {Viewer[]} MultiViewerRoot * @global * @public */ return [ { viewerId:EXPANDED_MODE_RESERVED, itemIdAry:[], viewType:SINGLE, layout: GRID, canReceiveNewPlots: NewPlotMode.create_replace.key, reservedContainer:true, mounted: false, containerType : IMAGE, layoutDetail : 'none', customData: {}, renderTreeId: undefined }, { viewerId:DEFAULT_FITS_VIEWER_ID, itemIdAry:[], viewType:GRID, layout: GRID, canReceiveNewPlots: NewPlotMode.create_replace.key, reservedContainer:true, mounted: false, containerType : IMAGE, layoutDetail : 'none', customData: {}, renderTreeId: undefined, lastActiveItemId: '' }, { viewerId:DEFAULT_PLOT2D_VIEWER_ID, itemIdAry:[], viewType:GRID, layout: GRID, canReceiveNewPlots: NewPlotMode.create_replace.key, reservedContainer:true, mounted: false, containerType : PLOT2D, layoutDetail : 'none', customData: {}, renderTreeId: undefined, lastActiveItemId: '' }, { viewerId:'some id', itemIdAry:[], viewType:SINGLE, layout: SINGLE, canReceiveNewPlots: NewPlotMode.none.key, mounted: false, containerType : WRAPPER, layoutDetail : 'none', customData: {}, renderTreeId: undefined, lastActiveItemId: '' } ]; } //======================================== Dispatch Functions ============================= //======================================== Dispatch Functions ============================= //======================================== Dispatch Functions ============================= /** * * @param {string} viewerId * @param {string} canReceiveNewPlots a string representation of one of NewPlotMode. * @param {string} containerType a string with container type, IMAGE and PLOT2D are predefined * @param {boolean} mounted * @param {string} [renderTreeId] - used only with multiple rendered tree, like slate in jupyter lab * @param {string} [layout] - layout type - SINGLE or GRID, defaults to GRID */ export function dispatchAddViewer(viewerId, canReceiveNewPlots, containerType, mounted=false, renderTreeId, layout=GRID) { flux.process({ type: ADD_VIEWER, payload: {viewerId, canReceiveNewPlots, containerType, mounted, renderTreeId, lastActiveItemId:'', layout} }); } /** * * @param {string} viewerId */ export function dispatchRemoveViewer(viewerId) { flux.process({type: REMOVE_VIEWER , payload: {viewerId} }); } /** * * @param {string} viewerId * @param {string[]} itemIdAry array of itemIds * @param {string} containerType a string with container type, IMAGE and PLOT2D are predefined * @param {string} [renderTreeId] - used only with multiple rendered tree, like slate in jupyter lab * */ export function dispatchAddViewerItems(viewerId, itemIdAry, containerType, renderTreeId) { flux.process({type: ADD_VIEWER_ITEMS , payload: {viewerId, itemIdAry, containerType, renderTreeId} }); } /** * * @param {string} viewerId * @param {string} layout single or grid * @param {string} layoutDetail more detail about the type of layout, hint to UI, typically detail is with GRID */ export function dispatchChangeViewerLayout(viewerId, layout, layoutDetail=undefined) { flux.process({type: CHANGE_VIEWER_LAYOUT , payload: {viewerId, layout, layoutDetail} }); } /** * * @param {string} viewerId * @param {string[]} itemIdAry array of string of itemId */ export function dispatchRemoveViewerItems(viewerId, itemIdAry) { flux.process({type: REMOVE_VIEWER_ITEMS , payload: {viewerId, itemIdAry} }); } /** * * @param {string} viewerId * @param {string[]} itemIdAry array of string of itemId * @param {string} containerType a string with container type, IMAGE and PLOT2D are predefined */ export function dispatchReplaceViewerItems(viewerId, itemIdAry, containerType) { flux.process({type: REPLACE_VIEWER_ITEMS , payload: {viewerId, itemIdAry, containerType} }); } /** * * @param {string} viewerId */ export function dispatchViewerMounted(viewerId) { flux.process({type: VIEWER_MOUNTED , payload: {viewerId} }); } /** * * @param {string} viewerId */ export function dispatchViewerUnmounted(viewerId) { flux.process({type: VIEWER_UNMOUNTED , payload: {viewerId} }); } /** * * @param {string} viewerId * @param {Object} customData */ export function dispatchUpdateCustom(viewerId, customData) { flux.process({type: UPDATE_VIEWER_CUSTOM_DATA , payload: {viewerId,customData} }); } //======================================== ActionCreators ============================= //======================================== ActionCreators ============================= const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); export function* watchForResizing(options) { let remainingIdAry= options.plotIdAry.slice(0); let waitingForMore= true; if (!visRoot().wcsMatchType) return; while (waitingForMore) { const raceWinner = yield race({ action: take([ImagePlotCntlr.UPDATE_VIEW_SIZE]), timer: call(delay, 1000) }); const {action}= raceWinner; if (action && action.payload.plotId) { remainingIdAry= remainingIdAry.filter( (id) => id!==action.payload.plotId); waitingForMore= remainingIdAry.length>0; } else { waitingForMore= false; console.log('watchForResizing: hit timeout'); } } const vr= visRoot(); const pv= getPlotViewById(vr, vr.mpwWcsPrimId); if (pv && primePlot(vr, pv)) { if (vr.wcsMatchType) { const centerOnImage= ( vr.wcsMatchType===WcsMatchType.Standard || vr.wcsMatchType===WcsMatchType.Pixel || vr.wcsMatchType===WcsMatchType.PixelCenter); setTimeout(() => dispatchRecenter({ plotId: vr.activePlotId, centerOnImage}) , 100); } } } /** * @param {Action} rawAction * @returns {Function} */ function changeViewerLayoutActionCreator(rawAction) { return (dispatcher, getState) => { dispatcher(rawAction); const {viewerId}= rawAction.payload; const viewer= getViewer(getState()[IMAGE_MULTI_VIEW_KEY], viewerId); if (get(viewer, 'containerType')===IMAGE) { dispatchAddSaga(watchForResizing, { plotIdAry:viewer.layout===GRID ? viewer.itemIdAry: [viewer.lastActiveItemId]}); } }; } //======================================== Utilities ============================= //======================================== Utilities ============================= //======================================== Utilities ============================= /** * * @param {MultiViewerRoot} multiViewRoot * @param {string} viewerId * @return {boolean} */ export function hasViewerId(multiViewRoot, viewerId) { if (!multiViewRoot || !viewerId) return false; return multiViewRoot.find((entry) => entry.viewerId === viewerId); } /** * * @param {MultiViewerRoot} multiViewRoot * @param {string} viewerId * @return {string} will be 'single' or 'grid' */ export function getLayoutType(multiViewRoot, viewerId) { if (!multiViewRoot || !viewerId) return GRID; const v= multiViewRoot.find((entry) => entry.viewerId === viewerId); return v ? v.layout : GRID; } /** * * @param {MultiViewerRoot} multiViewRoot * @return {string[]} an array of item ids */ export function getExpandedViewerItemIds(multiViewRoot) { return getViewerItemIds(multiViewRoot,EXPANDED_MODE_RESERVED); } /** * * @param {MultiViewerRoot} multiViewRoot * @param {string} viewerId * @return {string[]} an array of item ids */ export function getViewerItemIds(multiViewRoot, viewerId) { if (!multiViewRoot || !viewerId) return []; var viewerObj= multiViewRoot.find( (entry) => entry.viewerId===viewerId); return (viewerObj) ? viewerObj.itemIdAry : []; } /** * get the viewer for an id * @param {MultiViewerRoot} multiViewRoot * @param {string} viewerId * @return {Viewer} */ export function getViewer(multiViewRoot,viewerId) { if (!multiViewRoot || !viewerId) return null; return multiViewRoot.find( (entry) => entry.viewerId===viewerId); } /** * * @param {MultiViewRoot} multiViewRoot * @param {string} itemId * @param {string} containerType * @return {Viewer} */ export function findViewerWithItemId(multiViewRoot, itemId, containerType) { if (!multiViewRoot) return null; const v= multiViewRoot.find((entry) => entry.viewerId!==EXPANDED_MODE_RESERVED && entry.itemIdAry.includes(itemId) && entry.containerType===containerType); return v ? v.viewerId : null; } // get an available view from multiple views /** * * @param {MultiViewerRoot} multiViewRoot * @param {string} containerType * @param {string} [renderTreeId] - used only with multiple rendered tree, like slate in jupyter lab * @return {Viewer} */ export function getAViewFromMultiView(multiViewRoot, containerType, renderTreeId= undefined) { const viewer= multiViewRoot.find((entry) => (!entry.viewerId.includes('RESERVED')&& !entry.customData.independentLayout && entry.containerType===containerType && (get(entry, 'canReceiveNewPlots') === NewPlotMode.create_replace.key))); if (viewer.reservedContainer && renderTreeId) { const newId= `${viewer.viewerId}_${renderTreeId}`; const modViewer= getViewer(multiViewRoot, newId); if (modViewer) return modViewer; dispatchAddViewer(newId, NewPlotMode.create_replace.key, containerType,false,renderTreeId); return getViewer(getMultiViewRoot(), newId); } else { return viewer; } } /** * * @param {MultiViewRoot} multiViewRoot * @param {VisRoot} visRoot * @param {string} plotId * @return {boolean} */ export function isImageViewerSingleLayout(multiViewRoot, visRoot, plotId) { if (visRoot.expandedMode!==ExpandType.COLLAPSE) { return visRoot.expandedMode!==ExpandType.GRID; } else { const viewerId= findViewerWithItemId(multiViewRoot, plotId, IMAGE); const viewer = viewerId ? getViewer(multiViewRoot, viewerId) : null; return viewer ? viewer.layout===SINGLE : true; } } //======================================== Action Creator ============================= //======================================== Action Creator ============================= //======================================== Action Creator ============================= // eslint-disable-next-line function xxPLACEHOLDERxxxxxActionCreator(rawAction) { // remember to export return (dispatcher) => { // eslint-disable-line }; } //============================================= //============================================= //============================================= function reducer(state=initState(), action={}) { if (!action.payload || !action.type) return state; var retState= state; const {payload}= action; switch (action.type) { case ADD_VIEWER: retState= addViewer(state,payload); break; case REMOVE_VIEWER: retState= removeViewer(state,action); break; case ADD_VIEWER_ITEMS: retState= addItems(state,payload.viewerId,payload.itemIdAry, payload.containerType, payload.renderTreeId); break; case ADD_TO_AUTO_RECEIVER: retState= addToAutoReceiver(state,action); break; case REMOVE_VIEWER_ITEMS: retState= removeItems(state,action); break; case REPLACE_VIEWER_ITEMS: retState= replaceImages(state,payload.viewerId,payload.itemIdAry, payload.containerType); break; case CHANGE_VIEWER_LAYOUT: retState= changeLayout(state,action); break; case VIEWER_MOUNTED: retState= changeMount(state,payload.viewerId,true); break; case VIEWER_UNMOUNTED: retState= changeMount(state,payload.viewerId,false); break; case UPDATE_VIEWER_CUSTOM_DATA: retState= updateCustomData(state,action); break; case ImagePlotCntlr.DELETE_PLOT_VIEW: retState= deleteSingleItem(state,payload.plotId, IMAGE); break; case ImagePlotCntlr.PLOT_HIPS: case ImagePlotCntlr.PLOT_IMAGE_START: const {viewerId, plotId, renderTreeId} = payload; if (imageViewerCanAdd(state,viewerId, plotId)) { //if (payload.viewerId && payload.plotId) { state= addItems(state,payload.viewerId,[payload.plotId], IMAGE, renderTreeId); retState= addItems(state,EXPANDED_MODE_RESERVED,[payload.plotId],IMAGE); } break; case ImagePlotCntlr.CHANGE_ACTIVE_PLOT_VIEW: case ImagePlotCntlr.PLOT_IMAGE: retState = changeActiveItem(state, payload, IMAGE); break; case REINIT_APP: retState= initState(); break; default: break; } return retState; } function imageViewerCanAdd(state, viewerId, plotId) { if (!viewerId || !plotId) return false; if (!hasViewerId(state,viewerId)) return true; return !state.find( (viewer) => { // look for the plotId in all the normal image viewers if (viewer.containerType!==IMAGE || viewer.viewerId===EXPANDED_MODE_RESERVED) return false; return getViewerItemIds(state,viewer.viewerId).includes(plotId); }); } function addViewer(state,payload) { const {viewerId,containerType, layout=GRID,canReceiveNewPlots=NewPlotMode.replace_only.key, mounted=false, renderTreeId}= payload; var {lastActiveItemId} = payload; var entryInState = hasViewerId(state,viewerId); if (entryInState) { entryInState = Object.assign(entryInState, {canReceiveNewPlots, mounted, containerType}); if (has(entryInState, 'lastActiveItemId')) { lastActiveItemId = entryInState.lastActiveItemId ? entryInState.lastActiveItemId : get(entryInState, ['itemIdAry', '0'], ''); entryInState = Object.assign(entryInState, {lastActiveItemId}); } return [...state]; } else { // set default layout for the viewer with viewerId, META_VIEWER_ID, is full-grid type const layoutDetail = viewerId === META_VIEWER_ID ? GRID_FULL : undefined; const entry = {viewerId, containerType, canReceiveNewPlots, layout, mounted, itemIdAry: [], customData: {}, lastActiveItemId, layoutDetail, renderTreeId}; return [...state, entry]; } } function removeViewer(state,action) { const {viewerId}= action.payload; return state.filter( (v) => v.viewId!==viewerId); } /** * * @param {MultiViewerRoot} state * @param {string} viewerId * @param {string[]} itemIdAry * @param {string} containerType * @param {string} [renderTreeId] - used only with multiple rendered tree, like slate in jupyter lab, only used here * if the viewerId does not exist and it needs to make one. * @return {MultiViewerRoot} */ function addItems(state,viewerId,itemIdAry, containerType, renderTreeId) { if (renderTreeId) { itemIdAry.forEach( (id) => { const v= findViewerWithItemId(state, id, containerType); if (v && v.renderTreeId!==renderTreeId) state= deleteSingleItem(state,id,containerType); }); } let viewer= state.find( (entry) => entry.viewerId===viewerId); if (!viewer) { state= addViewer(state,{viewerId,containerType, renderTreeId}); viewer= state.find( (entry) => entry.viewerId===viewerId); } itemIdAry= union(viewer.itemIdAry,itemIdAry); return state.map( (entry) => entry.viewerId===viewerId ? clone(entry, {itemIdAry}) : entry); } /** * * @param {MultiViewerRoot} state * @param {string} viewerId * @param {string[]} itemIdAry * @param {string} containerType * @return {MultiViewerRoot} */ function replaceImages(state,viewerId,itemIdAry,containerType) { let viewer= state.find( (entry) => entry.viewerId===viewerId); if (!viewer) { state= addViewer(state,{viewerId,containerType}); } const updateViewer = (entry) => { if (has(entry, 'lastActiveItemId')) { return {itemIdAry, lastActiveItemId: get(itemIdAry, '0', '')}; } else { return {itemIdAry}; } }; return state.map( (entry) => entry.viewerId===viewerId ? clone(entry, updateViewer(entry)) : entry); } function addToAutoReceiver(state,action) { const {imageAry}= action.payload; return state.map( (entry) => entry.canReceiveNewPlots === NewPlotMode.create_replace.key ? clone(entry, {itemIdAry: union(entry.itemIdAry,imageAry)}) : entry); } function removeItems(state,action) { var {viewerId,itemIdAry}= action.payload; var viewer= state.find( (entry) => entry.viewerId===viewerId); if (!viewer) return state; var rmIdAry = itemIdAry.slice(); itemIdAry= difference(viewer.itemIdAry,itemIdAry); var updateViewer = (entry) => { if (has(entry, 'lastActiveItemId')&&rmIdAry.includes(entry.lastActiveItemId)) { return {itemIdAry, lastActiveItemId: get(itemIdAry, '0', '')}; } else { return {itemIdAry}; } }; return state.map( (entry) => entry.viewerId===viewerId ? clone(entry, updateViewer(entry)) : entry); } /** * Delete an item with only knowing the itemId and containerType but not the viewerId * @param {MultiViewRoot} state * @param {string} itemId * @param {string} containerType * @return {MultiViewRoot} */ function deleteSingleItem(state,itemId, containerType) { return state.map( (viewer) => { if (viewer.containerType!==containerType || !viewer.itemIdAry.includes( itemId)) return viewer; const v = clone(viewer, {itemIdAry: without(viewer.itemIdAry, itemId)}); if (has(v, 'lastActiveItemId') && (v.lastActiveItemId === itemId)) { return clone(v, {lastActiveItemId: get(v, 'itemIdAry.0', '')}); } else { return v; } }); } function changeLayout(state,action) { const {viewerId,layout,layoutDetail}= action.payload; var viewer= state.find( (entry) => entry.viewerId===viewerId); if (!viewer) return state; if (viewer.layout===layout && viewer.layoutDetail===layoutDetail) return state; return state.map( (entry) => entry.viewerId===viewerId ? clone(entry, {layout,layoutDetail}) : entry); } function changeMount(state,viewerId,mounted) { var viewer= state.find( (entry) => entry.viewerId===viewerId); if (!viewer) return state; if (viewer.mounted===mounted) return state; return state.map( (entry) => entry.viewerId===viewerId ? clone(entry, {mounted}) : entry); } function updateCustomData(state,action) { const {viewerId,customData}= action.payload; return state.map( (entry) => entry.viewerId===viewerId ? clone(entry, {customData}) : entry); } function changeActiveItem(state, payload, containerType) { var {plotId, viewerId} = payload; return state.map((viewer) => { var isView = false; if (!has(viewer, 'lastActiveItemId')) return viewer; if (viewerId) { // plot image action case if ((viewerId === viewer.viewerId) && viewer.itemIdAry.includes(plotId)) { isView = true; } } else { // change active plot action case if ((viewer.containerType === containerType) && (viewer.itemIdAry.includes(plotId))) { isView = true; } } if (isView) { return clone(viewer, {lastActiveItemId: plotId}); } else { return viewer; } }); }