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