Source: visualize/task/PlotImageTask.js

/*
 * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
 */

import {flatten, flattenDeep, isArray, uniqueId, uniqBy, get, isEmpty} from 'lodash';
import {WebPlotRequest, GridOnStatus} from '../WebPlotRequest.js';
import ImagePlotCntlr, {visRoot, makeUniqueRequestKey, IMAGE_PLOT_KEY} from '../ImagePlotCntlr.js';
import {dlRoot, dispatchCreateDrawLayer, dispatchAttachLayerToPlot} from '../DrawLayerCntlr.js';
import {dispatchActiveTarget, getActiveTarget} from '../../core/AppDataCntlr.js';
import {WebPlot,PlotAttribute, RDConst, isImage} from '../WebPlot.js';
import CsysConverter from '../CsysConverter.js';
import VisUtils from '../VisUtil.js';
import {PlotState} from '../PlotState.js';
import Point, {makeImagePt} from '../Point.js';
import {WPConst, DEFAULT_THUMBNAIL_SIZE} from '../WebPlotRequest.js';
import {Band} from '../Band.js';
import {PlotPref} from '../PlotPref.js';
// import ActiveTarget  from '../../drawingLayers/ActiveTarget.js';
import {clone} from '../../util/WebUtil.js';
import {makePostPlotTitle} from '../reducer/PlotTitle.js';
import {dispatchAddViewerItems, getMultiViewRoot, findViewerWithItemId, EXPANDED_MODE_RESERVED, IMAGE, DEFAULT_FITS_VIEWER_ID} from '../MultiViewCntlr.js';
import {getPlotViewById, getDrawLayerByType, getDrawLayersByType, getDrawLayerById, getPlotViewIdList} from '../PlotViewUtil.js';
import {enableMatchingRelatedData, enableRelatedDataLayer} from '../RelatedDataUtil.js';
import {modifyRequestForWcsMatch} from './WcsMatchTask.js';
import WebGrid from '../../drawingLayers/WebGrid.js';
import HiPSGrid from '../../drawingLayers/HiPSGrid.js';
import {getDlAry} from '../DrawLayerCntlr.js';
import HiPSMOC from '../../drawingLayers/HiPSMOC.js';
import {dispatchPlotProgressUpdate, dispatchRecenter, dispatchWcsMatch} from '../ImagePlotCntlr';
import {isDefined} from '../../util/WebUtil';
import {HdrConst} from '../FitsHeaderUtil.js';
import {doFetchTable} from '../../tables/TableUtil';
import ImageRoot from '../../drawingLayers/ImageRoot';

//======================================== Exported Functions =============================
//======================================== Exported Functions =============================


export function ensureWPR(inVal) {
    if (isArray(inVal)) {
        return inVal.map( (v) => WebPlotRequest.makeFromObj(v));
    }
    else {
        return WebPlotRequest.makeFromObj(inVal);
    }
}

export function determineViewerId(viewerId, plotId) {
    if (viewerId) return viewerId;
    const newViewerId= findViewerWithItemId(getMultiViewRoot(), plotId, IMAGE);
    return newViewerId || DEFAULT_FITS_VIEWER_ID;
}

export function getHipsImageConversion(hipsImageConversion ) {
    return hipsImageConversion && {
        hipsRequestRoot:ensureWPR(hipsImageConversion.hipsRequestRoot),
        imageRequestRoot:ensureWPR(hipsImageConversion.imageRequestRoot),
        allSkyRequest: ensureWPR(hipsImageConversion.allSkyRequest),
        fovDegFallOver: hipsImageConversion.fovDegFallOver,
        fovMaxFitsSize: hipsImageConversion.fovMaxFitsSize,
        plotAllSkyFirst: hipsImageConversion.plotAllSkyFirst,
        autoConvertOnZoom: hipsImageConversion.autoConvertOnZoom
    };

}

const getFirstReq= (wpRAry) => isArray(wpRAry) ? wpRAry.find( (r) => Boolean(r)) : wpRAry;


