/* * DMS.js * Transcription of DMS.[ch]pp into JavaScript. * * See the documentation for the C++ class. The conversion is a literal * conversion from C++. * * Copyright (c) Charles Karney (2011-2019) and licensed * under the MIT/X11 License. For more information, see * https://geographiclib.sourceforge.io/ */ GeographicLib.DMS = {}; (function( /** * @exports GeographicLib/DMS * @description Decode/Encode angles expressed as degrees, minutes, and * seconds. This module defines several constants: * - hemisphere indicator (returned by * {@link module:GeographicLib/DMS.Decode Decode}) and a formatting * indicator (used by * {@link module:GeographicLib/DMS.Encode Encode}) * - NONE = 0, no designator and format as plain angle; * - LATITUDE = 1, a N/S designator and format as latitude; * - LONGITUDE = 2, an E/W designator and format as longitude; * - AZIMUTH = 3, format as azimuth; * - the specification of the trailing component in * {@link module:GeographicLib/DMS.Encode Encode} * - DEGREE; * - MINUTE; * - SECOND. */ d) { var lookup, zerofill, internalDecode, numMatch, hemispheres_ = "SNWE", signs_ = "-+", digits_ = "0123456789", dmsindicators_ = "D'\":", // dmsindicatorsu_ = "\u00b0\u2032\u2033"; // Unicode variants dmsindicatorsu_ = "\u00b0'\"", // Use degree symbol components_ = ["degrees", "minutes", "seconds"]; lookup = function(s, c) { return s.indexOf(c.toUpperCase()); }; zerofill = function(s, n) { return String("0000").substr(0, Math.max(0, Math.min(4, n-s.length))) + s; }; d.NONE = 0; d.LATITUDE = 1; d.LONGITUDE = 2; d.AZIMUTH = 3; d.DEGREE = 0; d.MINUTE = 1; d.SECOND = 2; /** * @summary Decode a DMS string. * @description The interpretation of the string is given in the * documentation of the corresponding function, Decode(string&, flag&) * in the {@link * https://geographiclib.sourceforge.io/html/classGeographicLib_1_1DMS.html * C++ DMS class} * @param {string} dms the string. * @returns {object} r where r.val is the decoded value (degrees) and r.ind * is a hemisphere designator, one of NONE, LATITUDE, LONGITUDE. * @throws an error if the string is illegal. */ d.Decode = function(dms) { var dmsa = dms, end, v = 0, i = 0, mi, pi, vals, ind1 = d.NONE, ind2, p, pa, pb; dmsa = dmsa .replace(/\u2212/g, '-') // U+2212 minus sign .replace(/\u00b0/g, 'd') // U+00b0 degree symbol .replace(/\u00ba/g, 'd') // U+00ba alt symbol .replace(/\u2070/g, 'd') // U+2070 sup zero .replace(/\u02da/g, 'd') // U+02da ring above .replace(/\u2032/g, '\'') // U+2032 prime .replace(/\u00b4/g, '\'') // U+00b4 acute accent .replace(/\u2019/g, '\'') // U+2019 right single quote .replace(/\u2033/g, '"') // U+2033 double prime .replace(/\u201d/g, '"') // U+201d right double quote .replace(/\u00a0/g, '') // U+00a0 non-breaking space .replace(/\u202f/g, '') // U+202f narrow space .replace(/\u2007/g, '') // U+2007 figure space .replace(/''/g, '"') // '' -> " .trim(); end = dmsa.length; // p is pointer to the next piece that needs decoding for (p = 0; p < end; p = pb, ++i) { pa = p; // Skip over initial hemisphere letter (for i == 0) if (i === 0 && lookup(hemispheres_, dmsa.charAt(pa)) >= 0) ++pa; // Skip over initial sign (checking for it if i == 0) if (i > 0 || (pa < end && lookup(signs_, dmsa.charAt(pa)) >= 0)) ++pa; // Find next sign mi = dmsa.substr(pa, end - pa).indexOf('-'); pi = dmsa.substr(pa, end - pa).indexOf('+'); if (mi < 0) mi = end; else mi += pa; if (pi < 0) pi = end; else pi += pa; pb = Math.min(mi, pi); vals = internalDecode(dmsa.substr(p, pb - p)); v += vals.val; ind2 = vals.ind; if (ind1 === d.NONE) ind1 = ind2; else if (!(ind2 === d.NONE || ind1 === ind2)) throw new Error("Incompatible hemisphere specifies in " + dmsa.substr(0, pb)); } if (i === 0) throw new Error("Empty or incomplete DMS string " + dmsa); return {val: v, ind: ind1}; }; internalDecode = function(dmsa) { var vals = {}, errormsg = "", sign, beg, end, ind1, k, ipieces, fpieces, npiece, icurrent, fcurrent, ncurrent, p, pointseen, digcount, intcount, x; do { // Executed once (provides the ability to break) sign = 1; beg = 0; end = dmsa.length; ind1 = d.NONE; k = -1; if (end > beg && (k = lookup(hemispheres_, dmsa.charAt(beg))) >= 0) { ind1 = (k & 2) ? d.LONGITUDE : d.LATITUDE; sign = (k & 1) ? 1 : -1; ++beg; } if (end > beg && (k = lookup(hemispheres_, dmsa.charAt(end-1))) >= 0) { if (k >= 0) { if (ind1 !== d.NONE) { if (dmsa.charAt(beg - 1).toUpperCase() === dmsa.charAt(end - 1).toUpperCase()) errormsg = "Repeated hemisphere indicators " + dmsa.charAt(beg - 1) + " in " + dmsa.substr(beg - 1, end - beg + 1); else errormsg = "Contradictory hemisphere indicators " + dmsa.charAt(beg - 1) + " and " + dmsa.charAt(end - 1) + " in " + dmsa.substr(beg - 1, end - beg + 1); break; } ind1 = (k & 2) ? d.LONGITUDE : d.LATITUDE; sign = (k & 1) ? 1 : -1; --end; } } if (end > beg && (k = lookup(signs_, dmsa.charAt(beg))) >= 0) { if (k >= 0) { sign *= k ? 1 : -1; ++beg; } } if (end === beg) { errormsg = "Empty or incomplete DMS string " + dmsa; break; } ipieces = [0, 0, 0]; fpieces = [0, 0, 0]; npiece = 0; icurrent = 0; fcurrent = 0; ncurrent = 0; p = beg; pointseen = false; digcount = 0; intcount = 0; while (p < end) { x = dmsa.charAt(p++); if ((k = lookup(digits_, x)) >= 0) { ++ncurrent; if (digcount > 0) { ++digcount; // Count of decimal digits } else { icurrent = 10 * icurrent + k; ++intcount; } } else if (x === '.') { if (pointseen) { errormsg = "Multiple decimal points in " + dmsa.substr(beg, end - beg); break; } pointseen = true; digcount = 1; } else if ((k = lookup(dmsindicators_, x)) >= 0) { if (k >= 3) { if (p === end) { errormsg = "Illegal for colon to appear at the end of " + dmsa.substr(beg, end - beg); break; } k = npiece; } if (k === npiece - 1) { errormsg = "Repeated " + components_[k] + " component in " + dmsa.substr(beg, end - beg); break; } else if (k < npiece) { errormsg = components_[k] + " component follows " + components_[npiece - 1] + " component in " + dmsa.substr(beg, end - beg); break; } if (ncurrent === 0) { errormsg = "Missing numbers in " + components_[k] + " component of " + dmsa.substr(beg, end - beg); break; } if (digcount > 0) { fcurrent = parseFloat(dmsa.substr(p - intcount - digcount - 1, intcount + digcount)); icurrent = 0; } ipieces[k] = icurrent; fpieces[k] = icurrent + fcurrent; if (p < end) { npiece = k + 1; icurrent = fcurrent = 0; ncurrent = digcount = intcount = 0; } } else if (lookup(signs_, x) >= 0) { errormsg = "Internal sign in DMS string " + dmsa.substr(beg, end - beg); break; } else { errormsg = "Illegal character " + x + " in DMS string " + dmsa.substr(beg, end - beg); break; } } if (errormsg.length) break; if (lookup(dmsindicators_, dmsa.charAt(p - 1)) < 0) { if (npiece >= 3) { errormsg = "Extra text following seconds in DMS string " + dmsa.substr(beg, end - beg); break; } if (ncurrent === 0) { errormsg = "Missing numbers in trailing component of " + dmsa.substr(beg, end - beg); break; } if (digcount > 0) { fcurrent = parseFloat(dmsa.substr(p - intcount - digcount, intcount + digcount)); icurrent = 0; } ipieces[npiece] = icurrent; fpieces[npiece] = icurrent + fcurrent; } if (pointseen && digcount === 0) { errormsg = "Decimal point in non-terminal component of " + dmsa.substr(beg, end - beg); break; } // Note that we accept 59.999999... even though it rounds to 60. if (ipieces[1] >= 60 || fpieces[1] > 60) { errormsg = "Minutes " + fpieces[1] + " not in range [0,60)"; break; } if (ipieces[2] >= 60 || fpieces[2] > 60) { errormsg = "Seconds " + fpieces[2] + " not in range [0,60)"; break; } vals.ind = ind1; // Assume check on range of result is made by calling routine (which // might be able to offer a better diagnostic). vals.val = sign * ( fpieces[2] ? (60*(60*fpieces[0] + fpieces[1]) + fpieces[2]) / 3600 : ( fpieces[1] ? (60*fpieces[0] + fpieces[1]) / 60 : fpieces[0] ) ); return vals; } while (false); vals.val = numMatch(dmsa); if (vals.val === 0) throw new Error(errormsg); else vals.ind = d.NONE; return vals; }; numMatch = function(s) { var t, sign, p0, p1; if (s.length < 3) return 0; t = s.toUpperCase().replace(/0+$/, ""); sign = t.charAt(0) === '-' ? -1 : 1; p0 = t.charAt(0) === '-' || t.charAt(0) === '+' ? 1 : 0; p1 = t.length - 1; if (p1 + 1 < p0 + 3) return 0; // Strip off sign and trailing 0s t = t.substr(p0, p1 + 1 - p0); // Length at least 3 if (t === "NAN" || t === "1.#QNAN" || t === "1.#SNAN" || t === "1.#IND" || t === "1.#R") return Number.NaN; else if (t === "INF" || t === "1.#INF") return sign * Number.POSITIVE_INFINITY; return 0; }; /** * @summary Decode two DMS strings interpreting them as a latitude/longitude * pair. * @param {string} stra the first string. * @param {string} strb the first string. * @param {bool} [longfirst = false] if true assume then longitude is given * first (in the absence of any hemisphere indicators). * @returns {object} r where r.lat is the decoded latitude and r.lon is the * decoded longitude (both in degrees). * @throws an error if the strings are illegal. */ d.DecodeLatLon = function(stra, strb, longfirst) { var vals = {}, valsa = d.Decode(stra), valsb = d.Decode(strb), a = valsa.val, ia = valsa.ind, b = valsb.val, ib = valsb.ind, lat, lon; if (!longfirst) longfirst = false; if (ia === d.NONE && ib === d.NONE) { // Default to lat, long unless longfirst ia = longfirst ? d.LONGITUDE : d.LATITUDE; ib = longfirst ? d.LATITUDE : d.LONGITUDE; } else if (ia === d.NONE) ia = d.LATITUDE + d.LONGITUDE - ib; else if (ib === d.NONE) ib = d.LATITUDE + d.LONGITUDE - ia; if (ia === ib) throw new Error("Both " + stra + " and " + strb + " interpreted as " + (ia === d.LATITUDE ? "latitudes" : "longitudes")); lat = ia === d.LATITUDE ? a : b; lon = ia === d.LATITUDE ? b : a; if (Math.abs(lat) > 90) throw new Error("Latitude " + lat + " not in [-90,90]"); vals.lat = lat; vals.lon = lon; return vals; }; /** * @summary Decode a DMS string interpreting it as an arc length. * @param {string} angstr the string (this must not include a hemisphere * indicator). * @returns {number} the arc length (degrees). * @throws an error if the string is illegal. */ d.DecodeAngle = function(angstr) { var vals = d.Decode(angstr), ang = vals.val, ind = vals.ind; if (ind !== d.NONE) throw new Error("Arc angle " + angstr + " includes a hemisphere N/E/W/S"); return ang; }; /** * @summary Decode a DMS string interpreting it as an azimuth. * @param {string} azistr the string (this may include an E/W hemisphere * indicator). * @returns {number} the azimuth (degrees). * @throws an error if the string is illegal. */ d.DecodeAzimuth = function(azistr) { var vals = d.Decode(azistr), azi = vals.val, ind = vals.ind; if (ind === d.LATITUDE) throw new Error("Azimuth " + azistr + " has a latitude hemisphere N/S"); return azi; }; /** * @summary Convert angle (in degrees) into a DMS string (using °, ', * and "). * @param {number} angle input angle (degrees). * @param {number} trailing one of DEGREE, MINUTE, or SECOND to indicate * the trailing component of the string (this component is given as a * decimal number if necessary). * @param {number} prec the number of digits after the decimal point for * the trailing component. * @param {number} [ind = NONE] a formatting indicator, one of NONE, * LATITUDE, LONGITUDE, AZIMUTH. * @returns {string} the resulting string formatted as follows: * * NONE, signed result no leading zeros on degrees except in the units * place, e.g., -8°03'. * * LATITUDE, trailing N or S hemisphere designator, no sign, pad * degrees to 2 digits, e.g., 08°03'S. * * LONGITUDE, trailing E or W hemisphere designator, no sign, pad * degrees to 3 digits, e.g., 008°03'W. * * AZIMUTH, convert to the range [0, 360°), no sign, pad degrees to * 3 digits, e.g., 351°57'. */ d.Encode = function(angle, trailing, prec, ind) { // Assume check on range of input angle has been made by calling // routine (which might be able to offer a better diagnostic). var scale = 1, i, sign, idegree, fdegree, f, pieces, ip, fp, s; if (!ind) ind = d.NONE; if (!isFinite(angle)) return angle < 0 ? String("-inf") : (angle > 0 ? String("inf") : String("nan")); // 15 - 2 * trailing = ceiling(log10(2^53/90/60^trailing)). // This suffices to give full real precision for numbers in [-90,90] prec = Math.min(15 - 2 * trailing, prec); for (i = 0; i < trailing; ++i) scale *= 60; for (i = 0; i < prec; ++i) scale *= 10; if (ind === d.AZIMUTH) angle -= Math.floor(angle/360) * 360; sign = angle < 0 ? -1 : 1; angle *= sign; // Break off integer part to preserve precision in manipulation of // fractional part. idegree = Math.floor(angle); fdegree = (angle - idegree) * scale + 0.5; f = Math.floor(fdegree); // Implement the "round ties to even" rule fdegree = (f === fdegree && (f & 1) === 1) ? f - 1 : f; fdegree /= scale; fdegree = Math.floor((angle - idegree) * scale + 0.5) / scale; if (fdegree >= 1) { idegree += 1; fdegree -= 1; } pieces = [fdegree, 0, 0]; for (i = 1; i <= trailing; ++i) { ip = Math.floor(pieces[i - 1]); fp = pieces[i - 1] - ip; pieces[i] = fp * 60; pieces[i - 1] = ip; } pieces[0] += idegree; s = ""; if (ind === d.NONE && sign < 0) s += '-'; switch (trailing) { case d.DEGREE: s += zerofill(pieces[0].toFixed(prec), ind === d.NONE ? 0 : 1 + Math.min(ind, 2) + prec + (prec ? 1 : 0)) + dmsindicatorsu_.charAt(0); break; default: s += zerofill(pieces[0].toFixed(0), ind === d.NONE ? 0 : 1 + Math.min(ind, 2)) + dmsindicatorsu_.charAt(0); switch (trailing) { case d.MINUTE: s += zerofill(pieces[1].toFixed(prec), 2 + prec + (prec ? 1 : 0)) + dmsindicatorsu_.charAt(1); break; case d.SECOND: s += zerofill(pieces[1].toFixed(0), 2) + dmsindicatorsu_.charAt(1); s += zerofill(pieces[2].toFixed(prec), 2 + prec + (prec ? 1 : 0)) + dmsindicatorsu_.charAt(2); break; default: break; } } if (ind !== d.NONE && ind !== d.AZIMUTH) s += hemispheres_.charAt((ind === d.LATITUDE ? 0 : 2) + (sign < 0 ? 0 : 1)); return s; }; })(GeographicLib.DMS);