latlon_and_geo.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  2. /* Latitude/longitude spherical geodesy formulae & scripts (c) Chris Veness 2002-2012 */
  3. /* - www.movable-type.co.uk/scripts/latlong.html */
  4. /* */
  5. /* Sample usage: */
  6. /* var p1 = new LatLon(51.5136, -0.0983); */
  7. /* var p2 = new LatLon(51.4778, -0.0015); */
  8. /* var dist = p1.distanceTo(p2); // in km */
  9. /* var brng = p1.bearingTo(p2); // in degrees clockwise from north */
  10. /* ... etc */
  11. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  12. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  13. /* Note that minimal error checking is performed in this example code! */
  14. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  15. /**
  16. * @requires Geo
  17. */
  18. /**
  19. * Creates a point on the earth's surface at the supplied latitude / longitude
  20. *
  21. * @constructor
  22. * @param {Number} lat: latitude in numeric degrees
  23. * @param {Number} lon: longitude in numeric degrees
  24. * @param {Number} [rad=6371]: radius of earth if different value is required from standard 6,371km
  25. */
  26. function LatLon(lat, lon, rad) {
  27. if (typeof(rad) == 'undefined') rad = 6371; // earth's mean radius in km
  28. // only accept numbers or valid numeric strings
  29. this._lat = typeof(lat)=='number' ? lat : typeof(lat)=='string' && lat.trim()!='' ? +lat : NaN;
  30. this._lon = typeof(lon)=='number' ? lon : typeof(lon)=='string' && lon.trim()!='' ? +lon : NaN;
  31. this._radius = typeof(rad)=='number' ? rad : typeof(rad)=='string' && trim(lon)!='' ? +rad : NaN;
  32. }
  33. /**
  34. * Returns the distance from this point to the supplied point, in km
  35. * (using Haversine formula)
  36. *
  37. * from: Haversine formula - R. W. Sinnott, "Virtues of the Haversine",
  38. * Sky and Telescope, vol 68, no 2, 1984
  39. *
  40. * @param {LatLon} point: Latitude/longitude of destination point
  41. * @param {Number} [precision=4]: no of significant digits to use for returned value
  42. * @returns {Number} Distance in km between this point and destination point
  43. */
  44. LatLon.prototype.distanceTo = function(point, precision) {
  45. // default 4 sig figs reflects typical 0.3% accuracy of spherical model
  46. if (typeof precision == 'undefined') precision = 4;
  47. var R = this._radius;
  48. var lat1 = this._lat.toRad(), lon1 = this._lon.toRad();
  49. var lat2 = point._lat.toRad(), lon2 = point._lon.toRad();
  50. var dLat = lat2 - lat1;
  51. var dLon = lon2 - lon1;
  52. var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
  53. Math.cos(lat1) * Math.cos(lat2) *
  54. Math.sin(dLon/2) * Math.sin(dLon/2);
  55. var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  56. var d = R * c;
  57. return d.toPrecisionFixed(precision);
  58. }
  59. /**
  60. * Returns the (initial) bearing from this point to the supplied point, in degrees
  61. * see http://williams.best.vwh.net/avform.htm#Crs
  62. *
  63. * @param {LatLon} point: Latitude/longitude of destination point
  64. * @returns {Number} Initial bearing in degrees from North
  65. */
  66. LatLon.prototype.bearingTo = function(point) {
  67. var lat1 = this._lat.toRad(), lat2 = point._lat.toRad();
  68. var dLon = (point._lon-this._lon).toRad();
  69. var y = Math.sin(dLon) * Math.cos(lat2);
  70. var x = Math.cos(lat1)*Math.sin(lat2) -
  71. Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
  72. var brng = Math.atan2(y, x);
  73. return (brng.toDeg()+360) % 360;
  74. }
  75. /**
  76. * Returns final bearing arriving at supplied destination point from this point; the final bearing
  77. * will differ from the initial bearing by varying degrees according to distance and latitude
  78. *
  79. * @param {LatLon} point: Latitude/longitude of destination point
  80. * @returns {Number} Final bearing in degrees from North
  81. */
  82. LatLon.prototype.finalBearingTo = function(point) {
  83. // get initial bearing from supplied point back to this point...
  84. var lat1 = point._lat.toRad(), lat2 = this._lat.toRad();
  85. var dLon = (this._lon-point._lon).toRad();
  86. var y = Math.sin(dLon) * Math.cos(lat2);
  87. var x = Math.cos(lat1)*Math.sin(lat2) -
  88. Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
  89. var brng = Math.atan2(y, x);
  90. // ... & reverse it by adding 180°
  91. return (brng.toDeg()+180) % 360;
  92. }
  93. /**
  94. * Returns the midpoint between this point and the supplied point.
  95. * see http://mathforum.org/library/drmath/view/51822.html for derivation
  96. *
  97. * @param {LatLon} point: Latitude/longitude of destination point
  98. * @returns {LatLon} Midpoint between this point and the supplied point
  99. */
  100. LatLon.prototype.midpointTo = function(point) {
  101. lat1 = this._lat.toRad(), lon1 = this._lon.toRad();
  102. lat2 = point._lat.toRad();
  103. var dLon = (point._lon-this._lon).toRad();
  104. var Bx = Math.cos(lat2) * Math.cos(dLon);
  105. var By = Math.cos(lat2) * Math.sin(dLon);
  106. lat3 = Math.atan2(Math.sin(lat1)+Math.sin(lat2),
  107. Math.sqrt( (Math.cos(lat1)+Bx)*(Math.cos(lat1)+Bx) + By*By) );
  108. lon3 = lon1 + Math.atan2(By, Math.cos(lat1) + Bx);
  109. lon3 = (lon3+3*Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..+180º
  110. return new LatLon(lat3.toDeg(), lon3.toDeg());
  111. }
  112. /**
  113. * Returns the destination point from this point having travelled the given distance (in km) on the
  114. * given initial bearing (bearing may vary before destination is reached)
  115. *
  116. * see http://williams.best.vwh.net/avform.htm#LL
  117. *
  118. * @param {Number} brng: Initial bearing in degrees
  119. * @param {Number} dist: Distance in km
  120. * @returns {LatLon} Destination point
  121. */
  122. LatLon.prototype.destinationPoint = function(brng, dist) {
  123. dist = typeof(dist)=='number' ? dist : typeof(dist)=='string' && dist.trim()!='' ? +dist : NaN;
  124. dist = dist/this._radius; // convert dist to angular distance in radians
  125. brng = brng.toRad(); //
  126. var lat1 = this._lat.toRad(), lon1 = this._lon.toRad();
  127. var lat2 = Math.asin( Math.sin(lat1)*Math.cos(dist) +
  128. Math.cos(lat1)*Math.sin(dist)*Math.cos(brng) );
  129. var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(dist)*Math.cos(lat1),
  130. Math.cos(dist)-Math.sin(lat1)*Math.sin(lat2));
  131. lon2 = (lon2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..+180º
  132. return new LatLon(lat2.toDeg(), lon2.toDeg());
  133. }
  134. /**
  135. * Returns the point of intersection of two paths defined by point and bearing
  136. *
  137. * see http://williams.best.vwh.net/avform.htm#Intersection
  138. *
  139. * @param {LatLon} p1: First point
  140. * @param {Number} brng1: Initial bearing from first point
  141. * @param {LatLon} p2: Second point
  142. * @param {Number} brng2: Initial bearing from second point
  143. * @returns {LatLon} Destination point (null if no unique intersection defined)
  144. */
  145. LatLon.intersection = function(p1, brng1, p2, brng2) {
  146. brng1 = typeof brng1 == 'number' ? brng1 : typeof brng1 == 'string' && trim(brng1)!='' ? +brng1 : NaN;
  147. brng2 = typeof brng2 == 'number' ? brng2 : typeof brng2 == 'string' && trim(brng2)!='' ? +brng2 : NaN;
  148. lat1 = p1._lat.toRad(), lon1 = p1._lon.toRad();
  149. lat2 = p2._lat.toRad(), lon2 = p2._lon.toRad();
  150. brng13 = brng1.toRad(), brng23 = brng2.toRad();
  151. dLat = lat2-lat1, dLon = lon2-lon1;
  152. dist12 = 2*Math.asin( Math.sqrt( Math.sin(dLat/2)*Math.sin(dLat/2) +
  153. Math.cos(lat1)*Math.cos(lat2)*Math.sin(dLon/2)*Math.sin(dLon/2) ) );
  154. if (dist12 == 0) return null;
  155. // initial/final bearings between points
  156. brngA = Math.acos( ( Math.sin(lat2) - Math.sin(lat1)*Math.cos(dist12) ) /
  157. ( Math.sin(dist12)*Math.cos(lat1) ) );
  158. if (isNaN(brngA)) brngA = 0; // protect against rounding
  159. brngB = Math.acos( ( Math.sin(lat1) - Math.sin(lat2)*Math.cos(dist12) ) /
  160. ( Math.sin(dist12)*Math.cos(lat2) ) );
  161. if (Math.sin(lon2-lon1) > 0) {
  162. brng12 = brngA;
  163. brng21 = 2*Math.PI - brngB;
  164. } else {
  165. brng12 = 2*Math.PI - brngA;
  166. brng21 = brngB;
  167. }
  168. alpha1 = (brng13 - brng12 + Math.PI) % (2*Math.PI) - Math.PI; // angle 2-1-3
  169. alpha2 = (brng21 - brng23 + Math.PI) % (2*Math.PI) - Math.PI; // angle 1-2-3
  170. if (Math.sin(alpha1)==0 && Math.sin(alpha2)==0) return null; // infinite intersections
  171. if (Math.sin(alpha1)*Math.sin(alpha2) < 0) return null; // ambiguous intersection
  172. //alpha1 = Math.abs(alpha1);
  173. //alpha2 = Math.abs(alpha2);
  174. // ... Ed Williams takes abs of alpha1/alpha2, but seems to break calculation?
  175. alpha3 = Math.acos( -Math.cos(alpha1)*Math.cos(alpha2) +
  176. Math.sin(alpha1)*Math.sin(alpha2)*Math.cos(dist12) );
  177. dist13 = Math.atan2( Math.sin(dist12)*Math.sin(alpha1)*Math.sin(alpha2),
  178. Math.cos(alpha2)+Math.cos(alpha1)*Math.cos(alpha3) )
  179. lat3 = Math.asin( Math.sin(lat1)*Math.cos(dist13) +
  180. Math.cos(lat1)*Math.sin(dist13)*Math.cos(brng13) );
  181. dLon13 = Math.atan2( Math.sin(brng13)*Math.sin(dist13)*Math.cos(lat1),
  182. Math.cos(dist13)-Math.sin(lat1)*Math.sin(lat3) );
  183. lon3 = lon1+dLon13;
  184. lon3 = (lon3+3*Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..+180º
  185. return new LatLon(lat3.toDeg(), lon3.toDeg());
  186. }
  187. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  188. /**
  189. * Returns the distance from this point to the supplied point, in km, travelling along a rhumb line
  190. *
  191. * see http://williams.best.vwh.net/avform.htm#Rhumb
  192. *
  193. * @param {LatLon} point: Latitude/longitude of destination point
  194. * @returns {Number} Distance in km between this point and destination point
  195. */
  196. LatLon.prototype.rhumbDistanceTo = function(point) {
  197. var R = this._radius;
  198. var lat1 = this._lat.toRad(), lat2 = point._lat.toRad();
  199. var dLat = (point._lat-this._lat).toRad();
  200. var dLon = Math.abs(point._lon-this._lon).toRad();
  201. var dPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4));
  202. var q = (isFinite(dLat/dPhi)) ? dLat/dPhi : Math.cos(lat1); // E-W line gives dPhi=0
  203. // if dLon over 180° take shorter rhumb across anti-meridian:
  204. if (Math.abs(dLon) > Math.PI) {
  205. dLon = dLon>0 ? -(2*Math.PI-dLon) : (2*Math.PI+dLon);
  206. }
  207. var dist = Math.sqrt(dLat*dLat + q*q*dLon*dLon) * R;
  208. return dist.toPrecisionFixed(4); // 4 sig figs reflects typical 0.3% accuracy of spherical model
  209. }
  210. /**
  211. * Returns the bearing from this point to the supplied point along a rhumb line, in degrees
  212. *
  213. * @param {LatLon} point: Latitude/longitude of destination point
  214. * @returns {Number} Bearing in degrees from North
  215. */
  216. LatLon.prototype.rhumbBearingTo = function(point) {
  217. var lat1 = this._lat.toRad(), lat2 = point._lat.toRad();
  218. var dLon = (point._lon-this._lon).toRad();
  219. var dPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4));
  220. if (Math.abs(dLon) > Math.PI) dLon = dLon>0 ? -(2*Math.PI-dLon) : (2*Math.PI+dLon);
  221. var brng = Math.atan2(dLon, dPhi);
  222. return (brng.toDeg()+360) % 360;
  223. }
  224. /**
  225. * Returns the destination point from this point having travelled the given distance (in km) on the
  226. * given bearing along a rhumb line
  227. *
  228. * @param {Number} brng: Bearing in degrees from North
  229. * @param {Number} dist: Distance in km
  230. * @returns {LatLon} Destination point
  231. */
  232. LatLon.prototype.rhumbDestinationPoint = function(brng, dist) {
  233. var R = this._radius;
  234. var d = parseFloat(dist)/R; // d = angular distance covered on earth’s surface
  235. var lat1 = this._lat.toRad(), lon1 = this._lon.toRad();
  236. brng = brng.toRad();
  237. var dLat = d*Math.cos(brng);
  238. // nasty kludge to overcome ill-conditioned results around parallels of latitude:
  239. if (Math.abs(dLat) < 1e-10) dLat = 0; // dLat < 1 mm
  240. var lat2 = lat1 + dLat;
  241. var dPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4));
  242. var q = (isFinite(dLat/dPhi)) ? dLat/dPhi : Math.cos(lat1); // E-W line gives dPhi=0
  243. var dLon = d*Math.sin(brng)/q;
  244. // check for some daft bugger going past the pole, normalise latitude if so
  245. if (Math.abs(lat2) > Math.PI/2) lat2 = lat2>0 ? Math.PI-lat2 : -Math.PI-lat2;
  246. lon2 = (lon1+dLon+3*Math.PI)%(2*Math.PI) - Math.PI;
  247. return new LatLon(lat2.toDeg(), lon2.toDeg());
  248. }
  249. /**
  250. * Returns the loxodromic midpoint (along a rhumb line) between this point and the supplied point.
  251. * see http://mathforum.org/kb/message.jspa?messageID=148837
  252. *
  253. * @param {LatLon} point: Latitude/longitude of destination point
  254. * @returns {LatLon} Midpoint between this point and the supplied point
  255. */
  256. LatLon.prototype.rhumbMidpointTo = function(point) {
  257. lat1 = this._lat.toRad(), lon1 = this._lon.toRad();
  258. lat2 = point._lat.toRad(), lon2 = point._lon.toRad();
  259. if (Math.abs(lon2-lon1) > Math.PI) lon1 += 2*Math.PI; // crossing anti-meridian
  260. var lat3 = (lat1+lat2)/2;
  261. var f1 = Math.tan(Math.PI/4 + lat1/2);
  262. var f2 = Math.tan(Math.PI/4 + lat2/2);
  263. var f3 = Math.tan(Math.PI/4 + lat3/2);
  264. var lon3 = ( (lon2-lon1)*Math.log(f3) + lon1*Math.log(f2) - lon2*Math.log(f1) ) / Math.log(f2/f1);
  265. if (!isFinite(lon3)) lon3 = (lon1+lon2)/2; // parallel of latitude
  266. lon3 = (lon3+3*Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..+180º
  267. return new LatLon(lat3.toDeg(), lon3.toDeg());
  268. }
  269. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  270. /**
  271. * Returns the latitude of this point; signed numeric degrees if no format, otherwise format & dp
  272. * as per Geo.toLat()
  273. *
  274. * @param {String} [format]: Return value as 'd', 'dm', 'dms'
  275. * @param {Number} [dp=0|2|4]: No of decimal places to display
  276. * @returns {Number|String} Numeric degrees if no format specified, otherwise deg/min/sec
  277. */
  278. LatLon.prototype.lat = function(format, dp) {
  279. if (typeof format == 'undefined') return this._lat;
  280. return Geo.toLat(this._lat, format, dp);
  281. }
  282. /**
  283. * Returns the longitude of this point; signed numeric degrees if no format, otherwise format & dp
  284. * as per Geo.toLon()
  285. *
  286. * @param {String} [format]: Return value as 'd', 'dm', 'dms'
  287. * @param {Number} [dp=0|2|4]: No of decimal places to display
  288. * @returns {Number|String} Numeric degrees if no format specified, otherwise deg/min/sec
  289. */
  290. LatLon.prototype.lon = function(format, dp) {
  291. if (typeof format == 'undefined') return this._lon;
  292. return Geo.toLon(this._lon, format, dp);
  293. }
  294. /**
  295. * Returns a string representation of this point; format and dp as per lat()/lon()
  296. *
  297. * @param {String} [format]: Return value as 'd', 'dm', 'dms'
  298. * @param {Number} [dp=0|2|4]: No of decimal places to display
  299. * @returns {String} Comma-separated latitude/longitude
  300. */
  301. LatLon.prototype.toString = function(format, dp) {
  302. if (typeof format == 'undefined') format = 'dms';
  303. return Geo.toLat(this._lat, format, dp) + ', ' + Geo.toLon(this._lon, format, dp);
  304. }
  305. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  306. // ---- extend Number object with methods for converting degrees/radians
  307. /** Converts numeric degrees to radians */
  308. if (typeof Number.prototype.toRad == 'undefined') {
  309. Number.prototype.toRad = function() {
  310. return this * Math.PI / 180;
  311. }
  312. }
  313. /** Converts radians to numeric (signed) degrees */
  314. if (typeof Number.prototype.toDeg == 'undefined') {
  315. Number.prototype.toDeg = function() {
  316. return this * 180 / Math.PI;
  317. }
  318. }
  319. /**
  320. * Formats the significant digits of a number, using only fixed-point notation (no exponential)
  321. *
  322. * @param {Number} precision: Number of significant digits to appear in the returned string
  323. * @returns {String} A string representation of number which contains precision significant digits
  324. */
  325. if (typeof Number.prototype.toPrecisionFixed == 'undefined') {
  326. Number.prototype.toPrecisionFixed = function(precision) {
  327. // use standard toPrecision method
  328. var n = this.toPrecision(precision);
  329. // ... but replace +ve exponential format with trailing zeros
  330. n = n.replace(/(.+)e\+(.+)/, function(n, sig, exp) {
  331. sig = sig.replace(/\./, ''); // remove decimal from significand
  332. l = sig.length - 1;
  333. while (exp-- > l) sig = sig + '0'; // append zeros from exponent
  334. return sig;
  335. });
  336. // ... and replace -ve exponential format with leading zeros
  337. n = n.replace(/(.+)e-(.+)/, function(n, sig, exp) {
  338. sig = sig.replace(/\./, ''); // remove decimal from significand
  339. while (exp-- > 1) sig = '0' + sig; // prepend zeros from exponent
  340. return '0.' + sig;
  341. });
  342. return n;
  343. }
  344. }
  345. /** Trims whitespace from string (q.v. blog.stevenlevithan.com/archives/faster-trim-javascript) */
  346. if (typeof String.prototype.trim == 'undefined') {
  347. String.prototype.trim = function() {
  348. return String(this).replace(/^\s\s*/, '').replace(/\s\s*$/, '');
  349. }
  350. }
  351. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  352. if (!window.console) window.console = { log: function() {} };
  353. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  354. /* Geodesy representation conversion functions (c) Chris Veness 2002-2012 */
  355. /* - www.movable-type.co.uk/scripts/latlong.html */
  356. /* */
  357. /* Sample usage: */
  358. /* var lat = Geo.parseDMS('51° 28′ 40.12″ N'); */
  359. /* var lon = Geo.parseDMS('000° 00′ 05.31″ W'); */
  360. /* var p1 = new LatLon(lat, lon); */
  361. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  362. var Geo = {}; // Geo namespace, representing static class
  363. /**
  364. * Parses string representing degrees/minutes/seconds into numeric degrees
  365. *
  366. * This is very flexible on formats, allowing signed decimal degrees, or deg-min-sec optionally
  367. * suffixed by compass direction (NSEW). A variety of separators are accepted (eg 3º 37' 09"W)
  368. * or fixed-width format without separators (eg 0033709W). Seconds and minutes may be omitted.
  369. * (Note minimal validation is done).
  370. *
  371. * @param {String|Number} dmsStr: Degrees or deg/min/sec in variety of formats
  372. * @returns {Number} Degrees as decimal number
  373. * @throws {TypeError} dmsStr is an object, perhaps DOM object without .value?
  374. */
  375. Geo.parseDMS = function(dmsStr) {
  376. if (typeof deg == 'object') throw new TypeError('Geo.parseDMS - dmsStr is [DOM?] object');
  377. // check for signed decimal degrees without NSEW, if so return it directly
  378. if (typeof dmsStr === 'number' && isFinite(dmsStr)) return Number(dmsStr);
  379. // strip off any sign or compass dir'n & split out separate d/m/s
  380. var dms = String(dmsStr).trim().replace(/^-/,'').replace(/[NSEW]$/i,'').split(/[^0-9.,]+/);
  381. if (dms[dms.length-1]=='') dms.splice(dms.length-1); // from trailing symbol
  382. if (dms == '') return NaN;
  383. // and convert to decimal degrees...
  384. switch (dms.length) {
  385. case 3: // interpret 3-part result as d/m/s
  386. var deg = dms[0]/1 + dms[1]/60 + dms[2]/3600;
  387. break;
  388. case 2: // interpret 2-part result as d/m
  389. var deg = dms[0]/1 + dms[1]/60;
  390. break;
  391. case 1: // just d (possibly decimal) or non-separated dddmmss
  392. var deg = dms[0];
  393. // check for fixed-width unseparated format eg 0033709W
  394. //if (/[NS]/i.test(dmsStr)) deg = '0' + deg; // - normalise N/S to 3-digit degrees
  395. //if (/[0-9]{7}/.test(deg)) deg = deg.slice(0,3)/1 + deg.slice(3,5)/60 + deg.slice(5)/3600;
  396. break;
  397. default:
  398. return NaN;
  399. }
  400. if (/^-|[WS]$/i.test(dmsStr.trim())) deg = -deg; // take '-', west and south as -ve
  401. return Number(deg);
  402. }
  403. /**
  404. * Convert decimal degrees to deg/min/sec format
  405. * - degree, prime, double-prime symbols are added, but sign is discarded, though no compass
  406. * direction is added
  407. *
  408. * @private
  409. * @param {Number} deg: Degrees
  410. * @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
  411. * @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
  412. * @returns {String} deg formatted as deg/min/secs according to specified format
  413. * @throws {TypeError} deg is an object, perhaps DOM object without .value?
  414. */
  415. Geo.toDMS = function(deg, format, dp) {
  416. if (typeof deg == 'object') throw new TypeError('Geo.toDMS - deg is [DOM?] object');
  417. if (isNaN(deg)) return null; // give up here if we can't make a number from deg
  418. // default values
  419. if (typeof format == 'undefined') format = 'dms';
  420. if (typeof dp == 'undefined') {
  421. switch (format) {
  422. case 'd': dp = 4; break;
  423. case 'dm': dp = 2; break;
  424. case 'dms': dp = 0; break;
  425. default: format = 'dms'; dp = 0; // be forgiving on invalid format
  426. }
  427. }
  428. deg = Math.abs(deg); // (unsigned result ready for appending compass dir'n)
  429. switch (format) {
  430. case 'd':
  431. d = deg.toFixed(dp); // round degrees
  432. if (d<100) d = '0' + d; // pad with leading zeros
  433. if (d<10) d = '0' + d;
  434. dms = d + '\u00B0'; // add º symbol
  435. break;
  436. case 'dm':
  437. var min = (deg*60).toFixed(dp); // convert degrees to minutes & round
  438. var d = Math.floor(min / 60); // get component deg/min
  439. var m = (min % 60).toFixed(dp); // pad with trailing zeros
  440. if (d<100) d = '0' + d; // pad with leading zeros
  441. if (d<10) d = '0' + d;
  442. if (m<10) m = '0' + m;
  443. dms = d + '\u00B0' + m + '\u2032'; // add º, ' symbols
  444. break;
  445. case 'dms':
  446. var sec = (deg*3600).toFixed(dp); // convert degrees to seconds & round
  447. var d = Math.floor(sec / 3600); // get component deg/min/sec
  448. var m = Math.floor(sec/60) % 60;
  449. var s = (sec % 60).toFixed(dp); // pad with trailing zeros
  450. if (d<100) d = '0' + d; // pad with leading zeros
  451. if (d<10) d = '0' + d;
  452. if (m<10) m = '0' + m;
  453. if (s<10) s = '0' + s;
  454. dms = d + '\u00B0' + m + '\u2032' + s + '\u2033'; // add º, ', " symbols
  455. break;
  456. }
  457. return dms;
  458. }
  459. /**
  460. * Convert numeric degrees to deg/min/sec latitude (suffixed with N/S)
  461. *
  462. * @param {Number} deg: Degrees
  463. * @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
  464. * @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
  465. * @returns {String} Deg/min/seconds
  466. */
  467. Geo.toLat = function(deg, format, dp) {
  468. var lat = Geo.toDMS(deg, format, dp);
  469. return lat==null ? '–' : lat.slice(1) + (deg<0 ? 'S' : 'N'); // knock off initial '0' for lat!
  470. }
  471. /**
  472. * Convert numeric degrees to deg/min/sec longitude (suffixed with E/W)
  473. *
  474. * @param {Number} deg: Degrees
  475. * @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
  476. * @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
  477. * @returns {String} Deg/min/seconds
  478. */
  479. Geo.toLon = function(deg, format, dp) {
  480. var lon = Geo.toDMS(deg, format, dp);
  481. return lon==null ? '–' : lon + (deg<0 ? 'W' : 'E');
  482. }
  483. /**
  484. * Convert numeric degrees to deg/min/sec as a bearing (0º..360º)
  485. *
  486. * @param {Number} deg: Degrees
  487. * @param {String} [format=dms]: Return value as 'd', 'dm', 'dms'
  488. * @param {Number} [dp=0|2|4]: No of decimal places to use - default 0 for dms, 2 for dm, 4 for d
  489. * @returns {String} Deg/min/seconds
  490. */
  491. Geo.toBrng = function(deg, format, dp) {
  492. deg = (Number(deg)+360) % 360; // normalise -ve values to 180º..360º
  493. var brng = Geo.toDMS(deg, format, dp);
  494. return brng==null ? '–' : brng.replace('360', '0'); // just in case rounding took us up to 360º!
  495. }
  496. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  497. if (!window.console) window.console = { log: function() {} };