function makeSinglePlotPayload(vr, rawPayload, requestKey) {

   const {threeColor, attributes, setNewPlotAsActive= true,
         holdWcsMatch= true, useContextModifications= true, enableRestore= true,
         renderTreeId}= rawPayload;
   let {plotId, wpRequest, pvOptions= {}}= rawPayload;

    wpRequest= ensureWPR(wpRequest);

    const hipsImageConversion= getHipsImageConversion(rawPayload.hipsImageConversion);
    if (hipsImageConversion) pvOptions= clone(pvOptions, {hipsImageConversion});

    const req= getFirstReq(wpRequest);


    if (isArray(wpRequest)) {
        if (!plotId) plotId= req.getPlotId() || uniqueId('defaultPlotId-');
        wpRequest.forEach( (r) => {if (r) r.setPlotId(plotId);});
    }
    else {
        if (!plotId) plotId= req.getPlotId() || uniqueId('defaultPlotId-');
        wpRequest.setPlotId(plotId);
    }

    if (vr.wcsMatchType && vr.mpwWcsPrimId && holdWcsMatch) {
        const wcsPrim= getPlotViewById(vr,vr.mpwWcsPrimId);
        wpRequest= isArray(wpRequest) ?
            wpRequest.map( (r) => modifyRequestForWcsMatch(wcsPrim, r)) :
            modifyRequestForWcsMatch(wcsPrim, wpRequest);
    }

    const payload= { plotId: req.getPlotId(),
                     plotGroupId: req.getPlotGroupId(),
                     groupLocked: req.isGroupLocked(),
                     viewerId: determineViewerId(rawPayload.viewerId, plotId),
                     hipsImageConversion,
                     requestKey, attributes, pvOptions, enableRestore,
                     useContextModifications, threeColor, setNewPlotAsActive, renderTreeId};

    const existingPv= getPlotViewById(vr,plotId);
    if (existingPv) {
        payload.oldOverlayPlotViews= {[plotId] :existingPv.overlayPlotViews};
    }

    if (threeColor) {
        if (isArray(wpRequest)) {
            payload.redReq= addRequestKey(wpRequest[Band.RED.value], requestKey);
            payload.greenReq= addRequestKey(wpRequest[Band.GREEN.value], requestKey);
            payload.blueReq= addRequestKey(wpRequest[Band.BLUE.value], requestKey);
        }
        else {
            payload.redReq= addRequestKey(wpRequest,requestKey);
        }
    }
    else {
        payload.wpRequest= addRequestKey(wpRequest,requestKey);
    }

    return payload;
}


/**
 *
 * @param rawAction
 * @return {Function}
 */
export function makePlotImageAction(rawAction) {
    return (dispatcher, getState) => {

        let vr= getState()[IMAGE_PLOT_KEY];
        const {wpRequestAry}= rawAction.payload;
        let payload;
        const requestKey= makeUniqueRequestKey('plotRequestKey');

        if (!wpRequestAry) {
            payload= makeSinglePlotPayload(vr, rawAction.payload, requestKey);
        }
        else {
            const {viewerId=DEFAULT_FITS_VIEWER_ID, attributes,
                   setNewPlotAsActive= true, pvOptions= {},
                   useContextModifications= true, enableRestore= true,
                   renderTreeId}= rawAction.payload;
            payload= {
                wpRequestAry:ensureWPR(wpRequestAry),
                viewerId,
                attributes,
                pvOptions,
                setNewPlotAsActive,
                threeColor:false,
                useContextModifications,
                enableRestore,
                groupLocked:true,
                requestKey,
                renderTreeId
            };

            payload.wpRequestAry= payload.wpRequestAry.map( (req) =>
                            addRequestKey(req,makeUniqueRequestKey('groupItemReqKey-'+req.getPlotId())));


            payload.oldOverlayPlotViews= wpRequestAry
                .map( (wpr) => getPlotViewById(vr,wpr.getPlotId()))
                .filter( (pv) => get(pv, 'overlayPlotViews'))
                .reduce( (obj, pv) => {
                    obj[pv.plotId]= pv.overlayPlotViews;
                    return obj;
            },{});

            if (vr.wcsMatchType && vr.mpwWcsPrimId && rawAction.payload.holdWcsMatch) {
                const wcsPrim= getPlotViewById(vr,vr.mpwWcsPrimId);
                payload.wpRequestAry= payload.wpRequestAry.map( (wpr) => modifyRequestForWcsMatch(wcsPrim, wpr));
            }
        }

        // if (!getDrawLayerByType(getDlAry(), ActiveTarget.TYPE_ID)) {
        //     initBuildInDrawLayers();
        // }

        payload.requestKey= requestKey;
        payload.plotType= 'image';

        vr= getState()[IMAGE_PLOT_KEY];

        if (vr.wcsMatchType && !rawAction.payload.holdWcsMatch) {
            dispatcher({ type: ImagePlotCntlr.WCS_MATCH, payload: {wcsMatchType:false} });
        }



        dispatcher( { type: ImagePlotCntlr.PLOT_IMAGE_START,payload});
        // NOTE - saga ImagePlotter handles next step
        // NOTE - saga ImagePlotter handles next step
        // NOTE - saga ImagePlotter handles next step
    };
}


