Source: visualize/VisUtil.js

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