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