function addRequestKey(r,requestKey) {
    if (!r) return;
    r= r.makeCopy();
    r.setRequestKey(requestKey);
    return r;
}



//======================================== Private ======================================
//======================================== Private ======================================
//======================================== Private ======================================


/**
 *
 * @param {object} pvCtx
 * @param {WebPlotRequest} r
 * @param {Band} band
 * @param useCtxMods
 * @return {WebPlotRequest}
 */
export function modifyRequest(pvCtx, r, band, useCtxMods) {

    if (!r) return r;

    const retval= r.makeCopy();

    if (retval.getRotateNorth()) retval.setRotateNorth(false);
    if (retval.getRotate()) retval.setRotate(false);
    if (retval.getRotationAngle()) retval.setRotationAngle(0);

    if (!pvCtx || !useCtxMods) return retval;

    if (pvCtx.defThumbnailSize!==DEFAULT_THUMBNAIL_SIZE && !r.containsParam(WPConst.THUMBNAIL_SIZE)) {
        retval.setThumbnailSize(pvCtx.defThumbnailSize);
    }


    const cPref= PlotPref.getCacheColorPref(pvCtx.preferenceColorKey);
    if (cPref) {
        if (cPref[band]) retval.setInitialRangeValues(cPref[band]);
        retval.setInitialColorTable(cPref.colorTableId);
    }

    const zPref= PlotPref.getCacheZoomPref(pvCtx.preferenceZoomKey);
    if (zPref) {
        retval.setInitialZoomLevel(zPref.zooomLevel);
    }

    return retval;

}
/**
 *
 * @param dispatcher
 * @param {object} payload the payload of the original action
 * @param {object} result the result of the search
 */

/**
 *
 * @param dispatcher
 * @param {object} payload the payload of the original action
 * @param {object} result the result of the search
 */
export function processPlotImageSuccessResponse(dispatcher, payload, result) {
    let successAry= [];
    let failAry= [];

     // the following line checks to see if we are processing the results from the right request
    if (payload.requestKey && result.requestKey && payload.requestKey!==result.requestKey) return;

    if (result.success && Array.isArray(result.data)) {
        successAry= result.data.filter( (d) => d.data.success);
        failAry= result.data.filter( (d) => !d.data.success);
    }
    else {
        if (result.success) successAry= [{data:result}];
        else                failAry= [{data:result}];
    }

    successAry.forEach( (r) => {
        const plotState= PlotState.makePlotStateWithJson(r.data.PlotCreate[0].plotState);
        const wpRequest= r.data.PlotCreateHeader ?  WebPlotRequest.parse(r.data.PlotCreateHeader.plotRequestSerialize) : plotState.getWebPlotRequest();
        dispatchPlotProgressUpdate(wpRequest.getPlotId(), 'Loading Images', false,wpRequest.getRequestKey());
    });

    lookForRelatedDataThenContinue(successAry,failAry, payload, dispatcher);

}

