Source: visualize/Point.js

import {isString, isNumber, isNil, isObject, isArray} from 'lodash';
import CoordinateSys from './CoordSys.js';
import Resolver, {parseResolver} from '../astro/net/Resolver.js';
import validator from 'validator';

const SPT= 'ScreenPt';
const IM_PT= 'ImagePt';
const FITS_IM_PT= 'FitsImagePt';
const ZERO_BASED_IM_PT= 'ZeroBasedImagePt';
const IM_WS_PT= 'ImageWorkSpacePt';
const W_PT= 'WorldPt';
const DEV_PT= 'DevicePt';
const PROJ_PT= 'ProjectionPt';
const OFFSET_PT= 'OffsetPt';

const Point = {  SPT, IM_PT, IM_WS_PT, FITS_IM_PT, ZERO_BASED_IM_PT, DEV_PT, PROJ_PT, W_PT, OFFSET_PT};



/**
 * @typedef {Object} Point
 * @summary a Point
 *
 * @prop {Number} x
 * @prop {Number} y
 * @prop {String} type one of 'RenderedPt', 'ScreenPt', 'ImagePt', 'ImageWorkSpacePt', 'WorldPt', 'ProjectionPt', 'OffsetPt'
 * @public
 * @global
 */

/**
 * @typedef {Object} DevicePt
 * @summary a rendered point on the device screen, including rotation, flipping, etc
 * @prop {Number} x
 * @prop {Number} y
 * @prop {String} type constant must be 'DevicePt'
 * @public
 * @global
 */

/**
 * @typedef {Object} ScreenPt
 * @summary a point on the image screen
 * @prop {Number} x
 * @prop {Number} y
 * @prop {String} type constant must be 'ScreenPt'
 * @public
 * @global
 */

/**
 * @typedef {Object} ImagePt
 * @summary a point in image file coordinates
 * @prop {Number} x
 * @prop {Number} y
 * @prop {String} type constant must be 'ImagePt'
 * @public
 * @global
 */

/**
 * @typedef {Object} FitsImagePt
 * @summary a point in FITS standard image file coordinates
 * @prop {Number} x
 * @prop {Number} y
 * @prop {String} type constant must be 'ImagePt'
 * @public
 * @global
 */



/**
 * @typedef {Object} ZeroBasedImagePt
 * @summary a point in Zero based image file coordinates, ca be offset by LTV1 and LTV2
 * @prop {Number} x
 * @prop {Number} y
 * @prop {String} type constant must be 'ImagePt'
 * @public
 * @global
 */


/**
 * @typedef {Object} WorldPt
 * @summary a point on the sky
 * @prop {Number} x
 * @prop {Number} y
 * @prop {String} type constant must be 'WorldPt'
 * @prop {CoordinateSys} cSys - the coordinate system constant
 * @prop {String} objName - the object name the was used for name resolution, may not be defined
 * @prop {Resolver} objName - the resolver used to create this worldPt, may not be defined
 *
 * @public
 * @global
 */



export class SimplePt {
    constructor(objOrX,y, forceToInt) {
        if (isArray(objOrX)) {
            forceToInt= y || forceToInt; // in  this case, forceToInt was passed in second position
            this.x= Number(objOrX[0]);
            this.y= Number(objOrX[1]);
        }
        else if (isObject(objOrX)) {
            forceToInt= y || forceToInt; // in  this case, forceToInt was passed in second position
            this.x= Number(objOrX.x);
            this.y= Number(objOrX.y);
        }
        else {
            this.x= Number(objOrX);
            this.y= Number(y);
        }
        if (forceToInt) {
            this.x= Math.trunc(this.x);
            this.y= Math.trunc(this.y);
        }
    }
    toString() { return this.x+';'+this.y; }

    static make(x, y, type) {
        const pt = new SimplePt(x, y);

        if (type && Object.keys(Point).includes(type)) {
            pt.type = type;
        }
        return pt;
    }
}


/**
 * WorldPt constructor
 * @type {Function}
 * @constructor
 * @alias module:firefly/visualize/Pt.WorldPt
 * @param {number} lon - the longitude
 * @param {number} lat - the latitude
 * @param {CoordinateSys} [coordSys=CoordinateSys.EQ_J2000]- the coordinate system constant
 * @param {string} [objName] - the object name the was used for name resolution
 * @param {Resolver} [resolver] - the resolver use to return this point
 * @public
 * @global
 */
