/*
* License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
*/
import {get} from 'lodash';
import update from 'immutability-helper';
import Enum from 'enum';
import {PlotAttribute, isImage} from './../WebPlot.js';
import {clone} from '../../util/WebUtil.js';
import {WPConst} from './../WebPlotRequest.js';
import {makeScreenPt, makeDevicePt} from './../Point.js';
import {getActiveTarget} from '../../core/AppDataCntlr.js';
import VisUtil from './../VisUtil.js';
import {getPlotViewById, matchPlotViewByPositionGroup, primePlot, findCurrentCenterPoint, findPlotGroup} from './../PlotViewUtil.js';
import {changeProjectionCenter} from '../HiPSUtil.js';
import {UserZoomTypes} from '../ZoomUtil.js';
import {ZoomType} from '../ZoomType.js';
import {PlotPref} from './../PlotPref.js';
import {DEFAULT_THUMBNAIL_SIZE} from '../WebPlotRequest.js';
import {CCUtil, CysConverter} from './../CsysConverter.js';
import {getDefMenuItemKeys} from '../MenuItemKeys.js';
import {ExpandType, WcsMatchType} from '../ImagePlotCntlr.js';
import {updateTransform, makeTransform} from '../PlotTransformUtils.js';
import {isHiPS} from '../WebPlot.js';
import {makeImagePt} from './../Point';
export const ServerCallStatus= new Enum(['success', 'working', 'fail'], { ignoreCase: true });
/**
* @global
* @public
* @typedef {Object} PlotView
*
* There is one PlotView object for each react ImageViewer. A PlotView is uniquely identified by the plotId. The
* plot id will not change for the life time of the plotView. A plot view can be connected to a plot group. That is done
* by the plotGroupId. There will be several plotViews in a plot group.
*
* PlotView is mostly about the viewing of the plot. The plot data is contained in a WebPlot. A plotView can have an
* array of WebPlots. The array length will only be one for normals fits files and n for multi image fits and cube fits
* files. plots[primeIdx] refers to the plot currently showing in the plot view.
*
* @prop {String} plotId, immutable
* @prop {String} plotGroupId, immutable
* @prop {String} drawingSubGroupId, immutable
* @peop {boolean} visible true when we draw the base image
* @prop {WebPlot[]} plots all the plots that this plotView can show, usually the image in the fits file
* @prop {String} plottingStatus, end user description of the what is doing on
* @prop {String} serverCall, one of 'success', 'working', 'fail'
* @prop {number} primeIdx, which of the plots array is active
* @prop {number} scrollX scroll position X
* @prop {number} scrollY scroll position Y
* @prop {{width:number, height:number}} viewDim size of viewable area (div size: offsetWidth & offsetHeight)
* @prop {Object} menuItemKeys - which toolbar button are enables for this plotView
* @prop {Object} overlayPlotViews
* @prop {Object} options
* @prop {number} rotation if > 0 then the plot is rotated by this many degrees
* @prop {boolean} flipY if true, the the plot is flipped on the Y axis
* @prop {PlotViewContextData} plotViewCtx
*/
/**
* @global
* @public
* @typedef {Object} PlotViewContextData
* Various properties about this PlotView
*
* @prop {boolean} userCanDeletePlots true if this plotView can be deleted by the user
* @prop {boolean} zoomLockingEnabled the plot will automaticly adjust the zoom when resized
* @prop {UserZoomTypes} zoomLockingType the type of zoom lockeing
* @prop {number} lastCollapsedZoomLevel used for returning from expanded mode, keeps recode of the level before expanded
* @prop {HipsImageConversionSettings} hipsImageConversion - if defined, then plotview can convert between hips and image
* @prop {number} plotCounter index of how many plots, used for making next ID
*/
/**
* @global
* @public
* @typedef {Object} HipsImageConversionSettings
* @summary Parameters to do conversion between hips and images
*
* @prop {WebPlotParams|WebPlotRequest} hipsRequestRoot a request object that contains the base parameter to display a HiPS
* @prop {WebPlotParams|WebPlotRequest} imageRequestRoot a request object that contains the base parameter to display an image. It must be a service type.
* @prop {number} fovDegFallOver The field of view size to determine when to move between and HiPS and an image
* @prop {number} fovMaxFitsSize how big this fits image can be
* @prop {boolean} autoConvertOnZoom do auto convert on zoom
*/
/**
* @global
* @public
* @typedef {Object} PVCreateOptions
* Object used for creating the PlotView
*
* @prop {HipsImageConversionSettings} hipsImageConversion If object is defined and populated correctly then
* the PlotView will convert between HiPS and Image
* @prop {Object} menuItemKeys - defines which menu items shows on the toolbar
* @prop {boolean} userCanDeletePlots - default to true, defines if a PlotView can be deleted by the user
*/
/**
* @param {string} plotId
* @param {WebPlotRequest} req
* @param {PVCreateOptions} pvOptions options for this plot view
* @return {PlotView}
*/
export function makePlotView(plotId, req, pvOptions= {}) {
const pv= {
plotId, // should never change
plotGroupId: req.getPlotGroupId(), //should never change
drawingSubGroupId: req.getDrawingSubGroupId(), //todo, string, this is an id, should never change
plots:[],
visible: true,
request: req && req.makeCopy(),
plottingStatus:'Plotting...',
serverCall:'success', // one of 'success', 'working', 'fail'
primeIdx: -1,
scrollX : -1, // in ScreenCoords
scrollY : -1, // in ScreenCoords
affTrans: null,
viewDim : {width:0, height:0}, // size of viewable area (i.e. div size: offsetWidth & offsetHeight)
overlayPlotViews: [],
menuItemKeys: makeMenuItemKeys(req,pvOptions,getDefMenuItemKeys()), // normally will not change
plotViewCtx: createPlotViewContextData(req, pvOptions),
rotation: 0,
flipY: false,
flipX: false,
};
return pv;
}
/**
*
* @param {WebPlotRequest} req
* @param {PVCreateOptions} pvOptions
* @return {PlotViewContextData}
*/
function createPlotViewContextData(req, pvOptions={}) {
const plotViewCtx= {
userCanDeletePlots: get(pvOptions, 'userCanDeletePlots', true),
annotationOps : req.getAnnotationOps(), // how titles are drawn
rotateNorthLock : false,
zoomLockingEnabled : false,
zoomLockingType: UserZoomTypes.FIT, // can be FIT or FILL
displayFixedTarget: get(pvOptions, 'displayFixedTarget',true),
lastCollapsedZoomLevel: 0,
preferenceColorKey: req.getPreferenceColorKey(),
preferenceZoomKey: req.getPreferenceZoomKey(), // currently not used
defThumbnailSize: DEFAULT_THUMBNAIL_SIZE,
plotCounter:0 // index of how many plots, used for making next ID
};
const {hipsImageConversion:hi}= pvOptions;
if (hi && hi.hipsRequestRoot && hi.imageRequestRoot && hi.fovDegFallOver) { // confirm all three parameters are there
const defaults= {autoConvertOnZoom: false};
plotViewCtx.hipsImageConversion= {...defaults, ...hi};
if (!hi.fovMaxFitsSize ) hi.fovMaxFitsSize= hi.fovDegFallOver;
}
return plotViewCtx;
}
//todo - this function should determine which menuItem are visible and which are hidden
// for now just return the default
function makeMenuItemKeys(req,pvOptions,defMenuItemKeys) {
return Object.assign({},defMenuItemKeys, pvOptions.menuItemKeys);
}
/**
*
* @param pv
* @return {PlotView}
*/
export function initScrollCenterPoint(pv) {
if (isImage(primePlot(pv))) {
return updatePlotViewScrollXY(pv,findScrollPtForCenter(pv));
}
else {
const plot= primePlot(pv);
if (!plot || !plot.attributes[PlotAttribute.FIXED_TARGET]) return pv;
const wp= CCUtil.getWorldCoords(plot, plot.attributes[PlotAttribute.FIXED_TARGET]);
return replacePrimaryPlot(pv, changeProjectionCenter(plot,wp));
}
}
export function changePrimePlot(pv, nextIdx) {
const {plots}= pv;
if (!plots[nextIdx]) return pv;
const currentScrollImPt= CCUtil.getImageCoords(primePlot(pv),makeScreenPt(pv.scrollX,pv.scrollY));
//=================
pv= Object.assign({},pv,{primeIdx:nextIdx});
const cc= CysConverter.make(plots[nextIdx]);
if (cc.pointInData(currentScrollImPt)) {
pv= updatePlotViewScrollXY(pv,cc.getScreenCoords(currentScrollImPt));
}
else {
pv= initScrollCenterPoint(pv);
}
pv= updateTransform(pv);
return pv;
}
/**
* Replace the plotAry and overlayPlotViews into the PlotView, return a new PlotView
* @param {PlotView} pv
* @param {WebPlot[]} plotAry
* @param {Array} overlayPlotViews
* @param {ExpandType} expandedMode
* @param {boolean} newPlot true, this is a new plot otherwise is is from a flip, rotate, etc
* @return {PlotView}
*/
export function replacePlots(pv, plotAry, overlayPlotViews, expandedMode, newPlot) {
pv= clone(pv);
pv.plotViewCtx= clone(pv.plotViewCtx);
if (overlayPlotViews) {
const oPlotAry= overlayPlotViews.map( (opv) => opv.plot);
pv.overlayPlotViews= pv.overlayPlotViews.map( (opv) => {
const plot= oPlotAry.find( (p) => p.plotId===opv.imageOverlayId);
return plot ? clone(opv, {plot}) : opv;
});
}
if (newPlot || get(pv, 'plots.length') !== plotAry.length) {
pv.plots= plotAry;
}
else {
const oldPlots= pv.plots;
pv.plots= plotAry.map( (p,idx) => clone(p, {relatedData:oldPlots[idx].relatedData}) );
}
pv.plots.forEach( (plot) => {
plot.attributes= Object.assign({},plot.attributes, getNewAttributes(plot));
plot.plotImageId= `${pv.plotId}--${pv.plotViewCtx.plotCounter}`;
pv.plotViewCtx.plotCounter++;
});
if (pv.primeIdx<0 || pv.primeIdx>=pv.plots.length) pv.primeIdx=0;
pv.plottingStatus='';
pv.serverCall='success';
PlotPref.putCacheColorPref(pv.plotViewCtx.preferenceColorKey, pv.plots[pv.primeIdx].plotState);
PlotPref.putCacheZoomPref(pv.plotViewCtx.preferenceZoomKey, pv.plots[pv.primeIdx].plotState);
if (expandedMode===ExpandType.COLLAPSE) {
pv.plotViewCtx.lastCollapsedZoomLevel= pv.plots[pv.primeIdx].zoomFactor;
}
else {
pv.plotViewCtx.zoomLockingEnabled= primePlot(pv).plotState.getWebPlotRequest().getZoomType() !== ZoomType.LEVEL;
}
pv= initScrollCenterPoint(pv);
return pv;
}
/**
* create a copy of the PlotView with a new scroll position and a new view port if necessary
* The scroll position is the top left visible point.
* @param {PlotView} plotView the current plotView
* @param {Point} newScrollPt the screen point of the scroll position
* @return {PlotView} new copy of plotView
*/
export function updatePlotViewScrollXY(plotView,newScrollPt) {
if (!plotView) return plotView;
if (!newScrollPt) return Object.assign({},plotView, {scrollX:undefined, scrollY:undefined});
const plot= primePlot(plotView);
if (!plot) return plotView;
const {scrollWidth,scrollHeight}= getScrollSize(plotView);
if (!scrollWidth || !scrollHeight) return plotView;
const cc= CysConverter.make(plot);
newScrollPt= cc.getScreenCoords(newScrollPt);
const {x:newSx,y:newSy}= newScrollPt;
const newPlotView= Object.assign({},plotView, {scrollX:newSx, scrollY:newSy});
return updateTransform(newPlotView);
}
/**
* replace the PlotView in plotview array keyed by plotId
* @param {Array.<PlotView>} plotViewAry
* @param {PlotView} newPlotView
* @return {Array.<PlotView>} new plotView array after return a plotview
*/
export function replacePlotView(plotViewAry,newPlotView) {
return plotViewAry.map( (pv) => pv.plotId===newPlotView.plotId ? newPlotView : pv);
}
/**
*
* @param {PlotView} plotView
* @param {WebPlot} primePlot
* @return {PlotView} return the new PlotView object
*/
export function replacePrimaryPlot(plotView,primePlot) {
return update(plotView, { plots : {[plotView.primeIdx] : { $set : primePlot } }} );
}
/**
* scroll a plot view to a new screen pt, if positionLock is true then all the plot views in the group
* will be scrolled to match
* @param {VisRoot} visRoot
* @param {string} plotId plot id to set the scrolling on
* @param {Array} plotViewAry an array of plotView
* @param {Array} plotGroupAry the plotGroup array
* @param {ScreenPt} newScrollPt a screen point in the plot to scroll to
* @return {Array.<PlotView>}
*/
export function updatePlotGroupScrollXY(visRoot, plotId,plotViewAry, plotGroupAry, newScrollPt) {
const plotView= updatePlotViewScrollXY(getPlotViewById(plotViewAry, plotId), newScrollPt);
plotViewAry= replacePlotView(plotViewAry, plotView);
if (get(visRoot,'positionLock')) {
plotViewAry= matchPlotViewByPositionGroup(visRoot, plotView,plotViewAry,false, makeScrollPosMatcher(plotView, visRoot));
}
return plotViewAry;
}
/**
* Create a new plotView that will wcs match the scroll position of the master plotView.
* This function all all the safety checks for undefined plotview or plots. It is
* always safe to call.
* @param {WcsMatchType} wcsMatchType
* @param {PlotView} masterPv - master PlotView
* @param {PlotView} matchToPv - match to PlotView
* @return {PlotView} a new version of matchToPv with the scroll position matching
*/
export function updateScrollToWcsMatch(wcsMatchType, masterPv, matchToPv) {
if (!masterPv || !matchToPv || masterPv===matchToPv) return matchToPv;
if (masterPv.plotId===matchToPv.plotId || !primePlot(masterPv)|| !primePlot(matchToPv)) return matchToPv;
const newScrollPoint= findWCSMatchScrollPosition(wcsMatchType, masterPv, matchToPv);
return updatePlotViewScrollXY(matchToPv, newScrollPoint);
}
const PIXEL_MATCH_BY_CENTER= true;
/**
* Find a scroll point that the point puts the plot be scroll the to same wcs or target as the master plot
* To use this function the plot view objects and the primary plot objects must all be defined.
* @param {WcsMatchType} wcsMatchType
* @param {PlotView} masterPv - master PlotView
* @param {PlotView} matchToPv - match to PlotView
* @return {ScreenPt} the screen point offset
*/
function findWCSMatchScrollPosition(wcsMatchType, masterPv, matchToPv) {
const masterP= primePlot(masterPv);
const matchToP= primePlot(matchToPv);
const ccMaster= CysConverter.make(masterP);
const ccMatch= CysConverter.make(matchToP);
if (wcsMatchType===WcsMatchType.Standard) {
const centerMasterWorldPt= ccMaster.getWorldCoords(findCurrentCenterPoint(masterPv));
return findScrollPtToCenterImagePt( matchToPv, ccMatch.getImageCoords(centerMasterWorldPt));
}
else if (wcsMatchType===WcsMatchType.Target) {
if (!matchToP.attributes[PlotAttribute.FIXED_TARGET] || !masterP.attributes[PlotAttribute.FIXED_TARGET] ) {
return makeScreenPt(masterPv.scrollX, masterPv.scrollY);
}
const mastDevPt= ccMaster.getDeviceCoords(masterP.attributes[PlotAttribute.FIXED_TARGET]);
const matchPoint= ccMatch.getImageCoords(matchToP.attributes[PlotAttribute.FIXED_TARGET]);
return findScrollPtToPlaceOnDevPt( matchToPv, matchPoint, mastDevPt);
}
else if (wcsMatchType===WcsMatchType.PixelCenter) {
const centerMasterImagePt= findCurrentCenterPoint(masterPv);
const wDelta= (masterP.dataWidth - matchToP.dataWidth)/2;
const hDelta= (masterP.dataHeight - matchToP.dataHeight)/2;
return findScrollPtToCenterImagePt( matchToPv,
makeImagePt(centerMasterImagePt.x-wDelta, centerMasterImagePt.y-hDelta) );
}
else if (wcsMatchType===WcsMatchType.Pixel) {
const centerMasterImagePt= findCurrentCenterPoint(masterPv);
return findScrollPtToCenterImagePt( matchToPv, centerMasterImagePt);
}
else {
return makeScreenPt(masterPv.scrollX, masterPv.scrollY);
}
}
/**
* make a function that will match the scroll position of a plotview to the source plotview
* @param {PlotView} sourcePV the plotview that others will match to
* @param {VisRoot} visRoot
* @return {function} a function the takes the plotview to match scrolling as a parameter and
* returns the scrolled matched version
*/
function makeScrollPosMatcher(sourcePV, visRoot) {
const {scrollX:srcSx,scrollY:srcSy}= sourcePV;
const {wcsMatchType}= visRoot;
const sourcePlot= primePlot(sourcePV);
const {screenSize:{width:srcScreenWidth,height:srcScreenHeight}}= sourcePlot;
const {scrollWidth:srcSW,scrollHeight:srcSH}= getScrollSize(sourcePV);
const percentX= (srcSx+srcSW/2) / srcScreenWidth;
const percentY= (srcSy+srcSH/2) / srcScreenHeight;
return (pv) => {
let retPV= pv;
const plot= primePlot(pv);
if (plot) {
if (wcsMatchType) {
retPV= updateScrollToWcsMatch(visRoot.wcsMatchType, sourcePV, pv);
}
else {
const {screenSize:{width,height}}= plot;
const {scrollWidth:sw,scrollHeight:sh}= getScrollSize(pv);
const newSx= width*percentX - sw/2;
const newSy= height*percentY - sh/2;
retPV= updatePlotViewScrollXY(pv,makeScreenPt(newSx,newSy));
}
}
return retPV;
};
}
/**
*
* @param {object} plot
* @return {{}}
*/
function getNewAttributes(plot) {
//todo: figure out active target and how to set it
const attributes= {};
const req= plot.plotState.getWebPlotRequest();
if (!req) return attributes;
let worldPt;
const circle = req.getRequestArea();
if (req.containsParam(WPConst.OVERLAY_POSITION)) {
worldPt= req.getOverlayPosition();
}
else if (circle && circle.center) {
worldPt= circle.center;
}
else if (getActiveTarget()) {
worldPt= getActiveTarget().worldPt;
}
else {
worldPt= VisUtil.getCenterPtOfPlot(plot);
}
if (worldPt) {
const cc= CysConverter.make(plot);
if (isHiPS(plot) || cc.pointInPlot(worldPt) || req.getOverlayPosition()) {
attributes[PlotAttribute.FIXED_TARGET]= worldPt;
if (circle) attributes[PlotAttribute.REQUESTED_SIZE]= circle.radius; // says radius but really size
}
}
if (req.containsParam(WPConst.INITIAL_CENTER_POSITION)) {
attributes[PlotAttribute.INIT_CENTER]= req.getInitialCenterPosition();
}
if (req.getUniqueKey()) attributes[PlotAttribute.UNIQUE_KEY]= req.getUniqueKey();
if (req.isMinimalReadout()) attributes[PlotAttribute.MINIMAL_READOUT]=true;
return attributes;
}
/**
*
* @param {WebPlot} plot
* @param {{width: number, height: number}} viewDim
* @return {{scrollWidth: number, scrollHeight: number}}
*/
function computeScrollSizes(plot,viewDim) {
const {screenSize}= plot;
let scrollWidth= Math.min(screenSize.width,viewDim.width);
let scrollHeight= Math.min(screenSize.height,viewDim.height);
if (isNaN(scrollWidth)) scrollWidth= 0;
if (isNaN(scrollHeight)) scrollHeight= 0;
return {scrollWidth,scrollHeight};
}
/**
* @param {object} plotView
* @return {{scrollWidth: number, scrollHeight: number}}
*/
export const getScrollSize = (plotView) => computeScrollSizes(primePlot(plotView),plotView.viewDim);
/**
*
* @param {PlotView} plotView
* @return {ScreenPt}
*/
function findScrollPtForCenter(plotView) {
const {width,height}= plotView.viewDim;
const {width:scrW,height:scrH}= primePlot(plotView).screenSize;
const x= scrW/2- width/2;
const y= scrH/2- height/2;
return makeScreenPt(x,y);
}
/**
* find the scroll screen pt to put the image centered on the passed ImagePt
* @param {PlotView} plotView
* @param {ImagePt} ipt - if this is not an image point it will be converted to one
* @return {ScreenPt} the screen point to use as the scroll position
*/
export function findScrollPtToCenterImagePt(plotView, ipt) {
const {width,height}= plotView.viewDim;
return findScrollPtToPlaceOnDevPt(plotView,ipt, makeDevicePt(width/2,height/2));
}
/**
* Return the scroll point for a PlotView that will place the given image point on the given device point.
* or another way to say it:
* Given a device point and an image point, return the scroll point the would make the two line up.
* @param {PlotView} pv
* @param {ImagePt} ipt - if this is not an image point it will be converted to one
* @param {DevicePt} targetDevPtPos - the point on the device that the image
* @return {ScreenPt} the scroll position the places the image point on to the device point
*/
export function findScrollPtToPlaceOnDevPt(pv, ipt, targetDevPtPos) {
const plot= primePlot(pv);
// make a CsysConverter for a image that has a scroll position of 0,0
const altAffTrans= makeTransform(0,0, 0, 0, pv.rotation, pv.flipX, pv.flipY, pv.viewDim);
const cc= CysConverter.make(plot,altAffTrans);
const point= cc.getScreenCoords(ipt);
if (!point) return null;
const target= cc.getScreenCoords(targetDevPtPos);
if (!target) return null;
const x= point.x - target.x;
const y= point.y - target.y;
return makeScreenPt(pv.flipY ? -x : x,pv.flipX ? -y : y);
}