function lookForRelatedDataThenContinue(successAry,failAry, payload, dispatcher) {
    setTimeout( () => {
        const promiseAry= [Promise.resolve()];
        successAry.forEach( (s) => s.data.PlotCreate.forEach( (pc) => {
            const tType= pc.relatedData && pc.relatedData.find( (r) => r.dataType==='WAVELENGTH_TABLE');
            if (tType) {
                const p= doFetchTable(tType.searchParams).then( (wlTable) => {
                    pc.relatedData.push({dataType:'WAVELENGTH_TABLE_RESOLVED',dataKey:tType.dataKey+'-resolved', table:wlTable});
                });
                promiseAry.push(p);
            }
        }));
        Promise.all(promiseAry).then( () =>continuePlotImageSuccess(dispatcher, payload, successAry, failAry));
    } , 5);
}

function continuePlotImageSuccess(dispatcher, payload, successAry, failAry) {

    if (successAry.length) {
        const pvNewPlotInfoAry= successAry.map( (r) => handleSuccessfulCall(r.data.PlotCreate, r.data.PlotCreateHeader,payload, r.data.requestKey) );
        const resultPayload= Object.assign({},payload, {pvNewPlotInfoAry});
        dispatcher({type: ImagePlotCntlr.PLOT_IMAGE, payload: resultPayload});
        const plotIdAry = pvNewPlotInfoAry.map((info) => info.plotId);
        dispatcher({type: ImagePlotCntlr.ANY_REPLOT, payload: {plotIdAry}});

        matchAndActivateOverlayPlotViewsByGroup(plotIdAry);


        pvNewPlotInfoAry
            .forEach((info) => info.plotAry
                .forEach( (p)  => {
                    const pv= getPlotViewById(visRoot(),p.plotId);
                    addDrawLayers(p.plotState.getWebPlotRequest(), pv, p);
                    if (p.attributes[PlotAttribute.INIT_CENTER]) dispatchRecenter({plotId:p.plotId});
                } ));



        //todo- this this plot is in a group and locked, make a unique list of all the drawing layers in the group and add to new
        dispatchAddViewerItems(EXPANDED_MODE_RESERVED, plotIdAry, IMAGE);

        const vr= visRoot();
        if (vr.wcsMatchType && vr.positionLock) {
            dispatchWcsMatch( {plotId:vr.activePlotId, matchType:vr.wcsMatchType, lockMatch:true});
        }
    }


    failAry.forEach( (r) => {
        const {data}= r;
        if (payload.plotId) dispatchAddViewerItems(EXPANDED_MODE_RESERVED, [payload.plotId], IMAGE);
        const failPayload= {
            ...payload,
            briefDescription: data.briefFailReason,
            description: 'Failed- ' + data.userFailReason,
            detailFailReason: data.detailFailReason,
            plotId: data.plotId,
        };
        dispatcher( { type: ImagePlotCntlr.PLOT_IMAGE_FAIL, payload:failPayload} );
    });
}


export function addDrawLayers(request, pv, plot) {
    const {plotId}= plot;



    const fixedLayers= getDrawLayersByType(getDlAry(), ImageRoot.TYPE_ID);
    let newDL= fixedLayers.find( (dl) => dl.plotId===plot.plotId);
    if (!newDL) {
        newDL= dispatchCreateDrawLayer(ImageRoot.TYPE_ID, {plotId});
        dispatchAttachLayerToPlot(newDL.drawLayerId, pv.plotId, false);
    }

    request.getOverlayIds().forEach((drawLayerTypeId)=> {
        const dls = getDrawLayersByType(dlRoot(), drawLayerTypeId);
        dls.forEach((dl) => {
            if (dl.canAttachNewPlot) {
                const visibility = (dl.drawLayerTypeId === HiPSGrid.TYPE_ID) ||
                                    (dl.drawLayerTypeId === HiPSMOC.TYPE_ID && isEmpty(dl.visiblePlotIdAry))
                                    ? false : true;
                dispatchAttachLayerToPlot(dl.drawLayerId, plotId,  true, visibility, false);
            }
        });
    });

    if (request.getGridOn()!==GridOnStatus.FALSE && isImage(plot)) {
        const dl = getDrawLayerByType(dlRoot(), WebGrid.TYPE_ID);
        const useLabels= request.getGridOn()===GridOnStatus.TRUE;
        if (!dl) dispatchCreateDrawLayer(WebGrid.TYPE_ID, {useLabels});
        dispatchAttachLayerToPlot(WebGrid.TYPE_ID, plotId, false);
    }

    if (plot.relatedData) {
        plot.relatedData.forEach( (rd) => {
            if (rd.dataType === RDConst.TABLE) {
                const dl = getDrawLayerById(dlRoot(), rd.relatedDataId);
                if (!dl) enableRelatedDataLayer(visRoot(), getPlotViewById(visRoot(), plotId), rd);
            }

        });
    }
}



