Source: dms.js

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/*  Geodesy representation conversion functions                       (c) Chris Veness 2002-2016  */
/*                                                                                   MIT Licence  */
/* www.movable-type.co.uk/scripts/latlong.html                                                    */
/* www.movable-type.co.uk/scripts/geodesy/docs/module-dms.html                                    */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */


/**
 * Latitude/longitude points may be represented as decimal degrees, or subdivided into sexagesimal
 * minutes and seconds.
 *
 * @module dms
 */


/**
 * Functions for parsing and representing degrees / minutes / seconds.
 */
class Dms { // note prototype-based class not inheritance-based class

    // note Unicode Degree = U+00B0. Prime = U+2032, Double prime = U+2033

    /**
     * Parses string representing degrees/minutes/seconds into numeric degrees.
     *
     * This is very flexible on formats, allowing signed decimal degrees, or deg-min-sec optionally
     * suffixed by compass direction (NSEW). A variety of separators are accepted (eg 3° 37′ 09″W).
     * Seconds and minutes may be omitted.
     *
     * @param   {string|number} dmsStr - Degrees or deg/min/sec in variety of formats.
     * @returns {number} Degrees as decimal number.
     *
     * @example
     *   var lat = Dms.parseDMS('51° 28′ 40.12″ N');
     *   var lon = Dms.parseDMS('000° 00′ 05.31″ W');
     *   var p1 = new LatLon(lat, lon); // 51.4778°N, 000.0015°W
     */
    static parseDMS(dmsStr) {
        // check for signed decimal degrees without NSEW, if so return it directly
        if (typeof dmsStr == 'number' && isFinite(dmsStr)) return Number(dmsStr);

        // strip off any sign or compass dir'n & split out separate d/m/s
        var dms = String(dmsStr).trim().replace(/^-/, '').replace(/[NSEW]$/i, '').split(/[^0-9.,]+/);
        if (dms[dms.length-1]=='') dms.splice(dms.length-1);  // from trailing symbol

        if (dms == '') return NaN;

        // and convert to decimal degrees...
        var deg;
        switch (dms.length) {
            case 3:  // interpret 3-part result as d/m/s
                deg = dms[0]/1 + dms[1]/60 + dms[2]/3600;
                break;
            case 2:  // interpret 2-part result as d/m
                deg = dms[0]/1 + dms[1]/60;
                break;
            case 1:  // just d (possibly decimal) or non-separated dddmmss
                deg = dms[0];
                // check for fixed-width unseparated format eg 0033709W
                //if (/[NS]/i.test(dmsStr)) deg = '0' + deg;  // - normalise N/S to 3-digit degrees
                //if (/[0-9]{7}/.test(deg)) deg = deg.slice(0,3)/1 + deg.slice(3,5)/60 + deg.slice(5)/3600;
                break;
            default:
                return NaN;
        }
        if (/^-|[WS]$/i.test(dmsStr.trim())) deg = -deg; // take '-', west and south as -ve

        return Number(deg);
    }


    /**
     * Converts decimal degrees to deg/min/sec format
     *  - degree, prime, double-prime symbols are added, but sign is discarded, though no compass
     *    direction is added.
     *
     * @private
     * @param   {number} deg - Degrees to be formatted as specified.
     * @param   {string} [format=dms] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec.
     * @param   {number} [dp=0|2|4] - Number of decimal places to use – default 0 for dms, 2 for dm, 4 for d.
     * @returns {string} Degrees formatted as deg/min/secs according to specified format.
     */
    static toDMS(deg, format='dms', dp=undefined) {
        if (isNaN(deg)) return null;  // give up here if we can't make a number from deg

        // default values
        if (dp === undefined) {
            switch (format) {
                case 'd':    case 'deg':         dp = 4; break;
                case 'dm':   case 'deg+min':     dp = 2; break;
                case 'dms':  case 'deg+min+sec': dp = 0; break;
                default:    format = 'dms'; dp = 0;  // be forgiving on invalid format
            }
        }

        deg = Math.abs(deg);  // (unsigned result ready for appending compass dir'n)

        var dms, d, m, s;
        switch (format) {
            default: // invalid format spec!
            case 'd': case 'deg':
                d = deg.toFixed(dp);    // round degrees
                if (d<100) d = '0' + d; // pad with leading zeros
                if (d<10) d = '0' + d;
                dms = d + '°';
                break;
            case 'dm': case 'deg+min':
                var min = (deg*60).toFixed(dp); // convert degrees to minutes & round
                d = Math.floor(min / 60);       // get component deg/min
                m = (min % 60).toFixed(dp);     // pad with trailing zeros
                if (d<100) d = '0' + d;         // pad with leading zeros
                if (d<10) d = '0' + d;
                if (m<10) m = '0' + m;
                dms = d + '°' + m + '′';
                break;
            case 'dms': case 'deg+min+sec':
                var sec = (deg*3600).toFixed(dp); // convert degrees to seconds & round
                d = Math.floor(sec / 3600);       // get component deg/min/sec
                m = Math.floor(sec/60) % 60;
                s = (sec % 60).toFixed(dp);       // pad with trailing zeros
                if (d<100) d = '0' + d;           // pad with leading zeros
                if (d<10) d = '0' + d;
                if (m<10) m = '0' + m;
                if (s<10) s = '0' + s;
                dms = d + '°' + m + '′' + s + '″';
                break;
        }

        return dms;
    }