export class WorldPt {
    constructor(lon,lat,coordSys,objName,resolver) {
        this.x= lon;
        this.y= lat;
        this.cSys = coordSys || CoordinateSys.EQ_J2000;
        if (objName) {
            this.objName = objName;
        }
        if (resolver) {
            this.resolver = resolver;
        }
        this.type= W_PT;

    }
    /**
     * Return the lon
     * @type {function(this:exports.WorldPt)}
     * @return {Number}
     */
    getLon() { return this.x; }

    /**
     * Return the lat
     * @type {function(this:exports.WorldPt)}
     * @return {Number}
     */
    getLat() { return this.y; }

    /**
     * Returns the coordinate system of this point
     * @type {function(this:exports.WorldPt)}
     * @returns {CoordinateSys}
     */
    getCoordSys() { return this.cSys; }


    getResolver() { return this.resolver ? this.resolver : null; }

    getObjName() { return (this.objName) ? this.objName : null; }


    /**
     * return the string representation of the WorldPt. This output can be used
     * to recreate a WorldPt using parseWorldPt
     * @type {function(this:exports.WorldPt)}
     * @return {string}
     */
    toString() {
        let retval = this.x + ';' + this.y + ';' + this.cSys.toString();
        if (this.objName) {
            retval += ';' + this.objName;
            if (this.resolver) {
                retval += ';' + this.resolver.key;
            }
        }
        return retval;
    }

    static parse(inStr) {
        return parseWorldPt(inStr);
    }
}


/**
 *
 * @param {Array.<String>} wpParts
 * @return {*}
 */
function stringAryToWorldPt(wpParts) {
    const len= wpParts.length;
    const x= Number(wpParts[0]);
    const y= Number(wpParts[1]);
    if  (isNaN(x) || isNaN(y)) return null;

    const csys= CoordinateSys.parse(wpParts[2]);

    if (csys===CoordinateSys.PIXEL)        return makeImagePt(x, y); // image point
    if (csys===CoordinateSys.SCREEN_PIXEL) return makeScreenPt(x, y); // screen point

    if (len===2 || len===3) return makeWorldPt(x,y,csys);

    const resolver= wpParts[4] ? parseResolver(wpParts[4]) : Resolver.UNKNOWN;
    return makeWorldPt(x,y,csys, wpParts[3], resolver);
}

/**
 * @summary A point on the sky with a coordinate system
 * @param {number|string} lon - longitude in degrees, strings are converted to numbers
 * @param {number|string} lat - latitude in degrees, strings are converted to numbers
 * @param {CoordinateSys} [coordSys] - The coordinate system of this worldPt
 *
 * @param {String} [objName] -  object name used to create this worldPt
 * @param [resolver] - the resolver used to create this worldPt
 * @return {WorldPt}
 *
 *
 * @function makeWorldPt
 * @public
 * @global
 */
export function makeWorldPt(lon,lat,coordSys= undefined,objName= undefined,resolver= undefined) {
    return new WorldPt(Number(lon),Number(lat),coordSys,objName,resolver) ;
}


/**
 * @summary A point in the image file
 * @param {number|string} x - the x, string is converted to number
 * @param {number|string} y - the y, string is converted to number
 *
 * @return {ImagePt}
 *
 * @function makeImagePt
 * @memberof firefly.util.image
 * @public
 * @global
 */
export const makeImagePt= (x,y) => Object.assign(new SimplePt(x,y), {type:IM_PT});

/**
 * @summary A image point in a FITS file with FITS based offset Standard
 * @param {number|string} x - the x, string is converted to number
 * @param {number|string} y - the y, string is converted to number
 *
 * @return {FitsImagePt}
 *
 * @function makeFitsImagePt
 * @memberof firefly.util.image
 * @public
 * @global
 */
export const makeFitsImagePt= (x,y) => Object.assign(new SimplePt(x,y), {type:FITS_IM_PT});

/**
 * @summary A image point in a Zero based standard, can be offset with the LVT1 and LTV2 headers
 * @param {number|string} x - the x, string is converted to number
 * @param {number|string} y - the y, string is converted to number
 *
 * @return {ImagePt}
 *
 * @function makeZeroBasedImagePt
 * @memberof firefly.util.image
 * @public
 * @global
 */
export const makeZeroBasedImagePt= (x,y) => Object.assign(new SimplePt(x,y), {type:ZERO_BASED_IM_PT});

/**
 *
 * @param {number|string} x - the x, string is converted to number
 * @param {number|string} y - the y, string is converted to number
 * @memberof firefly.util.image
 * @return {Pt}
 */