// function getRequest(payload) {
//     return payload.wpRequest || payload.redReq ||  payload.blueReq ||  payload.greenReq;
// }

 /**
 * @global
 * @public
 * @typedef {Object} PvNewPlotInfo
 * @summary Main part of the payload of successful call to the server
 *
 * @prop {String} plotId,
 * @prop {String} requestKey,
 * @prop {WebPlot[]} plotAry
 * @prop {OverPlotView[]} overlayPlotViews
 *
 */

 function findCubePlane(plotCreate) {
     const plotState= PlotState.makePlotStateWithJson(plotCreate.plotState);
     if (plotState.isThreeColor()) return -1;
     if (plotCreate.headerAry && isDefined(plotCreate.headerAry[0][HdrConst.SPOT_PL])) { // this should be the zero plane of the cube
         return Number(plotCreate.headerAry[0][HdrConst.SPOT_PL].value);
     }
     else if (!plotCreate.headerAry) {  // if no headerAry, it is a plane of the cube
         return plotState.getCubePlaneNumber();
     }
     else {
         return -1;
     }
 }



 function populateBandStateFromHeader(bandState, plotCreateHeader) {
     bandState.plotRequestSerialize = plotCreateHeader.plotRequestSerialize;
     bandState.uploadFileNameStr= plotCreateHeader.uploadFileNameStr;
     bandState.originalFitsFileStr= plotCreateHeader.originalFitsFileStr;
     bandState.workingFitsFileStr= plotCreateHeader.workingFitsFileStr;
 }


/**
 * readds the data into each plotCreate from the plotCreateHeader.
 * The data in the header is replicated in each plot and was clear for network transfere
 * efficiency. This optimization is very import for large cubes.
 * Note- data is modified in place plotCreate will me changed, no new object is created.
 * @param plotCreateHeader
 * @param plotCreate
 */
export function populateFromHeader(plotCreateHeader, plotCreate) {
     if (!plotCreateHeader) return;
     for(let i=0; i<plotCreate.length; i++) {
         if (isArray(plotCreate[0].bandStateAry)) {
             for (let j = 0; j < 3; j++) {
                 if (plotCreate[0].bandStateAry[j]) {
                     populateBandStateFromHeader(plotCreate[i].plotState.bandStateAry[j],plotCreateHeader);
                 }
             }
         }
         else {
             populateBandStateFromHeader(plotCreate[i].plotState.bandStateAry,plotCreateHeader);
         }
         plotCreate[i].dataDesc= plotCreateHeader.dataDesc;
         plotCreate[i].zeroHeaderAry= plotCreateHeader.zeroHeaderAry;
     }
 }

/**
 *
 * @param {Array.<Object>} plotCreate
 * @param {Object} plotCreateHeader
 * @param payload
 * @param requestKey
 * @return {PvNewPlotInfo}
 */
