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