/*
* License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
*/
import {get,isEmpty,isArray} from 'lodash';
import {RequestType} from './RequestType.js';
import {clone} from '../util/WebUtil.js';
import CoordinateSys from './CoordSys.js';
import {makeProjection, makeProjectionNew} from './projection/Projection.js';
import PlotState from './PlotState.js';
import BandState from './BandState.js';
import {makeWorldPt, makeScreenPt} from './Point.js';
import {changeProjectionCenter} from './HiPSUtil.js';
import {CysConverter} from './CsysConverter.js';
import {makeImagePt} from './Point';
import {convert} from './VisUtil.js';
import {parseSpacialHeaderInfo, makeDirectFileAccessData} from './projection/ProjectionHeaderParser.js';
import {UNSPECIFIED, UNRECOGNIZED } from './projection/Projection.js';
import {getImageCubeIdx} from './PlotViewUtil.js';
import {parseWavelengthHeaderInfo} from './projection/WavelengthHeaderParser.js';
import {geAtlProjectionIDs} from './FitsHeaderUtil.js';
import {TAB} from './projection/Wavelength';
export const RDConst= {
IMAGE_OVERLAY: 'IMAGE_OVERLAY',
IMAGE_MASK: 'IMAGE_MASK',
TABLE: 'TABLE',
WAVELENGTH_TABLE: 'WAVELENGTH_TABLE',
SUPPORTED_DATATYPES: ['IMAGE_MASK', 'TABLE']
};
const HIPS_DATA_WIDTH= 10000000000;
const HIPS_DATA_HEIGHT= 10000000000;
export const getHiPsTitleFromProperties= (hipsProperties) => hipsProperties.obs_title || hipsProperties.label || 'HiPS';
/**
* FITS headers keys
* todo: add more headers
*/
export const PlotAttribute= {
MOVING_TARGET_CTX_ATTR: 'MOVING_TARGET_CTX_ATTR',
/**
* This will probably be a WebMouseReadoutHandler class
* @see WebMouseReadoutHandler
*/
READOUT_ATTR: 'READOUT_ATTR',
READOUT_ROW_PARAMS: 'READOUT_ROW_PARAMS',
/**
* This will probably be a WorldPt
* Used to overlay a target associated with this image
*/
FIXED_TARGET: 'FIXED_TARGET',
/**
*
*/
INIT_CENTER: 'INIT_CENTER',
/**
* This will probably be a double with the requested size of the plot
*/
REQUESTED_SIZE: 'REQUESTED_SIZE',
/**
* This will probably an object represent a rectangle {pt0: point,pt1: point}
* @See ./Point.js
*/
SELECTION: 'SELECTION',
IMAGE_BOUNDS_SELECTION: 'IMAGE_BOUNDS_SELECTION',
/**
* setting for outline image, bounds (for FootprintObj) or drawObj, text, textLoc,
*/
OUTLINEIMAGE_BOUNDS: 'OUTLINEIMAGE_BOUNDS',
OUTLINEIMAGE_TITLE: 'OUTLINEIMAGE_TITLE',
OUTLINEIMAGE_TITLELOC: 'OUTLINEIMAGE_TITLELOC',
OUTLINEIMAGE_DRAWOBJ: 'OUTLINE_OBJ',
/**
* This will probably an object to represent a line {pt0: point,pt1: point}
* @See ./Point.js
*/
ACTIVE_DISTANCE: 'ACTIVE_DISTANCE',
SHOW_COMPASS: 'SHOW_COMPASS',
/**
* This will probably an object {pt: point}
* @See ./Point.js
*/
ACTIVE_POINT: 'ACTIVE_POINT',
/**
* This is a String describing why this plot can't be rotated. If it is defined then
* rotating is disabled.
*/
DISABLE_ROTATE_REASON: 'DISABLE_ROTATE_HINT',
/**
* what should happen when multi-fits images are changed. If set the zoom is set to the same level
* eg 1x, 2x ect. If not set then flipping should attempt to make the image the same arcsec/screen pixel.
*/
FLIP_ZOOM_BY_LEVEL: 'FLIP_ZOOM_BY_LEVEL',
/**
* what should happen when multi-fits images are changed. If set the zoom is set to the same level
* eg 1x, 2x ect. If not set then flipping should attempt to make the image the same arcsec/screen pixel.
*/
FLIP_ZOOM_TO_FILL: 'FLIP_ZOOM_TO_FILL',
/**
* if set, when expanded the image will be zoom to no bigger than this level,
* this should be a subclass of Number
*/
MAX_EXPANDED_ZOOM_LEVEL : 'MAX_EXPANDED_ZOOM_LEVEL',
/**
* if set, this should be the last expanded single image zoom level.
* this should be a subclass of Number
*/
LAST_EXPANDED_ZOOM_LEVEL : 'LAST_EXPANDED_ZOOM_LEVEL',
/**
* if set, must be one of the string values defined by the enum ZoomUtil.FullType
* currently is is ONLY_WIDTH, WIDTH_HEIGHT, ONLY_HEIGHT
*/
EXPANDED_TO_FIT_TYPE : 'MAX_EXPANDED_ZOOM_LEVEL',
/**
* if true, the readout will be very small
*/
MINIMAL_READOUT : 'MINIMAL_READOUT',
TABLE_ROW : 'TABLE_ROW',
TABLE_ID : 'TABLE_ID',
UNIQUE_KEY : 'UNIQUE_KEY'
};
/**
* @global
* @public
* @typedef {Object} Dimension
*
* @prop {number} width
* @prop {number} height
*
*/
/**
* @global
* @public
* @typedef {Object} WebPlot
*
* @summary This class contains plot information.
* Publicly this class operations in many coordinate system.
* Some include a Image coordinate system, a world coordinate system, and a screen
* coordinate system.
*
* @prop {String} plotId - plot id, id of the plotView, immutable
* @prop {String} plotImageId, - plot image id, id of this WebPlot , immutable
* @prop {Object} serverImage, immutable
* @prop {String} title - the title
* @prop {{cubePlane,cubeHeaderAry}} cubeCtx
* @prop {PlotState} plotState - the plot state, immutable
* @prop {number} dataWidth - the width of the image data
* @prop {number} dataHeight - the height of the image data
* @prop {number} zoomFactor - the zoom factor
* @prop {string} title - title of the plot
* @prop {object} webFitsData - needs documentation
* @prop {ImageTileData} tileData - object contains the image tile information
* @prop {CoordinateSys} imageCoordSys - the image coordinate system
* @prop {Dimension} screenSize - width/height in screen pixels
* @prop {Projection} projection - projection routines for this projections
* @prop {Object} wlData - data object to wave length conversions, if defined then this conversion is available
* @prop {Object} affTrans - the affine transform
* @prop {{width:number, height:number}} viewDim size of viewable area (div size: offsetWidth & offsetHeight)
* @prop {Array.<Object>} directFileAccessDataAry - object of parameters to get flux from the FITS file
*
* @see PlotView
*/
/**
* @global
* @public
* @typedef {Object} RelatedData
* @summary overlay data that is associated with the image data
*
* @prop {string} relatedDataId - a globally unique id made from the plotId and the dataKey - this is added by the client and does
* not come from the server
* @prop {string} dataKey - should be a unique string key an array of plot of RelatedData, that is all
* RelatedData array entries for a plot should have a unqiue dataKey
* @prop {string} dataType - one of 'IMAGE_OVERLAY', 'IMAGE_MASK', 'TABLE'
* @prop {string} desc - user description of the data
* @prop {Object.<string, string>} searchParams - map of search parameters to get the related data
* @prop {Object.<string, string>} availableMask - only used for masks- key is the bit number, value is the description
*
*/
/**
* @global
* @public
* @typedef {Object} ThumbnailImage
* @summary the thumbnail information
*
* @prop {number} width - width of thumbnail
* @prop {number} height - height of thumbnail
* @prop {string} url - file key to use in the service to retrieve this tile
*
*/
/**
* @global
* @public
* @typedef {Object} ImageTile
* @summary a single image tile
*
* @prop {number} width - width of this tile
* @prop {number} height - height of this tile
* @prop {number} index - index of this tile
* @prop {string} url - file key to use in the service to retrieve this tile
* @prop {number} x - pixel offset of this tile
* @prop {number} y - pixel offset of this tile
*
*/
/**
* @global
* @public
* @typedef {Object} HiPSTile
* @summary a single hips image tile
* url computed by: NorderK/DirD/NpixN{.ext}
* where
* K= nOrder
* N= tileNumber
* D=(N/10000)*10000 (integer division)
*
* @prop {Array.<WorldPt>} corners (maybe) in worldPt
* @prop {Array.<devpt>} devPtCorners (maybe) in screenPt (keep here?)
* @prop {string} url - root url (maybe, don't know if necessary)
* @props {number} nOrder (K)
* @props {number} tileNumber (N)
*
*/
/**
* @global
* @public
* @typedef {Object} ImageTileData
* @summary The information about all the image tiles
*
* @prop {Array.<ImageTile>} images
* @prop {number} screenWidth - width of all the tiles
* @prop {number} screenHeight - height of all the tiles
* @prop {String} templateName - template name (not used)
* @prop {number} zfact - zoom factor
* @prop {ThumbnailImage} thumbnailImage - information about the thumbnail
*
*/
const relatedIdRoot= '-Related-';
export const isHiPS= (plot) => Boolean(plot && plot.plotType==='hips');
export const isImage= (plot) => Boolean(plot && plot.plotType==='image');
export const isKnownType= (plot) => Boolean(plot && (plot.plotType==='image' || plot.plotType==='hips'));
/**
*
* @param plotId
* @param plotType
* @param asOverlay
* @param imageCoordSys
* @return {WebPlot}
*/
function makePlotTemplate(plotId, plotType, asOverlay, imageCoordSys) {
return {
plotId,
plotType,
imageCoordSys,
asOverlay,
plotImageId : plotId+'---NEEDS___INIT',
tileData : undefined,
relatedData : undefined,
plotState : undefined,
projection: undefined,
dataWidth : undefined,
dataHeight : undefined,
title : '',
plotDesc : '',
dataDesc : '',
webFitsData : undefined,
//=== Mutable =====================
screenSize: {width:0, height:0},
zoomFactor: 1,
affTrans : undefined,
viewDim : undefined,
attributes: undefined,
// a note about conversionCache - the caches (using a map) calls to convert WorldPt to ImagePt
// have this here breaks the redux paradigm, however it still seems to be the best place. The cache
// is completely transient. If we start serializing the store there should not be much of an issue.
conversionCache: new Map(),
//=== End Mutable =====================
};
}
function processAllSpacialAltWcs(header) {
const availableAry= geAtlProjectionIDs(header);
if (isEmpty(availableAry)) return {};
return availableAry.reduce( (obj, altChar) => {
const processHeader= parseSpacialHeaderInfo(header, altChar);
const {maptype}= processHeader;
if (!maptype || maptype===UNSPECIFIED || maptype===UNRECOGNIZED) {
//todo did not find a spacial, do some other type of wcs computation
}
if (processHeader.headerType==='spacial') {
obj[altChar]= makeProjectionNew(processHeader, processHeader.imageCoordSys);
}
else {
obj[altChar]= undefined;
}
return obj;
}, {});
}
function processAllWavelengthAltWcs(header,wlTable) {
const availableAry= geAtlProjectionIDs(header);
if (isEmpty(availableAry)) return {};
return availableAry.reduce( (obj, altChar) => {
const wlData= parseWavelengthHeaderInfo(header, altChar, undefined, wlTable);
if (wlData) obj[altChar]= wlData;
return obj;
}, {});
}
/**
*
*/
export const WebPlot= {
/**
*
* @param {string} plotId
* @param wpInit init data returned from server
* @param {object} attributes any attributes to initialize
* @param {boolean} asOverlay
* @param {{cubePlane,cubeHeaderAry,relatedData,dataWidth,dataHeight,imageCoordSys}} cubeCtx
* @return {WebPlot} the plot
*/
makeWebPlotData(plotId, wpInit, attributes= {}, asOverlay= false, cubeCtx) {
const relatedData = cubeCtx ? cubeCtx.relatedData : wpInit.relatedData;
const plotState= PlotState.makePlotStateWithJson(wpInit.plotState);
const headerAry= !cubeCtx ? wpInit.headerAry : [cubeCtx.cubeHeaderAry[0]];
const header= headerAry[plotState.firstBand().value];
const zeroHeader= wpInit.zeroHeaderAry[0];
const processHeader= parseSpacialHeaderInfo(header,'',zeroHeader);
const projection= makeProjectionNew(processHeader, processHeader.imageCoordSys);
const processHeaderAry= !plotState.isThreeColor() ?
[processHeader] :
headerAry.map( (h,idx) => parseSpacialHeaderInfo(h,'',wpInit.zeroHeaderAry[idx]));
const fluxUnitAry= processHeaderAry.map( (p) => p.fluxUnits);
const wlRelated= relatedData && relatedData.find( (r) => r.dataType==='WAVELENGTH_TABLE_RESOLVED');
let wlData= parseWavelengthHeaderInfo(header, '', zeroHeader, get(wlRelated,'table'));
const allWCSMap= processAllSpacialAltWcs(header);
const allWlMap= processAllWavelengthAltWcs(header, get(wlRelated,'table'));
// if (!wlData && Object.values(allWlMap)>0) {
allWlMap['']= wlData;
allWCSMap['']= projection;
if (Object.values(allWlMap).length>0 && get(wlData, 'algorithm')!==TAB) {
wlData= Object.values(allWlMap)[0];
}
const zf= plotState.getZoomLevel();
for(let i= 0; (i<3); i++) {
if (headerAry[i]) plotState.get(i).directFileAccessData= makeDirectFileAccessData(headerAry[i], cubeCtx?cubeCtx.cubePlane:-1);
}
//original plot state come with header information for getting flux.
// this is only need for one call, so most time we strip it out.
// keeping clientFitsHeaderAry allows a way to put back the original
//todo: i think is could be cached on the server side so we don't need to be send it back and forth
const directFileAccessDataAry= plotState.getBands().map( (b) => plotState.getDirectFileAccessData(b));
const imageCoordSys= cubeCtx ? cubeCtx.imageCoordSys : wpInit.imageCoordSys;
let plot= makePlotTemplate(plotId,'image',asOverlay, CoordinateSys.parse(imageCoordSys));
const imagePlot= {
tileData : wpInit.initImages,
relatedData : null,
header,
headerAry,
zeroHeader,
fluxUnitAry,
cubeCtx,
plotState,
projection,
wlData,
allWCSMap,
allWlMap,
dataWidth : cubeCtx ? cubeCtx.dataWidth : wpInit.dataWidth,
dataHeight : cubeCtx ? cubeCtx.dataHeight : wpInit.dataHeight,
title : '',
plotDesc : wpInit.desc,
dataDesc : wpInit.dataDesc,
webFitsData : isArray(wpInit.fitsData) ? wpInit.fitsData : [wpInit.fitsData],
//=== Mutable =====================
screenSize: {width:wpInit.dataWidth*zf, height:wpInit.dataHeight*zf},
zoomFactor: zf,
attributes,
directFileAccessDataAry
//=== End Mutable =====================
};
plot= clone(plot, imagePlot);
plot.cubeIdx= getImageCubeIdx(plot);
if (relatedData) {
plot.relatedData= relatedData.map( (d) => clone(d,{relatedDataId: plotId+relatedIdRoot+d.dataKey}));
}
if ((!cubeCtx || cubeCtx.cubePlane===0) && wlData && wlData.failWarning) {
console.warn(`ImagePlot (${plotId}): Wavelength projection parse error: ${wlData.failWarning}`);
}
return plot;
},
/**
*
* @param plotId
* @param wpRequest
* @param hipsProperties
* @param desc
* @param zoomFactor
* @param attributes
* @param asOverlay
* @return {WebPlot} the new WebPlot object for HiPS
*/
makeWebPlotDataHIPS(plotId, wpRequest, hipsProperties, desc, zoomFactor=1, attributes= {}, asOverlay= false) {
const plotState= PlotState.makePlotState();
const bandState= BandState.makeBandState();
bandState.plotRequestTmp= wpRequest;
bandState.rangeValuesSerialize = null; // todo
bandState.rangeValues= null; //todo
plotState.bandStateAry= [bandState,null,null];
plotState.ctxStr=null;
plotState.zoomLevel= 1;
plotState.threeColor= false;
plotState.colorTableId= 0;
const hipsCoordSys= getHiPSCoordSysFromProperties(hipsProperties);
const lon= Number(hipsProperties.hips_initial_ra) || 0;
const lat= Number(hipsProperties.hips_initial_dec) || 0;
const projection= makeHiPSProjection(hipsCoordSys, lon,lat);
const plot= makePlotTemplate(plotId,'hips',asOverlay, hipsCoordSys);
const hipsPlot= {
//HiPS specific
nside: 3,
hipsUrlRoot: wpRequest.getHipsRootUrl(),
dataCoordSys : hipsCoordSys,
hipsProperties,
/// other
plotState,
projection,
allWCSMap: {'':projection},
dataWidth: HIPS_DATA_WIDTH,
dataHeight: HIPS_DATA_HEIGHT,
title : getHiPsTitleFromProperties(hipsProperties),
plotDesc : desc,
dataDesc : hipsProperties.label || 'HiPS',
//=== Mutable =====================
screenSize: {width:HIPS_DATA_WIDTH*zoomFactor, height:HIPS_DATA_HEIGHT*zoomFactor},
cubeDepth: Number(get(hipsProperties, 'hips_cube_depth')) || 1,
cubeIdx: Number(get(hipsProperties, 'hips_cube_firstframe')) || 0,
zoomFactor,
attributes,
//=== End Mutable =====================
};
return clone(plot, hipsPlot);
},
/**
*
* @param {WebPlot} plot
* @param {object} stateJson
* @param {ImageTileData} tileData
* @return {*}
*/
setPlotState(plot,stateJson,tileData) {
const plotState= PlotState.makePlotStateWithJson(stateJson);
const zf= plotState.getZoomLevel();
const screenSize= {width:plot.dataWidth*zf, height:plot.dataHeight*zf};
//keep the plotState populated with the fitsHeader information, this is only used with get flux calls
//todo: i think is could be cached on the server side so we don't need to be send it back and forth
const {bandStateAry}= plotState;
for(let i=0; (i<bandStateAry.length);i++) {
if (bandStateAry[i] && isEmpty(bandStateAry[i].directFileAccessData)) {
bandStateAry[i].directFileAccessData= plot.directFileAccessDataAry[i];
}
}
plot= {...plot,...{plotState, zoomFactor:zf,screenSize}};
if (tileData) plot.tileData= tileData;
return plot;
},
};
/**
*
* @param {CoordinateSys} coordinateSys
* @param lon
* @param lat
* @return {Projection}
*/
function makeHiPSProjection(coordinateSys, lon=0, lat=0) {
const header= {
cdelt1: 180/HIPS_DATA_WIDTH,
cdelt2: 180/HIPS_DATA_HEIGHT,
maptype: 5,
crpix1: HIPS_DATA_WIDTH*.5,
crpix2: HIPS_DATA_HEIGHT*.5,
crval1: lon,
crval2: lat
};
return makeProjection({header, coorindateSys:coordinateSys.toString()});
}
function getHiPSCoordSysFromProperties(hipsProperties) {
switch (hipsProperties.hips_frame) {
case 'equatorial' : return CoordinateSys.EQ_J2000;
case 'galactic' : return CoordinateSys.GALACTIC;
case 'ecliptic' : return CoordinateSys.ECL_B1950;
}
if (!hipsProperties.hips_frame) {
switch (hipsProperties.coordsys) { // fallback using old style
case 'C' : return CoordinateSys.EQ_J2000;
case 'G' : return CoordinateSys.GALACTIC;
case 'E' : return CoordinateSys.ECL_B1950;
}
}
return CoordinateSys.GALACTIC;
}
function makeHiPSProjectionUsingProperties(hipsProperties, lon=0, lat=0) {
return makeHiPSProjection(getHiPSCoordSysFromProperties(hipsProperties), lon,lat);
}
/**
* replace the hips projection if the coordinate system changes
* @param {WebPlot} plot
* @param hipsProperties
* @param {WorldPt} wp
*/
export function replaceHiPSProjectionUsingProperties(plot, hipsProperties, wp= makeWorldPt(0,0)) {
const projection= makeHiPSProjectionUsingProperties(hipsProperties, wp.x, wp.y);
const retPlot= clone(plot);
retPlot.imageCoordSys= projection.coordSys;
retPlot.dataCoordSys= projection.coordSys;
retPlot.projection= projection;
retPlot.allWCSMap= {'':projection};
return retPlot;
}
/**
* replace the hips projection if the coordinate system changes
* @param {WebPlot} plot
* @param coordinateSys
* @param {WorldPt} wp
*/
export function replaceHiPSProjection(plot, coordinateSys, wp= makeWorldPt(0,0)) {
const newWp= convert(wp, coordinateSys);
const projection= makeHiPSProjection(coordinateSys, newWp.x, newWp.y);
const retPlot= clone(plot);
retPlot.imageCoordSys= projection.coordSys;
//note- the dataCoordSys stays the same
retPlot.projection= projection;
retPlot.allWCSMap= {'':projection};
return retPlot;
}
/**
* replace the header in the transform of the plot object
* @param {WebPlot} plot
* @param {Object} header
* @return {WebPlot}
*/
export function replaceHeader(plot, header) {
const retPlot= clone(plot);
retPlot.conversionCache= new Map();
retPlot.projection= makeProjection({header:clone(header), coorindateSys:plot.projection.coordSys.toString()});
retPlot.allWCSMap= {'':retPlot.projection};
return retPlot;
}
/**
* Return true if this is a WebPlot obj
* @param obj
* @return boolean
*/
export function isPlot(obj) {
return Boolean(obj && obj.plotType && obj.plotId && obj.plotImageId && obj.conversionCache);
}
/**
* Check if the plot is is a blank image
* @param {WebPlot} plot - the plot
* @return {boolean}
*/
export function isBlankImage(plot) {
if (plot.plotState.isThreeColor()) return false;
const req= plot.plotState.getWebPlotRequest();
return (req && req.getRequestType()===RequestType.BLANK);
}
/**
*
* @param {WebPlot} plot
* @param {number} zoomFactor
* @return {WebPlot}
*/
export function clonePlotWithZoom(plot,zoomFactor) {
if (!plot) return null;
const screenSize= {width:plot.dataWidth*zoomFactor, height:plot.dataHeight*zoomFactor};
return Object.assign({},plot,{zoomFactor,screenSize});
}
/**
*
* @param {WebPlot|CysConverter} plot
* @return {number}
*/
export function getScreenPixScaleArcSec(plot) {
if (!plot || !plot.projection || !isKnownType(plot)) return 0;
if (isImage(plot)) {
return plot.projection.getPixelScaleArcSec() / plot.zoomFactor;
}
else if (isHiPS(plot)) {
const pt00= makeWorldPt(0,0, plot.imageCoordSys);
const tmpPlot= changeProjectionCenter(plot, pt00);
const cc= CysConverter.make(tmpPlot);
const scrP= cc.getScreenCoords( pt00);
const pt2= cc.getWorldCoords( makeScreenPt(scrP.x-1, scrP.y), plot.imageCoordSys);
return Math.abs(0-pt2.x)*3600; // note have to use angular distance formula here, because of the location of the point
}
return 0;
}
export function getFluxUnits(plot,band) {
if (!plot || !band || !isImage(plot)) return '';
return plot.fluxUnitAry[band.value];
}
/**
*
* @param {WebPlot|CysConverter} plot
* @return {number}
*/
export function getPixScaleArcSec(plot) {
return getPixScaleDeg(plot)*3600;
}
/**
*
* @param {WebPlot|CysConverter} plot
* @return {number}
*/
export function getPixScaleDeg(plot) {
if (!plot || !plot.projection || !isKnownType(plot) ) return 0;
if (!plot || !plot.projection) return 0;
if (isImage(plot)) {
return plot.projection.getPixelScaleDegree();
}
else if (isHiPS(plot)) {
const pt00= makeWorldPt(0,0, plot.imageCoordSys);
const tmpPlot= changeProjectionCenter(plot, pt00);
const cc= CysConverter.make(tmpPlot);
const imP= cc.getImageCoords( pt00);
const pt2= cc.getWorldCoords( makeImagePt(imP.x-1, imP.y), plot.imageCoordSys);
return Math.abs(0-pt2.x);
}
return 0;
}