function handleSuccessfulCall(plotCreate, plotCreateHeader, payload, requestKey) {
    // const plotCreate= plotCreateStrAry.map( (s) => JSON.parse(s));

    populateFromHeader(plotCreateHeader, plotCreate);
    let cubeStartIdx=-1;
    const cubeCtxAry= plotCreate
        .map( (pC,idx) => {
            const cubePlane= findCubePlane(pC);
            if (cubePlane===0) cubeStartIdx= idx;
            const cubeStartPC= plotCreate[cubeStartIdx];
            return cubePlane>-1 ? {
                cubePlane,
                cubeHeaderAry: cubeStartPC.headerAry,
                relatedData: cubeStartPC.relatedData,
                dataWidth: cubeStartPC.dataWidth,
                dataHeight: cubeStartPC.dataHeight,
                imageCoordSys: cubeStartPC.imageCoordSys
            } : undefined;
        });


    const plotState= PlotState.makePlotStateWithJson(plotCreate[0].plotState);
    const plotId= plotState.getWebPlotRequest().getPlotId();

    const plotAry= plotCreate.map((wpInit,idx) => makePlot(wpInit,plotId, payload.attributes, cubeCtxAry[idx]) );
    if (plotAry.length) updateActiveTarget(plotAry[0]);
    return {plotId, requestKey, plotAry, overlayPlotViews:null};
}

function makePlot(wpInit,plotId, attributes, cubeCtx) {
    const plot= WebPlot.makeWebPlotData(plotId, wpInit, {}, false, cubeCtx);
    const r= plot.plotState.getWebPlotRequest();
    plot.title= makePostPlotTitle(plot,r);
    if (r.isMinimalReadout()) plot.attributes[PlotAttribute.MINIMAL_READOUT]= true;
    if (r.getRelatedTableRow()>-1) plot.attributes[PlotAttribute.TABLE_ROW]= r.getRelatedTableRow();
    if (r.getRelatedTableId()) plot.attributes[PlotAttribute.TABLE_ID]= r.getRelatedTableId();
    Object.assign(plot.attributes,attributes);
    return plot;
}


/**
 * @param {WebPlot} plot
 */
function updateActiveTarget(plot) {
    if (!plot) return;

    const req= plot.plotState.getWebPlotRequest();
    if (!req) return;

    let corners;
    let activeTarget;


    if (!getActiveTarget()) {
        const circle = req.getRequestArea(); if (req.getOverlayPosition())     activeTarget= req.getOverlayPosition();
        else if (circle && circle.center) activeTarget= circle.center;
        else                              activeTarget= VisUtils.getCenterPtOfPlot(plot);

    }

    if (req.getSaveCorners()) {
        const w= plot.dataWidth;
        const h= plot.dataHeight;
        const cc= CsysConverter.make(plot);
        const pt1= cc.getWorldCoords(makeImagePt(0, 0));
        const pt2= cc.getWorldCoords(makeImagePt(w, 0));
        const pt3= cc.getWorldCoords(makeImagePt(w,h));
        const pt4= cc.getWorldCoords(makeImagePt(0, h));
        if (pt1 && pt2 && pt3 && pt4) {
            corners= [pt1,pt2,pt3,pt4];
        }
    }

    if (activeTarget || corners) dispatchActiveTarget(activeTarget,corners);
}

// export function initBuildInDrawLayers() {
//     dispatchCreateDrawLayer(ActiveTarget.TYPE_ID);
// }

/**
 *
 * @param {String[]} plotIdAry
 * @param {Object.<string, OverlayPlotView[]>} oldOverlayPlotViews
 */
function matchAndActivateOverlayPlotViews(plotIdAry, oldOverlayPlotViews) {
    // plotIdAry.forEach( (plotId) => dispatchDeleteOverlayPlot({plotId, deleteAll:true}));

    plotIdAry
        .map( (plotId) => getPlotViewById(visRoot(), plotId))
        .filter( (pv) => pv)
        .forEach( (pv) => enableMatchingRelatedData(pv,oldOverlayPlotViews[pv.plotId]));
}



/**
 *
 * @param {String[]} plotIdAry
 */
function matchAndActivateOverlayPlotViewsByGroup(plotIdAry) {
    const vr= visRoot();
    plotIdAry
        .map( (plotId) => getPlotViewById(visRoot(), plotId))
        .filter( (pv) => pv)
        .forEach( (pv) => {
            const opvMatchArray= uniqBy(flatten(getPlotViewIdList(vr, pv.plotId)
                                                       .filter( (id) => id!== pv.plotId)
                                                       .map( (id) => getPlotViewById(vr,id))
                                                       .map( (gpv) => gpv.overlayPlotViews)),
                                   'maskNumber' );
            enableMatchingRelatedData(pv,opvMatchArray);
        });
}