    /**
     * Converts numeric degrees to deg/min/sec latitude (2-digit degrees, suffixed with N/S).
     *
     * @param   {number} deg - Degrees to be formatted as specified.
     * @param   {string} [format=dms] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec.
     * @param   {number} [dp=0|2|4] - Number of decimal places to use – default 0 for dms, 2 for dm, 4 for d.
     * @returns {string} Degrees formatted as deg/min/secs according to specified format.
     */
    static toLat(deg, format, dp) {
        var lat = Dms.toDMS(deg, format, dp);
        return lat===null ? '–' : lat.slice(1) + (deg<0 ? 'S' : 'N');  // knock off initial '0' for lat!
    }


    /**
     * Convert numeric degrees to deg/min/sec longitude (3-digit degrees, suffixed with E/W)
     *
     * @param   {number} deg - Degrees to be formatted as specified.
     * @param   {string} [format=dms] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec.
     * @param   {number} [dp=0|2|4] - Number of decimal places to use – default 0 for dms, 2 for dm, 4 for d.
     * @returns {string} Degrees formatted as deg/min/secs according to specified format.
     */
    static toLon(deg, format, dp) {
        var lon = Dms.toDMS(deg, format, dp);
        return lon===null ? '–' : lon + (deg<0 ? 'W' : 'E');
    }


    /**
     * Converts numeric degrees to deg/min/sec as a bearing (0°..360°)
     *
     * @param   {number} deg - Degrees to be formatted as specified.
     * @param   {string} [format=dms] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec.
     * @param   {number} [dp=0|2|4] - Number of decimal places to use – default 0 for dms, 2 for dm, 4 for d.
     * @returns {string} Degrees formatted as deg/min/secs according to specified format.
     */
    static toBrng(deg, format, dp) {
        deg = (Number(deg)+360) % 360;  // normalise -ve values to 180°..360°
        var brng =  Dms.toDMS(deg, format, dp);
        return brng===null ? '–' : brng.replace('360', '0');  // just in case rounding took us up to 360°!
    }


    /**
     * Returns compass point (to given precision) for supplied bearing.
     *
     * @param   {number} bearing - Bearing in degrees from north.
     * @param   {number} [precision=3] - Precision (1:cardinal / 2:intercardinal / 3:secondary-intercardinal).
     * @returns {string} Compass point for supplied bearing.
     *
     * @example
     *   var point = Dms.compassPoint(24);    // point = 'NNE'
     *   var point = Dms.compassPoint(24, 1); // point = 'N'
     */
    static compassPoint(bearing, precision=3) {
        // note precision = max length of compass point; it could be extended to 4 for quarter-winds
        // (eg NEbN), but I think they are little used

        bearing = ((bearing%360)+360)%360; // normalise to 0..360

        var point;

        switch (precision) {
            case 1: // 4 compass points
                switch (Math.round(bearing*4/360)%4) {
                    case 0: point = 'N'; break;
                    case 1: point = 'E'; break;
                    case 2: point = 'S'; break;
                    case 3: point = 'W'; break;
                }
                break;
            case 2: // 8 compass points
                switch (Math.round(bearing*8/360)%8) {
                    case 0: point = 'N';  break;
                    case 1: point = 'NE'; break;
                    case 2: point = 'E';  break;
                    case 3: point = 'SE'; break;
                    case 4: point = 'S';  break;
                    case 5: point = 'SW'; break;
                    case 6: point = 'W';  break;
                    case 7: point = 'NW'; break;
                }
                break;
            case 3: // 16 compass points
                switch (Math.round(bearing*16/360)%16) {
                    case  0: point = 'N';   break;
                    case  1: point = 'NNE'; break;
                    case  2: point = 'NE';  break;
                    case  3: point = 'ENE'; break;
                    case  4: point = 'E';   break;
                    case  5: point = 'ESE'; break;
                    case  6: point = 'SE';  break;
                    case  7: point = 'SSE'; break;
                    case  8: point = 'S';   break;
                    case  9: point = 'SSW'; break;
                    case 10: point = 'SW';  break;
                    case 11: point = 'WSW'; break;
                    case 12: point = 'W';   break;
                    case 13: point = 'WNW'; break;
                    case 14: point = 'NW';  break;
                    case 15: point = 'NNW'; break;
                }
                break;
            default:
                throw new RangeError('Precision must be between 1 and 3');
        }

        return point;
    }

}


export default Dms;

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */