/*
* License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
*/
/**
*
* @author Trey, Booth and many more
*/
import {isArray, isEmpty, flattenDeep, isUndefined} from 'lodash';
import pointInPolygon from 'point-in-polygon';
import Enum from 'enum';
import CoordinateSys from './CoordSys.js';
import {CCUtil, CysConverter} from './CsysConverter.js';
import DrawOp from './draw/DrawOp.js';
import {primePlot} from './PlotViewUtil.js';
import {doConv} from '../astro/conv/CoordConv.js';
import Point, {makeImageWorkSpacePt, makeImagePt, makeScreenPt,
makeWorldPt, makeDevicePt, isValidPoint, pointEquals} from './Point.js';
import {Matrix} from 'transformation-matrix-js';
import {getPixScaleDeg, getFluxUnits} from './WebPlot.js';
import {SelectedShape} from '../drawingLayers/SelectArea.js';
/** Constant for conversion Degrees => Radians */
export const DtoR = Math.PI / 180.0;
/** Constant for conversion Radians => Degrees */
export const RtoD = 180.0 / Math.PI;
export const toDegrees = (angle) => angle * (180 / Math.PI);
export const toRadians = (angle) => (angle * Math.PI) / 180;
export const FullType= new Enum(['ONLY_WIDTH', 'WIDTH_HEIGHT', 'ONLY_HEIGHT', 'SMART']);
//======================================================================
//----------------------- Public Methods -------------------------------
//======================================================================
/**
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @return {number}
*/
export const computeScreenDistance= function (x1, y1, x2, y2) {
const deltaXSq = (x1 - x2) * (x1 - x2);
const deltaYSq = (y1 - y2) * (y1 - y2);
return Math.sqrt(deltaXSq + deltaYSq);
};
/**
* compute the distance on the sky between two world points
* @param p1 WorldPt
* @param p2 WorldPt
* @return {number}
*/
export function computeDistance(p1, p2) {
const lon1Radius = p1.getLon() * DtoR;
const lon2Radius = p2.getLon() * DtoR;
const lat1Radius = p1.getLat() * DtoR;
const lat2Radius = p2.getLat() * DtoR;
let cosine = Math.cos(lat1Radius) * Math.cos(lat2Radius) *
Math.cos(lon1Radius - lon2Radius) +
Math.sin(lat1Radius) * Math.sin(lat2Radius);
if (Math.abs(cosine) > 1.0) cosine = cosine / Math.abs(cosine);
return RtoD * Math.acos(cosine);
}
/**
*
* @param p1 {Pt}
* @param p2 {Pt}
* @return {number}
*/
const computeSimpleDistance= function(p1, p2) {
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
return Math.sqrt(dx * dx + dy * dy);
};
/**
* @summary Return a Imge point the represents the passed Image point with a distance in
* World coordinates added to it.
* @param {WebPlot} plot the plot
* @param {ImagePt} pt the x and y coordinate in image coordinates
* @param x the x distance away from the point in world coordinates
* @param y the y distance away from the point in world coordinates
* @return {ImagePt} the new point
* @public
*/
export function getDistanceCoords(plot, pt, x, y) {
if (!plot || !plot.projection) return undefined;
const scale= 1/getPixScaleDeg(plot);
return makeImagePt ( pt.x+(x * scale), pt.y+(y * scale) );
}
/**
* Convert from one coordinate system to another.
*
* @param {WorldPt} wpt the world point to convert
* @param {CoordinateSys} to CoordSys, the coordinate system to convert to
* @return WorldPt the world point in the new coordinate system
*/
export function convert(wpt, to= CoordinateSys.EQ_J2000) {
const from = wpt.getCoordSys();
if (!to || from===to) return wpt;
const tobs= (from===CoordinateSys.EQ_B1950) ? 1983.5 : 0;
const ll = doConv(
from.getJsys(), from.getEquinox(),
wpt.getLon(), wpt.getLat(),
to.getJsys(), to.getEquinox(), tobs);
return makeWorldPt(ll.lon, ll.lat, to);
}
function convertToJ2000(wpt) { return convert(wpt); }
/**
* Find an approximate central point and search radius for a group of positions
*
* @param inPoints array of points for which the central point is desired
* @return {{centralPoint:WorldPt, maxRadius: Number}}
*/
export function computeCentralPointAndRadius(inPoints) {
let lon;
let radius;
let maxRadius = Number.NEGATIVE_INFINITY;
const points= inPoints.map((wp) => convertToJ2000(wp));
/* get max,min of lon and lat */
let maxLon = Number.NEGATIVE_INFINITY;
let minLon = Number.POSITIVE_INFINITY;
let maxLat = Number.NEGATIVE_INFINITY;
let minLat = Number.POSITIVE_INFINITY;
points.forEach((pt) => {
if (pt.x > maxLon) {
maxLon = pt.x;
}
if (pt.x < minLon) {
minLon = pt.x;
}
if (pt.y > maxLat) {
maxLat = pt.y;
}
if (pt.y < minLat) {
minLat = pt.y;
}
});
if (maxLon - minLon > 180) {
minLon = 360 + minLon;
}
lon = (maxLon + minLon) / 2;
if (lon > 360) lon -= 360;
const lat = (maxLat + minLat) / 2;
const centralPoint = makeWorldPt(lon, lat);
points.forEach((pt) => {
radius = computeDistance(centralPoint,
makeWorldPt(pt.x, pt.y));
if (maxRadius < radius) {
maxRadius = radius;
}
});
return {centralPoint, maxRadius};
}
/**
* call computeCentralPointAndRadius in 2 ways first with a flatten version of the 2d array
* then again with group of points. This allows us not to overweight a larger group when computing the center.
*
* @param {Array.<Array.<WorldPt>>} inPoints2dAry a 2d array of world points. Each array represents a group of points
* @return {{centralPoint:WorldPt, maxRadius:number, avgOfCenters:number}}
*/
export function computeCentralPtRadiusAverage(inPoints2dAry) {
const testAry= flattenDeep(inPoints2dAry);
if (isEmpty(testAry)) return {centralPoint:undefined, maxRadius: 0, avgOfCenters:undefined};
if (isOnePoint(testAry)) return {centralPoint:testAry[0], maxRadius: .05, avgOfCenters:testAry[0]};
const {centralPoint, maxRadius}= computeCentralPointAndRadius(testAry);
if (inPoints2dAry.length===1) return {centralPoint, maxRadius, avgOfCenters:centralPoint};
const centers= inPoints2dAry.map( (ptAry) =>
isOnePoint(ptAry) ? ptAry[0] : computeCentralPointAndRadius(ptAry).centralPoint);
const {centralPoint:avgOfCenters}= computeCentralPointAndRadius(centers);
return {centralPoint, maxRadius, avgOfCenters};
}
function isOnePoint(wpList) {
return !wpList.some( (wp) => !pointEquals(wp,wpList[0]));
}
/**
* Compute position angle
*
* @param {number} ra0 the equatorial RA in degrees of the first object
* @param {number} dec0 the equatorial DEC in degrees of the first object
* @param {number} ra the equatorial RA in degrees of the second object
* @param {number} dec the equatorial DEC in degrees of the second object
* @return {number} position angle in degrees between the two objects
*/
export function getPositionAngle(ra0, dec0, ra, dec) {
let sind, sinpa, cospa;
const alf = ra * DtoR;
const alf0 = ra0 * DtoR;
const del = dec * DtoR;
const del0 = dec0 * DtoR;
const sd0 = Math.sin(del0);
const sd = Math.sin(del);
const cd0 = Math.cos(del0);
const cd = Math.cos(del);
const cosda = Math.cos(alf - alf0);
const cosd = sd0 * sd + cd0 * cd * cosda;
let dist = Math.acos(cosd);
let pa = 0.0;
if (dist > 0.0000004) {
sind = Math.sin(dist);
cospa = (sd * cd0 - cd * sd0 * cosda) / sind;
if (cospa > 1.0) cospa = 1.0;
if (cospa < -1.0) cospa = -1.0;
sinpa = cd * Math.sin(alf - alf0) / sind;
pa = Math.acos(cospa) * RtoD;
if (sinpa < 0.0) pa = 360.0 - (pa);
}
dist *= RtoD;
if (dec0===90) pa = 180.0;
if (dec0===-90) pa = 0.0;
return pa;
}
/**
* Rotates the given input position and returns the result. The rotation
* applied to positionToRotate is the one which maps referencePosition to
* rotatedReferencePosition.
* @author Serge Monkewitz
* @param {WorldPt} referencePosition the reference position to start
* @param {WorldPt} rotatedReferencePosition the rotated reference position
* @param {WorldPt} positionToRotate the position to be moved and rotated
* @return {WorldPt} the result new world position by applying the same displacement as the one from
* referencePosition to rotatedReferencePosition
*/
export function getTranslateAndRotatePosition(referencePosition, rotatedReferencePosition, positionToRotate) {
// Extract coordinates and transform to radians
const ra1 = toRadians(referencePosition.getLon());
const dec1 = toRadians(referencePosition.getLat());
const ra2 = toRadians(rotatedReferencePosition.getLon());
const dec2 = toRadians(rotatedReferencePosition.getLat());
const ra = toRadians(positionToRotate.getLon());
const dec = toRadians(positionToRotate.getLat());
// Compute (x, y, z), the unit vector in R3 corresponding to positionToRotate
const cos_ra = Math.cos(ra);
const sin_ra = Math.sin(ra);
const cos_dec = Math.cos(dec);
const sin_dec = Math.sin(dec);
let x = cos_ra * cos_dec;
let y = sin_ra * cos_dec;
let z = sin_dec;
// The rotation that maps referencePosition to rotatedReferencePosition
// can be broken down into 3 rotations. The first is a rotation by an
// angle of -ra1 around the z axis. The second is a rotation around the
// y axis by an angle equal to (dec1 - dec2), and the last is around the
// the z axis by ra2. We compute the individual rotations by
// multiplication with the corresponding 3x3 rotation matrix (see
// https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations)
// Rotate by angle theta = -ra1 around the z axis:
//
// [ x1 ] [ cos(ra1) -sin(ra1) 0 ] [ x ]
// [ y1 ] = [ sin(ra1) cos(ra1) 0 ] * [ y ]
// [ z1 ] [ 0 0 1 ] [ z ]
let cos_theta = Math.cos(-ra1);
let sin_theta = Math.sin(-ra1);
let x1 = cos_theta * x - sin_theta * y;
let y1 = sin_theta * x + cos_theta * y;
let z1 = z;
// Rotate by angle theta = (dec1 - dec2) around the y axis:
//
// [ x ] [ cos(dec1 - dec2) 0 sin(dec1 - dec2) ] [ x1 ]
// [ y ] = [ 0 1 0 ] * [ y1 ]
// [ z ] [ -sin(dec1 - dec2) 0 cos(dec1 - dec2) ] [ z1 ]
cos_theta = Math.cos(dec1 - dec2);
sin_theta = Math.sin(dec1 - dec2);
x = cos_theta * x1 + sin_theta * z1;
y = y1;
z = -sin_theta * x1 + cos_theta * z1;
// Rotate by angle theta = ra2 around the z axis:
//
// [ x1 ] [ cos(ra2) -sin(ra2) 0 ] [ x ]
// [ y1 ] = [ sin(ra2) cos(ra2) 0 ] * [ y ]
// [ z1 ] [ 0 0 1 ] [ z ]
cos_theta = Math.cos(ra2);
sin_theta = Math.sin(ra2);
x1 = cos_theta * x - sin_theta * y;
y1 = sin_theta * x + cos_theta * y;
z1 = z;
// Convert the unit vector result back to a WorldPt.
const d = x1 * x1 + y1 * y1;
let lon = 0.0;
let lat = 0.0;
if (d !== 0.0) {
lon = toDegrees(Math.atan2(y1, x1));
if (lon < 0.0) {
lon += 360.0;
}
}
if (z1 !== 0.0) {
lat = toDegrees(Math.atan2(z1, Math.sqrt(d)));
if (lat > 90.0) {
lat = 90.0;
} else if (lat < -90.0) {
lat = -90.0;
}
}
return makeWorldPt(lon, lat);
}
/**
* Compute new position given a position and a distance and position angle
*
* @param {number} ra the equatorial RA in degrees of the first object
* @param {number} dec the equatorial DEC in degrees of the first object
* @param {number} dist the distance in degrees to the second object
* @param {number} phi the position angle in degrees to the second object
* @return {WorldPt} WorldPt of the new object
*/
const getNewPosition= function(ra, dec, dist, phi) {
let tmp;
let ra1;
ra *= DtoR;
dec *= DtoR;
dist *= DtoR;
phi *= DtoR;
tmp = Math.cos(dist) * Math.sin(dec) + Math.sin(dist) * Math.cos(dec) * Math.cos(phi);
const newdec = Math.asin(tmp);
const dec1 = newdec * RtoD;
tmp = Math.cos(dist) * Math.cos(dec) - Math.sin(dist) * Math.sin(dec) * Math.cos(phi);
tmp /= Math.cos(newdec);
const deltaRa = Math.acos(tmp);
if (Math.sin(phi) < 0.0) {
ra1 = ra - deltaRa;
}
else {
ra1 = ra + deltaRa;
}
ra1 *= RtoD;
return makeWorldPt(ra1, dec1);
};
export const getRotationAngle= function(plot) {
let retval = 0;
const iWidth = plot.dataWidth;
const iHeight = plot.dataHeight;
const ix = iWidth / 2;
const iy = iHeight / 2;
const cc= CysConverter.make(plot);
const wptC = cc.getWorldCoords(makeImageWorkSpacePt(ix, iy));
const wpt2 = cc.getWorldCoords(makeImageWorkSpacePt(ix, iy+iHeight/4));
if (wptC && wpt2) {
retval = getPositionAngle(wptC.getLon(), wptC.getLat(), wpt2.getLon(), wpt2.getLat());
}
return retval;
};
/**
* Return true if the plot in both PlotViews are rotated the same
* @param {PlotView} pv1
* @param {PlotView} pv2
* @return {boolean}
*/
export function isRotationMatching(pv1, pv2) {
const p1= primePlot(pv1);
const p2= primePlot(pv2);
if (!p1 || !p2) return false;
if (isNorthCountingRotation(pv1) && isNorthCountingRotation(pv2)) {
return true;
}
else {
const r1= getRotationAngle(p1) + pv1.rotation;
const r2= getRotationAngle(p2) + pv2.rotation;
return Math.abs(r1-r2) < .9;
}
}
function isNorthCountingRotation(pv) {
const plot= primePlot(pv);
if (!plot) return false;
return (pv.plotViewCtx.rotateNorthLock || (isPlotNorth(plot) && !pv.rotation) );
}
/**
* Is the image positioned so that north is up.
* @param {WebPlot} plot
* @return {boolean}
*/
export function isPlotNorth(plot) {
let retval= false;
const ix = plot.dataWidth/ 2;
const iy = plot.dataHeight/ 2;
const cc= CysConverter.make(plot);
const wpt1 = cc.getWorldCoords(makeImageWorkSpacePt(ix, iy));
if (wpt1) {
const cdelt1 = getPixScaleDeg(plot);
const wpt2 = makeWorldPt(wpt1.getLon(), wpt1.getLat() + (Math.abs(cdelt1) / plot.zoomFactor) * (5));
const spt1 = cc.getScreenCoords(wpt1);
const spt2 = cc.getScreenCoords(wpt2);
if (spt1 && spt2) {
retval = (spt1.x===spt2.x && spt1.y > spt2.y);
}
}
return retval;
}
/**
* When plot is rotated north is east on the left hand side. This helps determine if a image is flipped.
* @param plot
* @return {boolean}
*/
export function isEastLeftOfNorth(plot) {
if (!plot) return true;
const mx = plot.dataWidth/2;
const my = plot.dataHeight/2;
const angle= getRotationAngle(plot);
const affTrans= new Matrix();
affTrans.translate(mx,my);
affTrans.rotate(toRadians(-angle));
affTrans.translate(-mx,-my);
affTrans.translateY(plot.dataHeight);
affTrans.scale(1,-1);
const cc= CysConverter.make(plot);
const cenPoint= affTrans.applyToPoint(mx,my);
const wpt1 = cc.getWorldCoords(makeImagePt(cenPoint));
if (wpt1) {
const rotPoint= affTrans.applyToPoint(1,my);
const wpt2 = cc.getWorldCoords(makeImagePt(rotPoint));
if (wpt2) return wpt2.x > wpt1.x;
}
return true;
}
const getEstimatedFullZoomFactor= function(fullType, dataWidth, dataHeight,
screenWidth, screenHeight, tryMinFactor=-1) {
let zFact;
if (fullType===FullType.ONLY_WIDTH || screenHeight <= 0 || dataHeight <= 0) {
zFact = screenWidth / dataWidth;
} else if (fullType===FullType.ONLY_HEIGHT || screenWidth <= 0 || dataWidth <= 0) {
zFact = screenHeight / dataHeight;
} else {
const zFactW = screenWidth / dataWidth;
const zFactH = screenHeight / dataHeight;
if (fullType===FullType.SMART) {
zFact = zFactW;
if (zFactW > Math.max(tryMinFactor, 2)) {
zFact = Math.min(zFactW, zFactH);
}
} else {
zFact = Math.min(zFactW, zFactH);
}
}
return zFact;
};
/**
* Test to see if two rectangles intersect
* @param {number} x0 the first point x, top left
* @param {number} y0 the first point y, top left
* @param {number} w0 the first rec width
* @param {number} h0 the first rec height
* @param {number} x the second point x, top left
* @param {number} y the second point y, top left
* @param {number} w h the second rec width
* @param {number} h the second rec height
* @return {boolean} true if rectangles intersect
*/
export const intersects= function(x0, y0, w0, h0, x, y, w, h) {
if (w0 <= 0 || h0 <= 0 || w <= 0 || h <= 0) {
return false;
}
return (x + w > x0 && y + h > y0 && x < x0 + w0 && y < y0 + h0);
};
/**
* test to see if a point is in a rectangle
* @param x0 the point x of the rec, top left
* @param y0 the point y of the rec, top left
* @param w0 the rec width
* @param h0 the rec height
* @param x the second point x, top left
* @param y the second point y, top left
* @return {boolean} true if rectangles intersect
*/
export const contains= function(x0, y0, w0, h0, x, y) {
return (x >= x0 && y >= y0 && x < x0 + w0 && y < y0 + h0);
};
/**
* test to see if the first rectangle contains the second rectangle
* @param x0 the point x of the rec, top left
* @param y0 the point y of the rec, top left
* @param w0 the rec width
* @param h0 the rec height
* @param x the second point x, top left
* @param y the second point y, top left
* @param w h the second rec width
* @param h the second rec height
* @return {boolean} true if rectangles intersect
*/
export const containsRec= function(x0, y0, w0, h0, x, y, w, h) {
return contains(x0,y0,w0,h0,x,y) && contains(x0,y0,w0,h0,x+w,y+h);
};
export const containsCircle= function(x, y, centerX, centerY, radius) {
return Math.pow((x - centerX), 2) + Math.pow((y - centerY), 2) < radius * radius;
};
export const containsEllipse=function(x, y, centerX, centerY, radius1, radius2) {
return (Math.pow((x - centerX)/radius1, 2) + Math.pow((y - centerY)/radius2, 2)) < 1;
};
const getArrowCoords= function(x1, y1, x2, y2) {
const barbLength = 10;
/* compute shaft angle from arrowhead to tail */
const deltaY = y2 - y1;
const deltaX = x2 - x1;
const shaftAngle = Math.atan2(deltaY, deltaX);
const barbAngle = shaftAngle - 20 * Math.PI / 180; // 20 degrees from shaft
const barbX = x2 - barbLength * Math.cos(barbAngle); // end of barb
const barbY = y2 - barbLength * Math.sin(barbAngle);
let extX = x2 + 6;
let extY = y2 + 6;
const textBaseline= 'top';
const diffX = x2 - x1;
const mult = ((y2 < y1) ? -1 : 1);
if (diffX===0) {
extX = x2;
extY = y2 + mult * 14;
} else {
const slope = ( y2 - y1) / ( x2 - x1);
if (slope >= 3 || slope <= -3) {
extX = x2;
extY = y2 + mult * 14;
} else if (slope < 3 || slope > -3) {
extY = y2 - 6;
if (x2 < x1) {
extX = x2 - 8;
} else {
extX = x2 + 2;
}
}
}
return {
x1, y1, x2, y2,
barbX1 : x2,
barbY1 : y2,
barbX2 : barbX,
barbY2 : barbY,
textX : extX,
textY : extY,
textBaseline
};
};
/**
* Get the bounding of of the array of points
* @param {Array.<{x:number, y:number}>} ptAry
* @return {{x, y, w: number, h: number}}
*/
export function getBoundingBox(ptAry) {
const sortX= ptAry.map( (pt) => pt.x).sort( (v1,v2) => v1-v2);
const sortY= ptAry.map( (pt) => pt.y).sort( (v1,v2) => v1-v2);
const minX= sortX[0];
const minY= sortY[0];
const maxX= sortX[sortX.length-1];
const maxY= sortY[sortY.length-1];
return {x:minX, y:minY, w:Math.abs(maxX-minX), h:Math.abs(maxY-minY)};
}
/**
* @param {object} selection obj with two properties pt0 & pt1
* @param {WebPlot} plot web plot
* @param objList array of DrawObj (must be an array and contain a getCenterPt() method)
* @param selectedShape shape of selected area
* @return {Array} indexes from the objList array that are selected
*/
export function getSelectedPts(selection, plot, objList, selectedShape) {
if (selectedShape === SelectedShape.circle.key) {
return getSelectedPtsFromEllipse(selection, plot, objList);
} else {
return getSelectedPtsFromRect(selection, plot, objList);
}
}
/**
* get selected points from circular selected area
* @param selection
* @param plot
* @param objList
* @returns {Array}
*/
function getSelectedPtsFromEllipse(selection, plot, objList) {
const selectedList= [];
if (selection && plot && objList && objList.length) {
const cc= CysConverter.make(plot);
const pt0= cc.getDeviceCoords(selection.pt0);
const pt1= cc.getDeviceCoords(selection.pt1);
if (!pt0 || !pt1) return selectedList;
const c_x = (pt0.x + pt1.x)/2;
const c_y = (pt0.y + pt1.y)/2;
const r1 = Math.abs(pt0.x-pt1.x)/2;
const r2 = Math.abs(pt0.y-pt1.y)/2;
objList.forEach( (obj,idx) => {
const testObj = cc.getDeviceCoords(DrawOp.getCenterPt(obj));
if (testObj && containsEllipse(testObj.x, testObj.y, c_x, c_y, r1, r2)) {
selectedList.push(idx);
}
});
}
return selectedList;
}
/**
* get selected points from rectanglur selected area
* @param {object} selection obj with two properties pt0 & pt1
* @param {WebPlot} plot web plot
* @param objList array of DrawObj (must be an array and contain a getCenterPt() method)
* @return {Array} indexes from the objList array that are selected
*/
function getSelectedPtsFromRect(selection, plot, objList) {
const selectedList= [];
if (selection && plot && objList && objList.length) {
const cc= CysConverter.make(plot);
const pt0= cc.getDeviceCoords(selection.pt0);
const pt1= cc.getDeviceCoords(selection.pt1);
if (!pt0 || !pt1) return selectedList;
const x= Math.min( pt0.x, pt1.x);
const y= Math.min(pt0.y, pt1.y);
const width= Math.abs(pt0.x-pt1.x);
const height= Math.abs(pt0.y-pt1.y);
objList.forEach( (obj,idx) => {
const testObj = cc.getDeviceCoords(DrawOp.getCenterPt(obj));
if (testObj && contains(x,y,width,height,testObj.x, testObj.y)) {
selectedList.push(idx);
}
});
}
return selectedList;
}
/**
* get the world point at the center of the plot
* @param {WebPlot} plot
* @return {WorldPt}
*/
export function getCenterPtOfPlot(plot) {
if (!plot) return null;
const ip= makeImagePt(plot.dataWidth/2,plot.dataHeight/2);
return CCUtil.getWorldCoords(plot,ip);
}
/**
* Return a WorldPt that is offset by the relative ra and dec from the passed in position
* @param {WorldPt} pos1
* @param {number} offsetRa
* @param {number} offsetDec
* @return {WorldPt}
*/
function calculatePosition(pos1, offsetRa, offsetDec ) {
const ra = toRadians(pos1.getLon());
const dec = toRadians(pos1.getLat());
const de = toRadians(offsetRa/3600.0); // east
const dn = toRadians(offsetDec)/3600.0; // north
const rhat= [];
const shat= [];
const uhat= [];
let ra2, dec2;
const cosRa = Math.cos(ra);
const sinRa = Math.sin(ra);
const cosDec = Math.cos(dec);
const sinDec = Math.sin(dec);
const cosDe = Math.cos(de);
const sinDe = Math.sin(de);
const cosDn = Math.cos(dn);
const sinDn = Math.sin(dn);
rhat[0] = cosDe * cosDn;
rhat[1] = sinDe * cosDn;
rhat[2] = sinDn;
shat[0] = cosDec * rhat[0] - sinDec * rhat[2];
shat[1] = rhat[1];
shat[2] = sinDec * rhat[0] + cosDec * rhat[2];
uhat[0] = cosRa * shat[0] - sinRa * shat[1];
uhat[1] = sinRa * shat[0] + cosRa * shat[1];
uhat[2] = shat[2];
const uxy = Math.sqrt(uhat[0] * uhat[0] + uhat[1] * uhat[1]);
if (uxy>0.0) {
ra2 = Math.atan2(uhat[1], uhat[0]);
}
else {
ra2 = 0.0;
}
dec2 = Math.atan2(uhat[2],uxy);
ra2 = toDegrees(ra2);
dec2 = toDegrees(dec2);
if (ra2 < 0.0) ra2 +=360.0;
return makeWorldPt(ra2, dec2);
}
/**
* Find the corners of a bounding box given the center and the radius
* of a circle
*
* @param center WorldPt the center of the circle
* @param radius in arcsec
* @return object with corners
*/
const getCorners= function(center, radius) {
const posLeft = calculatePosition(center, +radius, 0.0);
const posRight = calculatePosition(center, -radius, 0.0);
const posUp = calculatePosition(center, 0.0, +radius);
const posDown = calculatePosition(center, 0.0, -radius);
const upperLeft = makeWorldPt(posLeft.getLon(), posUp.getLat());
const upperRight = makeWorldPt(posRight.getLon(), posUp.getLat());
const lowerLeft = makeWorldPt(posLeft.getLon(), posDown.getLat());
const lowerRight = makeWorldPt(posRight.getLon(), posDown.getLat());
return {upperLeft, upperRight, lowerLeft, lowerRight};
};
/**
* Return the same point using the WorldPt object. the x,y value is the same but a world point is return with the
* proper coordinate system. If a WorldPt is passed the same point is returned.
* <i>Important</i>: This method should not be used to convert between coordinate systems.
* Example- a ScreenPt with (1,2) will return as a WorldPt with (1,2)
* @param pt the point to translate
* @return WorldPt the World point with the coordinate system set
*/
const getWorldPtRepresentation= function(pt) {
if (!isValidPoint(pt)) return null;
let retval= null;
switch (pt.type) {
case Point.IM_WS_PT:
retval= makeWorldPt(pt.x,pt.y, CoordinateSys.PIXEL);
break;
case Point.SPT:
retval= makeWorldPt(pt.x,pt.y, CoordinateSys.SCREEN_PIXEL);
break;
case Point.IM_PT:
retval= makeWorldPt(pt.x,pt.y, CoordinateSys.PIXEL);
break;
case Point.W_PT:
retval= pt;
break;
}
return retval;
};
const makePt= function(type, x, y) {
let retval= null;
switch (type) {
case Point.IM_WS_PT:
retval= makeImageWorkSpacePt(x,y);
break;
case Point.SPT:
retval= makeScreenPt(x,y);
break;
case Point.IM_PT:
retval= makeImagePt(x,y);
break;
case Point.W_PT:
retval= makeWorldPt(x,y);
break;
}
return retval;
};
export function isAngleUnit(unit) {
return ['deg', 'degree', 'arcmin', 'arcsec', 'radian', 'rad'].includes(unit.toLowerCase());
}
/**
* convert angle value of one unit to that of another unit
* @param {string} from 'degree' or 'deg', 'arcmin', 'arcsec', 'radian' case insensitive
* @param {string} to 'degree' or 'deg', 'arcmin', 'arcsec', 'radian' case insensitive
* @param {*} angle number or string
* @returns {number}
*/
export function convertAngle(from, to, angle) {
const angleUnit = [['deg', 'degree'], 'arcmin', 'arcsec', ['radian', 'rad']];
const rIdx = angleUnit.length-1;
let fromIdx, toIdx;
let numAngle = (typeof angle === 'string') ? parseFloat(angle) : angle;
const unitIdx = (unit) => angleUnit.findIndex( (au) => (isArray(au) ? au.includes(unit) : au === unit));
if (((fromIdx = unitIdx(from.toLowerCase())) < 0) || // invalid unit
((toIdx = unitIdx(to.toLowerCase())) < 0)) {
return numAngle;
} else {
if ( fromIdx === rIdx ) {
numAngle = numAngle * 180.0/Math.PI;
fromIdx = 0;
}
if (toIdx === rIdx) {
numAngle = numAngle * Math.PI/180.0;
toIdx = 0;
}
return numAngle * Math.pow(60.0, (toIdx - fromIdx));
}
}
export function formatFlux(value, plot, band) {
if (isUndefined(value)) return '';
return `${formatFluxValue(value)} ${getFluxUnits(plot,band)}`;
}
export function formatFluxValue(value) {
const absV= Math.abs(value);
return (absV>1000||absV<.01) ?
value.toExponential(6).replace('e+', 'E') :
value.toFixed(6);
}
/**
* find a point on the plot that is top and left but is still in view and on the image.
* If the image is off the screen the return undefined.
* @param {PlotView} pv
* @param {number} xOff
* @param {number} yOff
* @return {DevicePt} the found point
*/
export function getTopmostVisiblePoint(pv,xOff, yOff) {
const plot= primePlot(pv);
const cc= CysConverter.make(plot);
const ipt= cc.getImageCoords(makeDevicePt(xOff,yOff));
if (isImageCoveringArea(pv,ipt,2,2)) return ipt;
const {dataWidth,dataHeight}= plot;
const {viewDim} = pv;
const lineSegs= [
{pt1: cc.getDeviceCoords(makeImagePt(0,0)), pt2: cc.getDeviceCoords(makeImagePt(dataWidth,0))},
{pt1: cc.getDeviceCoords(makeImagePt(dataWidth,0)), pt2: cc.getDeviceCoords(makeImagePt(dataWidth,dataHeight))},
{pt1: cc.getDeviceCoords(makeImagePt(dataWidth,dataHeight)), pt2: cc.getDeviceCoords(makeImagePt(0,dataHeight))},
{pt1: cc.getDeviceCoords(makeImagePt(0,dataHeight)), pt2: cc.getDeviceCoords(makeImagePt(0,0))}
];
const foundSegs= lineSegs
.filter((lineSeg) => {
const {pt1,pt2}= lineSeg;
const iPt= findIntersectionPt(pt1.x,pt1.y,pt2.x,pt2.y, 0,0,viewDim.width-1,0);
return iPt && iPt.onSeg1 && iPt.onSeg2;
})
.sort( (l1, l2) => l1.pt1.x - l2.pt1.x);
if (foundSegs[0]) {
const pt= findIntersectionPt(foundSegs[0].pt1.x,foundSegs[0].pt1.y,
foundSegs[0].pt2.x,foundSegs[0].pt2.y, 0,0,viewDim.width-1,0);
return makeDevicePt(pt.x+xOff, pt.y+yOff);
}
const zXoff= xOff/plot.zoomFactor;
const zYoff= xOff/plot.zoomFactor;
const tryPts= [
makeImagePt(1+zXoff,1+zXoff),
makeImagePt(plot.dataWidth-zXoff,1+zYoff),
makeImagePt(plot.dataWidth-zXoff,plot.dataHeight-zYoff),
makeImagePt(1+zXoff, plot.dataHeight-zYoff),
];
const highPts= tryPts
.map( (p) => cc.getDeviceCoords(p) )
.filter( (p) => cc.pointOnDisplay(p))
.sort( (p1,p2) => p1.y!==p2.y ? p1.y - p2.y : p1.x - p2.x);
return highPts[0];
}
/**
* return true if the image is completely covering the area passed. The width and height are in Device coordinate
* system.
* @param {PlotView} pv
* @param {SimplePoint} pt
* @param {number} width in device coordinates
* @param {number} height in device coordinates
* @return {boolean} true if covering
*/
export function isImageCoveringArea(pv,pt, width,height) {
const plot= primePlot(pv);
const cc= CysConverter.make(plot);
pt= cc.getDeviceCoords(pt);
const testPts= [
makeDevicePt(pt.x,pt.y),
makeDevicePt(pt.x+width,pt.y),
makeDevicePt(pt.x+width,pt.y+height),
makeDevicePt(pt.x,pt.y+height),
];
const polyPts= [
cc.getDeviceCoords(makeImagePt(1,1)),
cc.getDeviceCoords(makeImagePt(plot.dataWidth,1)),
cc.getDeviceCoords(makeImagePt(plot.dataWidth,plot.dataHeight)),
cc.getDeviceCoords(makeImagePt(1, plot.dataHeight))
];
const polyPtsAsArray= polyPts.map( (p) => [p.x,p.y]);
return testPts.every( (p) => pointInPolygon([p.x,p.y], polyPtsAsArray));
}
/**
* Find the point at intersection of two line segments.
* If the lines do intersect then return an object with the intersection point x,y and
* two booleans to represent it the intersection point is on each line segment.
* Return false if the lines do not intersect.
* @param {number} seg1x1 - line segment 1 first point x
* @param {number} seg1y1 - line segment 1 first point y
* @param {number} seg1x2 - line segment 1 second point x
* @param {number} seg1y2 - line segment 1 second point y
* @param {number} seg2x1 - line segment 2 first point x
* @param {number} sec2y2 - line segment 2 first point y
* @param {number} seg2x2 - line segment 2 second point x
* @param {number} seg2y2 - line segment 2 second point y
* @return {{x: number, y:number, onSeg1:boolean, onSeg2:boolean} | boolean}
*/
export function findIntersectionPt(seg1x1, seg1y1, seg1x2, seg1y2, seg2x1, sec2y2, seg2x2, seg2y2) {
const denom = (seg2y2 - sec2y2)*(seg1x2 - seg1x1) - (seg2x2 - seg2x1)*(seg1y2 - seg1y1);
if (!denom) return false;
const ua = ((seg2x2 - seg2x1)*(seg1y1 - sec2y2) - (seg2y2 - sec2y2)*(seg1x1 - seg2x1))/denom;
const ub = ((seg1x2 - seg1x1)*(seg1y1 - sec2y2) - (seg1y2 - seg1y1)*(seg1x1 - seg2x1))/denom;
return {
x: seg1x1 + ua*(seg1x2 - seg1x1),
y: seg1y1 + ua*(seg1y2 - seg1y1),
onSeg1: ua >= 0 && ua <= 1,
onSeg2: ub >= 0 && ub <= 1
};
}
/**
* distance between point and line defined by two end points
* @param pts
* @param cc
* @param pt
* @returns {number}
*/
export function distToLine(pts, cc, pt) {
const spt = cc ? cc.getScreenCoords(pt) : makeScreenPt(pt.x, pt.y);
const pt0 = cc ? cc.getScreenCoords(pts[0]) : makeScreenPt(pts[0].x, pts[0].y);
const pt1 = cc ? cc.getScreenCoords(pts[1]) : makeScreenPt(pts[1].x, pts[1].y);
const e1 = makeScreenPt((pt1.x - pt0.x), (pt1.y - pt0.y));
const e2 = makeScreenPt((spt.x - pt0.x), (spt.y - pt0.y));
const e3 = makeScreenPt((pt0.x - pt1.x), (pt0.y - pt1.y));
const e4 = makeScreenPt((spt.x - pt1.x), (spt.y - pt1.y));
const dpe1e2 = e1.x * e2.x + e1.y * e2.y;
const dpe3e4 = e3.x * e4.x + e3.y * e4.y;
const e1len2 = e1.x * e1.x + e1.y * e1.y;
let ppt;
if (dpe1e2 > 0 && dpe3e4 > 0) { // spt projects between pt1 & pt2
ppt = makeScreenPt(dpe1e2 * e1.x / e1len2 + pt0.x, dpe1e2 * e1.y / e1len2 + pt0.y);
} else if (dpe1e2 <= 0) { // spt projects to right side of pt2
ppt = pt0;
} else { // spt projects to left side of pt1
ppt = pt1;
}
return computeScreenDistance(spt.x, spt.y, ppt.x, ppt.y);
}
/**
* distance between point to polygon boundary
* @param pts
* @param cc
* @param pt
* @returns {*}
*/
export function distanceToPolygon(pts, cc, pt) {
const spt = cc ? cc.getScreenCoords(pt) : makeScreenPt(pt.x, pt.y);
const dist = Number.MAX_VALUE;
if (pts.length < 3) return dist;
const corners = pts.map((pt) => (cc ? cc.getScreenCoords(pt) : makeScreenPt(pt.x, pt.y)));
const len = corners.length;
return corners.reduce((prev, pt, idx) => {
const nIdx = (idx+1)%len;
const d = distToLine([corners[idx], corners[nIdx]], cc, spt);
if (d < prev) {
prev = d;
}
return prev;
}, dist);
}
/**
* distance between point to circle boundary
* @param radius in screen pixel
* @param pts center in any domain
* @param cc
* @param pt
* @returns {Number}
*/
export function distanceToCircle(radius, pts, cc, pt) {
const spt = cc ? cc.getScreenCoords(pt) : makeScreenPt(pt.x, pt.y);
let dist = Number.MAX_VALUE;
let r, center;
if (!radius && pts) {
const p0 = cc ? cc.getScreenCoords(pts[0]) : makeScreenPt(pts[0].x, pts[0].y);
const p1 = cc ? cc.getScreenCoords(pts[1]) : makeScreenPt(pts[1].x, pts[1].y);
r = computeSimpleDistance(p0, p1)/2;
center = makeScreenPt((p0.x + p1.x)/2, (p0.y + p1.y)/2);
} else if (radius && pts) {
center = cc ? cc.getScreenCoords(pts[0]) : makeScreenPt(pts[0].x, pts[0].y);
r = radius;
} else {
return dist;
}
dist = Math.abs(computeSimpleDistance(center, spt) - r);
return dist;
}
export default {
DtoR,RtoD,FullType,computeScreenDistance, computeDistance,
computeSimpleDistance,convert,convertToJ2000,
computeCentralPointAndRadius, getPositionAngle, getNewPosition,
getRotationAngle,getTranslateAndRotatePosition,
isPlotNorth, getEstimatedFullZoomFactor,
intersects, contains, containsRec,containsCircle,
getArrowCoords, getSelectedPts, calculatePosition, getCorners,
makePt, getWorldPtRepresentation, getCenterPtOfPlot, toDegrees, convertAngle,
distToLine, distanceToPolygon, distanceToCircle
};