export const makeImageWorkSpacePt= (x,y) => Object.assign(new SimplePt(x,y), {type:IM_WS_PT});




/**
 * @summary A point of the display image
 * @param {number|string} x - the x, string is converted to number
 * @param {number|string} y - the y, string is converted to number
 *
 * @return {ScreenPt}
 *
 * @function makeScreenPt
 * @memberof firefly.util.image
 * @public
 * @global
 */
export const makeScreenPt= (x,y) => Object.assign(new SimplePt(x,y), {type:SPT});

/**
 * @summary A point on the physical space of the image display area
 * @param {number|string} x - the x, string is converted to number
 * @param {number|string} y - the y, string is converted to number
 *
 * @return {DevicePt}
 *
 * @function makeDevicePt
 * @memberof firefly.util.image
 * @public
 * @global
 */
export const makeDevicePt= (x,y) => Object.assign(new SimplePt(x,y), {type:DEV_PT});

export const makeProjectionPt= (x,y) => Object.assign(new SimplePt(x,y), {type:PROJ_PT});

export const makeOffsetPt= (x,y) => Object.assign(new SimplePt(x,y), {type:OFFSET_PT});


/**
 * given an x,y, and a CoordinageSys object or string, make the correct type of point.
 * @param {number} x
 * @param {number} y
 * @param {CoordinateSys|String} coordSys
 * @return {Point}
 */
export function makeAnyPt(x,y,coordSys) {
    const csys= (coordSys.isEquatorial && coordSys.getJsys && coordSys.getEquinox) ?
                        coordSys  : CoordinateSys.parse(coordSys);

    switch (csys) {
        case CoordinateSys.SCREEN_PIXEL:
        case CoordinateSys.UNDEFINED:    return makeScreenPt(x, y);
        case CoordinateSys.PIXEL:        return makeImagePt(x, y);
        case CoordinateSys.ZEROBASED:    return makeZeroBasedImagePt(x, y);
        case CoordinateSys.FITSPIXEL:    return makeFitsImagePt(x, y);
        default:                         return makeWorldPt(x,y,coordSys);
    }
}


/**
 * @summary Test if two points are equals.  They must be the same coordinate system and have the same values to be
 * equal. Two points that are null or undefined are also considered equal.
 * If both points are WorldPt and are equal in values and coordinate system but have a
 * different resolver and object names, * they are still considered equal.
 *
 * @param {Point} p1 - the first point
 * @param {Point} p2 - the second point
 *
 * @return  true if equals
 *
 * @function pointEquals
 * @memberof firefly.util.image
 * @public
 * @global
 */
export function pointEquals(p1,p2)  {
    if (isNil(p1) && isNil(p2)) return true;
    else if (isNil(p1) || isNil(p2)) return false;
    return (p1.x===p2.x && p1.y===p2.y && p1.type===p2.type && p1.csys===p2.csys);
}


/**
 * @summary Parse a point
 * @param type
 * @param inStr
 * @return {Point}
 * @memberof firefly.util.image
 * @public
 * @global
 */
export const parsePt= function(type, inStr) {
    if (!inStr) return null;
    const parts= inStr.split(';');
    if (parts.length===2 && validator.isFloat(parts[0]) && validator.isFloat(parts[1])) {
        const pt= new SimplePt(validator.toFloat(parts[0]), validator.toFloat(parts[1]));
        pt.type= type;
        return pt;
    }
    return null;
};

export const parseImagePt = (inStr) => parsePt(IM_PT,inStr);
export const parseImageWorkSpacePt = (inStr) => parsePt(IM_WS_PT,inStr);
export const parseScreenPt= (inStr) => parsePt(SPT,inStr);



/**
 *
 * @param serializedWP
 * @return {WorldPt}
 */
export const parseWorldPt = function (serializedWP) {
    if (!serializedWP || !isString(serializedWP)) return null;

    const sAry= serializedWP.split(';');
    if (sAry.length<2 || sAry.length>5) return null;
    return stringAryToWorldPt(sAry);
};

const ptTypes= Object.values(Point);

/**
 * Make sure this is a defined value, valid point object, has a valid type, and the coordinates are numbers.
 * @param {*} testPt - any value
 * @return {boolean} true if this point is valid
 */
export const isValidPoint= (testPt) =>  Boolean(testPt &&
                                                testPt.type && ptTypes.includes(testPt.type) &&
                                                isNumber(testPt.x) && isNumber(testPt.y));

export default Point;