/*
* License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
*/
import CoordinateSys from './CoordSys.js';
import {get} from 'lodash';
import VisUtil from './VisUtil.js';
import {makeRoughGuesser} from './ImageBoundsData.js';
import Point, {makeImageWorkSpacePt, makeImagePt,
makeScreenPt, makeWorldPt, makeDevicePt, isValidPoint} from './Point.js';
import {Matrix} from 'transformation-matrix-js';
import {getPixScaleDeg, isHiPS} from './WebPlot.js';
import {makeFitsImagePt, makeZeroBasedImagePt} from './Point';
function convertToCorrect(wp) {
if (!wp) return null;
const csys= wp.getCoordSys();
if (csys===CoordinateSys.SCREEN_PIXEL) {
return makeScreenPt(wp.x, wp.y);
}
else if (csys===CoordinateSys.PIXEL) {
return makeImagePt(wp.x, wp.y);
}
return wp;
}
const MAX_CACHE_ENTRIES = 38000; // set to never allows the cache array over 48000 with a 80% load factor
/**
* This class is for conversion
* Some include a Image coordinate system, a world coordinate system, and a screen
* coordinate system.
* <ul>
* <li>The image coordinate system is the coordinate system of the data.
* <li>The world coordinate system is the system that the data represents
* (i.e. the coordinate system of the sky)
* <li>Screen coordinates are the pixel values of the screen.
* </ul>
* @public
*/
export class CysConverter {
/**
* @param {object} plot
* @param {object} [altAffTrans] an alternate transform to use
* @param {String} [whichWCS] choose the wcs (must be defined) the default WCS is the empty string
* @public
*/
constructor(plot, altAffTrans, whichWCS='') {
this.plotImageId= plot.plotImageId;
this.plotId = plot.plotId;
this.plotState= plot.plotState;
this.dataWidth= plot.dataWidth;
this.dataHeight= plot.dataHeight;
this.projection= plot.allWCSMap[whichWCS];
this.zoomFactor= plot.zoomFactor;
this.imageCoordSys= plot.imageCoordSys;
this.inPlotRoughGuess= null;
this.conversionCache= plot.conversionCache;
this.screenSize= plot.screenSize;
this.affTrans= altAffTrans || plot.affTrans;
this.viewDim= plot.viewDim;
this.plotType= plot.plotType;
this.header= plot.header;
}
// isRotated() { return !Matrix.from(this.affTrans).isIdentity(); }
/**
*
* @param {WorldPt} wp world point
* @param {ImagePt} imp Image Point
*/
putInConversionCache(wp, imp) {
if (this.conversionCache.size<MAX_CACHE_ENTRIES) {
this.conversionCache.set(wp.toString(), imp);
}
}
//========================================================================================
//----------------------------- pointIn Methods -----------------------------------------
//========================================================================================
/**
* Determine if a world point is in data Area of the plot and is not null
* @param {WorldPt} iwPt the point to test.
* @returns {boolean} true if it is in the data boundaries, false if not.
*/
imagePointInData(iwPt) {
let retval= false;
if (iwPt && this.pointInPlot(iwPt)) {
const ipt= this.getImageCoords(iwPt);
const x= ipt.x;
const y= ipt.y;
retval= (x >= 0 && x <= this.dataWidth && y >= 0 && y <= this.dataHeight );
}
return retval;
}
/**
* Determine if a image point is in the plot boundaries and is not null.
* @param {Point} pt the point to test.
* @returns {boolean} true if it is in the boundaries, false if not.
*/
pointInData(pt) {
if (!isValidPoint(pt)) return false;
return this.imagePointInData(this.getImageWorkSpaceCoords(pt));
}
/**
* This method returns false it the point is definitely not in plot. It returns true if the point might be in the plot.
* Used for tossing out points that we know that are not in plot without having to do all the math. It is much faster.
* @param {WorldPt} wp
* @return {boolean} true in we guess it might be in the bounds, false if we know that it is not in the bounds
*/
pointInPlotRoughGuess(wp) {
if (!this.projection.isWrappingProjection()) {
if (!this.inPlotRoughGuess) this.inPlotRoughGuess= makeRoughGuesser( this);
return this.inPlotRoughGuess(wp);
}
else {
return true;
}
}
/**
* Determine if a image point is in the plot boundaries and is not null.
* @param {ImagePt} ipt the point to test.
* @returns {boolean} true if it is in the boundaries, false if not.
*/
imageWorkSpacePtInPlot(ipt) {
if (!ipt) return false;
const {x,y}= ipt;
return (x >= 0 && x <= this.dataWidth && y >= 0 && y <= this.dataHeight );
}
/**
* Determine if a image point is in the plot boundaries and is not null
* @param {Point} pt the point to test.
* @return {boolean} true if it is in the boundaries, false if not.
*/
pointInPlot(pt) {
let retval= false;
if (!isValidPoint(pt)) {
return false;
}
else if (pt.type===Point.W_PT) {
retval= this.pointInPlotRoughGuess(pt);
if (retval) {
retval= this.imageWorkSpacePtInPlot(this.getImageWorkSpaceCoords(pt));
}
}
else {
retval= this.imageWorkSpacePtInPlot(this.getImageWorkSpaceCoords(pt));
}
return retval;
}
pointOnDisplay(pt) {
if (!isValidPoint(pt)) return false;
const devPt= this.getDeviceCoords(pt);
if (!devPt) return false;
const {x,y}= devPt;
const {width,height}= this.viewDim;
let retval= (x>=0 && y>=0 && x<=width && y<=height);
if (retval) {
const minSize= Math.min(width,height);
const {width:sWidth,height:sHeight}= this.screenSize;
if (sWidth< minSize || sHeight< minSize) {
retval= this.pointInPlot(pt);
}
}
return retval;
}
/* check if the point within display range for either regular image or hips */
pointInView(pt) {
if (!pt) return false;
if (isHiPS(this)) {
const inViewDim = (pt) => {
const devPt = this.getDeviceCoords(pt);
return (devPt) && (devPt.x >= 0 && devPt.x < this.viewDim.width) &&
(devPt.y >= 0 && devPt.y < this.viewDim.height);
};
return inViewDim(pt);
} else {
return this.pointInData(pt);
}
};
/* check it the point is viewable by checking if the point is within range of viewDim for
hips image or if the point is within plot range for fits image
*/
isPointViewable(pt) {
return isHiPS(this) ? this.pointInView(pt) : this.pointOnDisplay(pt);
};
//========================================================================================
//----------------------------- End pointIn Methods -------------------------------------
//========================================================================================
//========================================================================================
//========================================================================================
//========================================================================================
//----------------------------- Conversion Methods Begin --------------------------------
//========================================================================================
//========================================================================================
//========================================================================================
//----------------------------- Conversion to ImageWorkSpacePt Methods ------------------
//========================================================================================
/**
* Return the ImageWorkSpacePt coordinates for a given Pt
* @param {object} pt the point to translate
* @param {number} [altZoomLevel] only use this parameter it you want to compute the point for a zoom level that
* if different than what the plotted zoom level
* @return ImageWorkSpacePt the image workspace coordinates
*/
getImageWorkSpaceCoords(pt, altZoomLevel) {
if (!isValidPoint(pt)) return null;
let retval= null;
switch (pt.type) {
case Point.IM_WS_PT:
retval= pt;
break;
case Point.FITS_IM_PT:
case Point.ZERO_BASED_IM_PT:
const imPt= this.getImageCoords(pt);
retval= makeImageWorkSpacePt(imPt.x, imPt.y);
break;
case Point.IM_PT:
retval= makeImageWorkSpacePt(pt.x, pt.y);
break;
case Point.SPT:
retval= this.makeIWPtFromSPt(pt,altZoomLevel);
break;
case Point.DEV_PT:
retval= this.makeIWPtFromSPt(this.getScreenCoords(pt),altZoomLevel);
break;
case Point.W_PT:
const checkedPt= convertToCorrect(pt);
if (checkedPt.type===Point.W_PT) {
retval= this.getImageWorkSpaceCoords(this.getImageCoords(checkedPt));
}
else {
retval= this.getImageWorkSpaceCoords(checkedPt, altZoomLevel);
}
break;
}
return retval;
}
/**
* @description Return a ImageWorkspacePt from the screen point.
* @param {ScreenPt} screenPt
* @param {number} [altZoomLevel]
*/
makeIWPtFromSPt(screenPt, altZoomLevel) {
if (!screenPt) return null;
const zoom= altZoomLevel || this.zoomFactor;
return makeImageWorkSpacePt(screenPt.x / zoom, this.dataHeight-screenPt.y/zoom);
}
//========================================================================================
//----------------------------- Conversion to ImageSpacePt Methods ----------------------
//========================================================================================
getFitsStandardImagePtFromInternal(pt) {
const imPt= this.getImageCoords(pt);
return makeFitsImagePt(imPt.x+.5, imPt.y+.5);
}
getZeroBasedImagePtFromInternal(pt) {
const {ltv1,ltv2}= CysConverter.getLtv(this.header);
const imPt= this.getImageCoords(pt);
return makeZeroBasedImagePt(imPt.x-.5-ltv1, imPt.y-.5-ltv2);
}
static convertFitsStandardImagePtToInternalImage(pt) {
return makeImagePt(pt.x-.5, pt.y-.5);
}
static getLtv(header) {
const {LTV1,LTV2, CRVAL1A,CRVAL2A}= header;
let ltv1, ltv2;
if (!isNaN(Number(get(LTV1,'value'))) && !isNaN(Number(get(LTV2,'value')))) {
ltv1= Number(get(LTV1,'value'));
ltv2= Number(get(LTV2,'value'));
}
else if (!isNaN(Number(get(CRVAL1A,'value'))) && !isNaN(Number(get(CRVAL2A,'value')))) {
ltv1= -Number(get(CRVAL1A,'value'));
ltv2= -Number(get(CRVAL2A,'value'));
}
else {
ltv1= 0;
ltv2= 0;
}
return {ltv1,ltv2};
}
/**
* Return the ImagePt coordinates given Pt
* @param {object} pt the point to translate
* @return {ImagePt} the image coordinates
*/
getImageCoords(pt) {
if (!isValidPoint(pt)) return null;
let retval = null;
switch (pt.type) {
case Point.IM_WS_PT:
retval= CysConverter.makeIPtFromIWPt(pt);
break;
case Point.SPT:
case Point.DEV_PT:
retval= CysConverter.makeIPtFromIWPt(this.getImageWorkSpaceCoords(pt));
break;
case Point.IM_PT:
retval = pt;
break;
case Point.FITS_IM_PT:
retval= CysConverter.makeIPtFromFitsImPt(pt);
break;
case Point.ZERO_BASED_IM_PT:
retval= this.makeIPtFromxZeroImPt(pt);
break;
case Point.W_PT:
retval = this.getImageCoordsFromWorldPt(pt);
break;
}
return retval;
}
/**
* return a ImagePoint from a ImageWorkspace point
* @param {ImageWpt} iwPt
* returns {ImagePt}
*/
static makeIPtFromIWPt(iwPt) {
return iwPt ? makeImagePt(iwPt.x, iwPt.y) : null;
}
static makeIPtFromFitsImPt(pt) {
return pt ? makeImagePt(pt.x-.5, pt.y-.5) : null;
}
makeIPtFromxZeroImPt(pt) {
if (!pt) return null;
const {ltv1,ltv2}= CysConverter.getLtv(this.header);
return makeImagePt(pt.x+.5+ltv1, pt.y+.5+ltv2);
}
/**
* @desc Return the image coordinates given a WorldPt class.
* @param {WorldPt} wpt the class containing the point in sky coordinates
* @returns {ImagePt} the translated coordinates
*/
getImageCoordsFromWorldPt(wpt) {
if (!wpt) return null;
let retval;
const checkedPt= convertToCorrect(wpt);
if (checkedPt.type===Point.W_PT) {
const originalWp= wpt;
retval= this.conversionCache.get(checkedPt.toString() );
if (!retval) {
if (this.imageCoordSys!==wpt.getCoordSys()) {
wpt= VisUtil.convert(wpt,this.imageCoordSys);
}
const projPt= this.projection.getImageCoords(wpt.getLon(),wpt.getLat());
retval= projPt ? makeImagePt( projPt.x+ 0.5 , projPt.y+ 0.5) : null;
this.putInConversionCache(originalWp,retval);
}
}
else {
retval= this.getImageCoords(checkedPt);
}
return retval;
}
//========================================================================================
//----------------------------- Conversion to DevicePt Methods --------------------------
//========================================================================================
/**
* Return the device coordinates given a Pt
* @param pt the point to translate
* @param {number} [altZoomLevel] only use this parameter it you want to compute the point for a zoom level that
* if different than what the plotted zoom level
* @param {object} [altTransform] an alternate affine transform to use
* @return {DevicePt} the device coordinates
*/
getDeviceCoords(pt, altZoomLevel, altTransform) {
if (!isValidPoint(pt)) return null;
let retval = null;
switch (pt.type) {
case Point.DEV_PT:
retval= pt;
break;
case Point.FITS_IM_PT:
case Point.ZERO_BASED_IM_PT:
case Point.IM_WS_PT:
case Point.IM_PT:
case Point.W_PT:
if (!altZoomLevel && !altTransform) {
retval= this.makeDevicePtFromSp(this.getScreenCoords(pt)); // normal case
}
else {
// special case, used with thumbnail: ignore Viewport
retval= this.makeDevicePtFromSp(this.getScreenCoords(pt, altZoomLevel), altTransform);
}
break;
case Point.SPT:
retval= this.makeDevicePtFromSp(pt, altTransform);
break;
break;
}
return retval;
}
getDevicePtCoordsOptimize(wpt, retPt) {
const success= this.getScreenCoordsOptimize(wpt,retPt);
if (!success || !this.affTrans) return false;
const {x,y}= Matrix.from(this.affTrans).applyToPoint(retPt.x,retPt.y);
retPt.x= x;
retPt.y= y;
retPt.type= Point.DEV_PT;
return true;
}
/**
*
* @param {Object} sp ScreenPt
* @param {Object} altTransform
* @return {DevicePt}
*/
makeDevicePtFromSp(sp, altTransform) {
if (!sp || !this.affTrans) return null;
const {x,y}= Matrix.from(altTransform || this.affTrans).applyToPoint(sp.x,sp.y);
return makeDevicePt(x,y);
}
//========================================================================================
//----------------------------- Conversion to ScreenPt Methods --------------------------
//========================================================================================
/**
* Return the screen coordinates given Pt
* @param pt the point to translate
* @param {number} [altZoomLevel] only use this parameter it you want to compute the point for a zoom level that
* if different than what the plotted zoom level
* @return ScreenPt the screen coordinates
*/
getScreenCoords(pt, altZoomLevel) {
if (!isValidPoint(pt)) return null;
let retval = null;
switch (pt.type) {
case Point.IM_WS_PT:
retval= this.makeSPtFromIWPt(pt, altZoomLevel);
break;
case Point.SPT:
retval= pt;
break;
case Point.FITS_IM_PT:
case Point.ZERO_BASED_IM_PT:
const imPt= this.getImageCoords(pt);
retval= this.makeSPtFromIWPt(this.getImageWorkSpaceCoords(imPt), altZoomLevel);
break;
case Point.IM_PT:
retval= this.makeSPtFromIWPt(this.getImageWorkSpaceCoords(pt), altZoomLevel);
break;
case Point.DEV_PT:
retval = this.makeSpFromDevPt(pt);
break;
case Point.W_PT:
const checkedPt= convertToCorrect(pt);
if (checkedPt) {
if (checkedPt.type===Point.W_PT) {
retval= this.makeSPtFromIWPt(this.getImageWorkSpaceCoords(checkedPt),altZoomLevel);
}
else {
retval= this.getScreenCoords(checkedPt, altZoomLevel);
}
}
break;
}
return retval;
}
makeSpFromDevPt(devPt) {
if (!devPt || !this.affTrans) return null;
const {x,y}= Matrix.from(this.affTrans).inverse().applyToPoint(devPt.x,devPt.y);
return makeScreenPt(x,y);
}
/**
* @desc An optimized conversion of WorldPt to Screen point.
* @param {WorldPt} wpt a world pt
* @param {ScreenPt} retPt mutable returned Screen Point, this object will be written to
* @return {boolean} success or failure
*/
getScreenCoordsOptimize(wpt, retPt) {
let success= false;
let imagePt= this.conversionCache.get(wpt.toString());
if (!imagePt) {
const csys= wpt.getCoordSys();
if (csys===CoordinateSys.SCREEN_PIXEL) {
retPt.x= wpt.x;
retPt.y= wpt.y;
success= true;
}
else if (csys===CoordinateSys.PIXEL) {
this.makeScreenPtFromImPtOptimized(wpt,retPt);
}
else {
const originalWp= wpt;
if (this.imageCoordSys!==wpt.getCoordSys()) {
wpt= VisUtil.convert(wpt,this.imageCoordSys);
}
const proj_pt= this.projection.getImageCoords(wpt.getLon(),wpt.getLat());
if (proj_pt) {
const imageX= proj_pt.x + 0.5;
const imageY= proj_pt.y + 0.5;
imagePt= makeImagePt(imageX,imageY);
this.putInConversionCache(originalWp, imagePt);
this.makeScreenPtFromImPtOptimized(imagePt,retPt);
success= true;
}
}
}
else {
this.makeScreenPtFromImPtOptimized(imagePt,retPt);
success= true;
}
return success;
}
makeScreenPtFromImPtOptimized(imagePt,retPt) {
if (!imagePt) return null;
// convert image to image workspace
const imageWorkspaceX= imagePt.x;
const imageWorkspaceY= imagePt.y;
// convert image workspace to screen
const zFact= this.zoomFactor;
retPt.x= imageWorkspaceX*zFact;
retPt.y= (this.dataHeight - imageWorkspaceY) *zFact;
}
/**
*
* @param {ImageWorkspacePt} iwpt ImageWorkspacePt
* @param {number} [altZoomLevel]
*/
makeSPtFromIWPt(iwpt, altZoomLevel) {
if (!iwpt) return null;
const zoom= altZoomLevel || this.zoomFactor;
return makeScreenPt(iwpt.x*zoom,
(this.dataHeight - iwpt.y) *zoom );
}
//========================================================================================
//----------------------------- Conversion to WorldPt Methods ---------------------------
//========================================================================================
/**
* @desc Return the sky coordinates given a image x (fsamp) and y (fline)
* @param {PointPt} pt the point to convert
* @param {CoordinateSys} [outputCoordSys] (optional) The coordinate system to return, default to coordinate system of image
* @returns {WorldPt} the translated coordinates
*/
getWorldCoords(pt, outputCoordSys= undefined) {
if (!isValidPoint(pt)) return null;
let retval = null;
switch (pt.type) {
case Point.IM_PT:
retval= this.makeWorldPtFromIPt(pt,outputCoordSys);
break;
case Point.FITS_IM_PT:
case Point.ZERO_BASED_IM_PT:
case Point.IM_WS_PT:
case Point.SPT:
case Point.DEV_PT:
retval= this.makeWorldPtFromIPt(this.getImageCoords(pt),outputCoordSys);
break;
case Point.W_PT:
retval= (outputCoordSys===pt.getCoordSys()) ? pt : VisUtil.convert(pt, outputCoordSys);
break;
}
return retval;
}
makeWorldPtFromIPt( ipt, outputCoordSys) {
if (!ipt) return null;
let wpt = this.projection.getWorldCoords(ipt.x - .5 ,ipt.y - .5);
if (wpt && outputCoordSys!==wpt.getCoordSys()) {
wpt= VisUtil.convert(wpt, outputCoordSys);
}
return wpt;
}
//========================================================================================
//========================================================================================
//----------------------------- Conversion Methods End -----------------------------------
//========================================================================================
//========================================================================================
//========================================================================================
//========================================================================================
//========================================================================================
//========================================================================================
coordsWrap(wp1, wp2) {
if (!wp1 || !wp2) return false;
let retval= false;
if (this.projection.isWrappingProjection()) {
const worldDist= VisUtil.computeDistance(wp1, wp2);
const pix= getPixScaleDeg(this);
const value1= worldDist/pix;
const ip1= this.getImageWorkSpaceCoords(wp1);
const ip2= this.getImageWorkSpaceCoords(wp2);
if (ip1 && ip2) {
const xDiff= ip1.x-ip2.x;
const yDiff= ip1.y-ip2.y;
const imageDist= Math.sqrt(xDiff*xDiff + yDiff*yDiff);
retval= ((imageDist / value1) > 3);
}
else {
retval= false;
}
}
return retval;
}
/**
*
* @param {Object} plot - the image
* @param {object} [altAffTrans] an alternate transform to use
* @returns {CysConverter}
*/
static make(plot, altAffTrans) {
return plot ? new CysConverter(plot, altAffTrans) : null;
}
} //end of class definition
/**
*
* @param pt
* @return {*}
* @public
* @memberof firefly.util.image.CCUtil
*
*/
function getWorldPtRepresentation(pt) {
if (!isValidPoint(pt)) return null;
if (pt.type===Point.W_PT) return pt;
else if (pt.type=== Point.IM_WS_PT) return makeWorldPt(pt.x,pt.y, CoordinateSys.PIXEL);
else if (pt.type=== Point.IM_PT) return makeWorldPt(pt.x,pt.y, CoordinateSys.PIXEL);
else if (pt.type=== Point.SPT) return makeWorldPt(pt.x,pt.y, CoordinateSys.SCREEN_PIXEL);
}
/** part of lowLevelApi
* @namespace firefly.util.image.CCUtil
* @public
*/
export const CCUtil = {
/**
* Convert to ImageWorkSpace Point
* @param {WebPlot} plot - the image
* @param pt
* @func getImageWorkSpaceCoords
* @memberof firefly.util.image.CCUtil
* @public
*/
getImageWorkSpaceCoords : (plot,pt) => CysConverter.make(plot).getImageWorkSpaceCoords(pt),
/**
*
* Convert to Image Point
* @param {WebPlot} plot - the image
* @param {object} pt - the point to convert
* @return {ImagePt}
* @function getImageCoords
* @memberof firefly.util.image.CCUtil
* @public
*/
getImageCoords: (plot,pt) => CysConverter.make(plot).getImageCoords(pt),
/**
*
* Convert to Device point
*/
/**
* @param {WebPlot} plot - the image
* @param {object} pt - the point to convert
* @public
* @memberof firefly.util.image.CCUtil
*/
getDeviceCoords: (plot,pt) => CysConverter.make(plot).getDeviceCoords(pt),
/*
*
* Convert to Screen Point
* */
/**
* @param {WebPlot} plot - the image
* @param {object} pt - the point to convert
* @function getScreenCoords
* @memberof firefly.util.image.CCUtil
*/
getScreenCoords: (plot,pt) => CysConverter.make(plot).getScreenCoords(pt),
/**
*
* Convert to World Point
* @param {WebPlot} plot - the image
* @param pt - the point to convert
* @return {WorldPt}
* @function getWorldCoords
* @memberof firefly.util.image.CCUtil
* @public
*/
getWorldCoords: (plot,pt) => CysConverter.make(plot).getWorldCoords(pt),
/**
*
* @ignore
*
*/
getWorldPtRepresentation
};
export default CysConverter;