/* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ import {take} from 'redux-saga/effects'; import {makeDrawingDef} from '../visualize/draw/DrawingDef.js'; import DrawLayer, {DataTypes, ColorChangeType} from '../visualize/draw/DrawLayer.js'; import {makeFactoryDef} from '../visualize/draw/DrawLayerFactory.js'; import {drawRegions} from '../visualize/region/RegionDrawer.js'; import {getRegionIndex, addNewRegion, removeRegion} from '../visualize/region/RegionUtil.js'; import {RegionFactory} from '../visualize/region/RegionFactory.js'; import {primePlot, getDrawLayerById, getDrawLayersByType} from '../visualize/PlotViewUtil.js'; import {visRoot} from '../visualize/ImagePlotCntlr.js'; import {MouseState} from '../visualize/VisMouseSync.js'; import DrawOp from '../visualize/draw/DrawOp.js'; import DrawLayerCntlr, {DRAWING_LAYER_KEY, dispatchDeleteRegionLayer, dispatchSelectRegion, dlRoot, getDlAry} from '../visualize/DrawLayerCntlr.js'; import {get, set, has, isEmpty, isString, isArray} from 'lodash'; import {dispatchAddSaga} from '../core/MasterSaga.js'; import {flux} from '../Firefly.js'; const ID= 'REGION_PLOT'; const TYPE_ID= 'REGION_PLOT_TYPE'; const factoryDef= makeFactoryDef(TYPE_ID, creator, getDrawData, getLayerChanges, null, null); export default {factoryDef, TYPE_ID}; function* regionsRemoveSaga({id, plotId}, dispatch, getState) { while (true) { var action = yield take([DrawLayerCntlr.REGION_REMOVE_ENTRY, DrawLayerCntlr.REGION_DELETE_LAYER, DrawLayerCntlr.DETACH_LAYER_FROM_PLOT]); if (action.payload.drawLayerId === id) { var dl; switch (action.type) { case DrawLayerCntlr.REGION_REMOVE_ENTRY : dl = getDrawLayerById(getState()[DRAWING_LAYER_KEY], id); if (dl && isEmpty(get(dl, 'drawObjAry'))) { dispatchDeleteRegionLayer(id, plotId); } break; case DrawLayerCntlr.REGION_DELETE_LAYER: case DrawLayerCntlr.DETACH_LAYER_FROM_PLOT: return; break; } } } } let idCnt = 0; export const createNewRegionLayerId = () => { const layerList = getDrawLayersByType(getDlAry(), TYPE_ID); while (true) { const newId = `${ID}-${idCnt++}`; const dl = layerList.find((layer) => { return (layer.drawLayerId && layer.drawLayerId === newId); }); if (!dl) { return newId; } } }; let titleCnt = 1; export const getRegionLayerTitle = (layerTitle) => { const defaultRegionTitle = 'ds9 region overlay'; const layerList = getDrawLayersByType(getDlAry(), TYPE_ID); let cntTitle = 0; while (true) { const newTitle = layerTitle ? (!cntTitle ? layerTitle : `${layerTitle}-${cntTitle}`) : `${defaultRegionTitle}-${titleCnt++}`; const dl = layerList.find((layer) => { return (layer.title && layer.title === newTitle); }); if (layerTitle) { cntTitle++; } if (!dl) { return newTitle; } } }; /** * create region plot layer * @param initPayload in region plot layer, attribute regionAry: region description array * dataFrom: regionAry is from 'json' object or 'ds9' string * regions: array of region object constructed by parsing regionAry * regionObjAry: array of drawing object constructed from regions * => regions and regionObjAry are updated as adding or removing regions occurs * highlightedRegion: selected region * @return {DrawLayer} */ function creator(initPayload) { const drawingDef= makeDrawingDef('green'); const pairs = { [MouseState.DOWN.key]: highlightChange }; const options= { canUseMouse:true, canHighlight:true, canUserChangeColor: ColorChangeType.DISABLE, isPointData:false, hasPerPlotData: true, destroyWhenAllDetached: true }; const actionTypes = [DrawLayerCntlr.REGION_ADD_ENTRY, DrawLayerCntlr.REGION_REMOVE_ENTRY, DrawLayerCntlr.REGION_SELECT]; const title = get(initPayload, 'title'); const id = initPayload.drawLayerId ? initPayload.drawLayerId : createNewRegionLayerId(); const dl = DrawLayer.makeDrawLayer( id, TYPE_ID, getRegionLayerTitle(title), options, drawingDef, actionTypes, pairs ); dl.regionAry = get(initPayload, 'regionAry', null); dl.dataFrom = get(initPayload, 'dataFrom', 'ds9'); dl.highlightedRegion = get(initPayload, 'highlightedRegion', null); dl.selectMode = get(initPayload, 'selectMode'); dispatchAddSaga(regionsRemoveSaga, {id, plotId: get(initPayload, 'plotId')}); return dl; } /** * find the drawObj which is selected for highlight * @param mouseStatePayload * @returns {Function} */ function highlightChange(mouseStatePayload) { const {drawLayer,plotId,screenPt} = mouseStatePayload; var done = false; var closestInfo = null; var closestObj = null; const maxChunk = 1000; const {data} = drawLayer.drawData; const plot = primePlot(visRoot(), plotId); function* getDrawObj() { var index = 0; var dataPlot = get(data, plotId); while (index < dataPlot.length) { yield dataPlot[index++]; } } var gen = getDrawObj(); const sId = window.setInterval( () => { if (done) { window.clearInterval(sId); // set the highlight region on current drawLayer, // unset the highlight on other drawLayer if a highlight is found for current layer dlRoot().drawLayerAry.forEach( (dl) => { if (dl.drawLayerId === drawLayer.drawLayerId) { dispatchSelectRegion(dl.drawLayerId, closestObj); } else if (closestObj) { dispatchSelectRegion(dl.drawLayerId, null); } }); } for (let i = 0; i < maxChunk; i++ ) { var dObj = gen.next().value; if (dObj) { var distInfo = DrawOp.isScreenPointInside(screenPt, dObj, plot); if (distInfo.inside) { if (!closestInfo || closestInfo.dist > distInfo.dist) { closestInfo = distInfo; closestObj = dObj; } } } else { done = true; break; } } }, 0); return () => window.clearInterval(sId); } /** * state update on the drawlayer change * @param drawLayer * @param action * @returns {*} */ function getLayerChanges(drawLayer, action) { const {regionChanges, drawLayerId } = action.payload; if (drawLayerId && drawLayerId !== drawLayer.drawLayerId) return null; var dd = Object.assign({}, drawLayer.drawData); var deHighlight = (obj) => { //obj.highlight = 0; obj.isRendered = 1; }; // re-render data in case border 'replace' style is used for region selected var reDrawData = (hiRegion) => { if (has(drawLayer, 'selectMode.selectStyle') && drawLayer.selectMode.selectStyle.includes('Replace')) { if (hiRegion) { hiRegion.isRendered = 0; // de-render the selected region } drawLayer.drawObjAry = drawLayer.drawObjAry.slice(); Object.keys(dd[DataTypes.DATA]).forEach((plotId) => { set(dd[DataTypes.DATA], plotId, null); }); } }; switch (action.type) { case DrawLayerCntlr.REGION_SELECT: const {selectedRegion} = action.payload; Object.keys(dd[DataTypes.HIGHLIGHT_DATA]).forEach((plotId) => { set(dd[DataTypes.HIGHLIGHT_DATA], plotId, null); }); var highlightedRegion = null; var selectRegionDesc = null; // no region is selected if (isEmpty(selectedRegion)) { // nothing is selected, empty string, empty array or null if (drawLayer.highlightedRegion) { deHighlight(drawLayer.highlightedRegion); // de-highlight the highlighted region if there was drawLayer.highlightedRegion = null; reDrawData(); } } else { // test if selected region is string or array of string description if (isString(selectedRegion) || isArray(selectedRegion)) { // region description in string or array of string highlightedRegion = getSelectedRegionDrawObj(drawLayer, selectedRegion); highlightedRegion = isEmpty(highlightedRegion) ? null : highlightedRegion[0]; } else { // a region drawObj highlightedRegion = selectedRegion; } // selected region is valid if (highlightedRegion) { if (drawLayer.highlightedRegion) { deHighlight(drawLayer.highlightedRegion); // de-highlight the highlighted region if there was } reDrawData(highlightedRegion); //highlightedRegion.highlight = 1; selectRegionDesc = get(highlightedRegion, 'region.desc', null); } } return Object.assign({}, {highlightedRegion}, {drawData: dd}, {selectRegionDesc}); case DrawLayerCntlr.REGION_ADD_ENTRY: if (regionChanges) { var {layerTitle} = action.payload; if (layerTitle) { drawLayer.title = layerTitle.slice(); // update title of the layer } addRegionsToData(drawLayer, regionChanges); Object.keys(dd[DataTypes.DATA]).forEach((plotId) => { set(dd[DataTypes.DATA], plotId, drawLayer.drawObjAry); }); } return {drawData: dd}; case DrawLayerCntlr.REGION_REMOVE_ENTRY: if (regionChanges) { removeRegionsFromData(drawLayer, regionChanges); Object.keys(dd[DataTypes.DATA]).forEach((plotId) => { set(dd[DataTypes.DATA], plotId, drawLayer.drawObjAry); }); } return {drawData: dd}; default: return null; } } function getDrawData(dataType, plotId, drawLayer, action, lastDataRet) { const {highlightedRegion, drawObjAry, selectMode} = drawLayer; switch (dataType) { case DataTypes.DATA: // based on the same drawObjAry to draw the region on each plot return isEmpty(lastDataRet) ? drawObjAry || plotAllRegions(drawLayer) : lastDataRet; case DataTypes.HIGHLIGHT_DATA: // create the region drawObj based on the original region for upright case. return isEmpty(lastDataRet) ? plotHighlightRegion(highlightedRegion, plotId, selectMode) : lastDataRet; } return null; } /** * @summary create DrawingObj for all regions * @param {Object} dl drawing layer * @returns {Object[]} */ function plotAllRegions(dl) { var {dataFrom, regionAry} = dl; //regionAry: array of region strings //dataFrom: from 'json' (server) or 'ds9' (original ds9 description) if (!regionAry) { return []; } dl.regions = (dataFrom === 'json') ? RegionFactory.parseRegionJson(regionAry) : RegionFactory.parseRegionDS9(regionAry); dl.drawObjAry = drawRegions(dl.regions); //no need regionAry anymore return dl.drawObjAry; } /** * @summary create DrawingObj for highlighted region * @param {Object} highlightedObj * @param {string} plotId * @param {Object} selectMode * @returns {Object[]} */ function plotHighlightRegion(highlightedObj, plotId, selectMode) { if (!highlightedObj) { return []; } if (highlightedObj.region) highlightedObj.region.highlighted = 1; var hObj = [DrawOp.makeHighlight(highlightedObj, primePlot(visRoot(), plotId), selectMode)]; hObj.forEach((oneObj) => { oneObj.highlight = 1; }); return hObj; } /** * @summary add new DrawingObj into originally displayed DrawingObj set * @param {Object} drawLayer * @param {string|string[]} addedRegions * @returns {Object[]} */ function addRegionsToData(drawLayer, addedRegions) { var {regions, drawObjAry: lastData} = drawLayer; var resultRegions = regions ? regions.slice() : []; var allDrawobjs = lastData ? lastData.slice() : []; if (!isEmpty(addedRegions)) { var allRegions = isString(addedRegions) ? [addedRegions] : addedRegions; var rgObj = RegionFactory.parseRegionDS9(allRegions); resultRegions = rgObj.reduce ( (prev, aRegion) => { var newDrawobj = addNewRegion(prev, aRegion); if (newDrawobj) { prev.push(aRegion); allDrawobjs.push(newDrawobj); } return prev; }, resultRegions); } drawLayer.regions = resultRegions; drawLayer.drawObjAry = allDrawobjs; return allDrawobjs; } /** * remove DrawingObj from originally displayed DrawingObj set * @param {Object} drawLayer * @param {string|string[]} removedRegions * @returns {Object[]} */ function removeRegionsFromData(drawLayer, removedRegions) { var {regions, drawObjAry: lastData} = drawLayer; var resultRegions = regions ? regions.slice() : []; var allDrawObjs = lastData ? lastData.slice() : []; if (resultRegions.length === 0) { return []; // no region to be removed } if (!isEmpty(removedRegions)) { var allRegions = isString(removedRegions) ? [removedRegions] : removedRegions; var rgObj = RegionFactory.parseRegionDS9(allRegions); resultRegions = rgObj.reduce((prev, rmRegion) => { var {index, regions} = removeRegion( prev, rmRegion ); if (index >= 0) { allDrawObjs.splice(index, 1); prev = regions; } return prev; }, resultRegions); } drawLayer.regions = resultRegions; drawLayer.drawObjAry = allDrawObjs; return allDrawObjs; } /** * @summary find the region drawObj based on region description * @param {object} drawLayer * @param {string|string[]} regionDes * @param {int} stopIndex maximum number of regions to be selected * @return {Object[]} if no region is found, an empty array is return. */ function getSelectedRegionDrawObj(drawLayer, regionDes, stopIndex = 1) { var {regions, drawObjAry} = drawLayer; var regs = RegionFactory.parseRegionDS9((isString(regionDes) ? [regionDes] : regionDes), true, stopIndex); var selDrawObj = []; if (!isEmpty(regs)) { selDrawObj = regs.reduce((prev, aRegion, index) => { if (index < stopIndex) { var idx = getRegionIndex(regions, aRegion); if (idx >= 0) { prev.push(drawObjAry[idx]); } } return prev; }, []); } return selDrawObj.slice(0, stopIndex); } /** * @summary get the region description of the selected region from the specified drawing layer * @param {string} drawLayerId id of the drawing layer * @return {string} description of the selected region * @public * @function getSelectedRegion * @memberof firefly.util.image */ export function getSelectedRegion(drawLayerId) { var drawLayer = getDrawLayerById(flux.getState()[DRAWING_LAYER_KEY], drawLayerId); return get(drawLayer, 'selectRegionDesc', ''); }