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