Source: latlon-ellipsoidal.js

  1. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  2. /* Geodesy tools for an ellipsoidal earth model (c) Chris Veness 2005-2016 */
  3. /* MIT Licence */
  4. /* www.movable-type.co.uk/scripts/latlong-convert-coords.html */
  5. /* www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-ellipsoidal.html */
  6. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  7. import Dms from './dms.js';
  8. /**
  9. * A latitude/longitude point defines a geographic location on or relative to the earth’s surface,
  10. * measured in degrees from the equator and the Greenwich meridian and metres above the ellipsoid,
  11. * and based on a given datum. The datum defines the reference ellipsoid, and the transformation
  12. * parameters between different datums.
  13. *
  14. * The module includes ellipsoid parameters and datums for different coordinate systems, and methods
  15. * for converting between them and to/from cartesian coordinates.
  16. *
  17. * q.v. Ordnance Survey ‘A guide to coordinate systems in Great Britain’ Section 6
  18. * www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf.
  19. *
  20. * This module is used for both trigonometric methods (eg latlon-vincenty) and n-vector methods (eg
  21. * latlon-nvector-ellipsoidal).
  22. *
  23. * @module latlon-ellipsoidal
  24. * @requires dms
  25. */
  26. /*
  27. * Ellipsoid parameters; semi-major axis (a), semi-minor axis (b), and flattening (f) for each ellipsoid.
  28. * Flattening f = (a−b)/a; at least one of these parameters is derived from defining constants.
  29. */
  30. const ellipsoids = {
  31. WGS84: { a: 6378137, b: 6356752.314245, f: 1/298.257223563 },
  32. GRS80: { a: 6378137, b: 6356752.314140, f: 1/298.257222101 },
  33. Airy1830: { a: 6377563.396, b: 6356256.909, f: 1/299.3249646 },
  34. AiryModified: { a: 6377340.189, b: 6356034.448, f: 1/299.3249646 },
  35. Bessel1841: { a: 6377397.155, b: 6356078.962818, f: 1/299.1528128 },
  36. Clarke1866: { a: 6378206.4, b: 6356583.8, f: 1/294.978698214 },
  37. Intl1924: { a: 6378388, b: 6356911.946, f: 1/297 }, // aka Hayford
  38. WGS72: { a: 6378135, b: 6356750.5, f: 1/298.26 },
  39. };
  40. /*
  41. * Datums; with associated ellipsoid, and Helmert transform parameters to convert from WGS-84 into
  42. * given datum.
  43. *
  44. * More are available from earth-info.nga.mil/GandG/coordsys/datums/NATO_DT.pdf,
  45. * www.fieldenmaps.info/cconv/web/cconv_params.js
  46. */
  47. const datums = {
  48. /* eslint key-spacing: 0, comma-dangle: 0 */
  49. WGS84: {
  50. ellipsoid: ellipsoids.WGS84,
  51. transform: { tx: 0.0, ty: 0.0, tz: 0.0, // m
  52. rx: 0.0, ry: 0.0, rz: 0.0, // sec
  53. s: 0.0 } // ppm
  54. },
  55. NAD83: { // (2009); functionally ≡ WGS84 - www.uvm.edu/giv/resources/WGS84_NAD83.pdf
  56. ellipsoid: ellipsoids.GRS80,
  57. transform: { tx: 1.004, ty: -1.910, tz: -0.515, // m
  58. rx: 0.0267, ry: 0.00034, rz: 0.011, // sec
  59. s: -0.0015 } // ppm
  60. }, // note: if you *really* need to convert WGS84<->NAD83, you need more knowledge than this!
  61. OSGB36: { // www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf
  62. ellipsoid: ellipsoids.Airy1830,
  63. transform: { tx: -446.448, ty: 125.157, tz: -542.060, // m
  64. rx: -0.1502, ry: -0.2470, rz: -0.8421, // sec
  65. s: 20.4894 } // ppm
  66. },
  67. ED50: { // og.decc.gov.uk/en/olgs/cms/pons_and_cop/pons/pon4/pon4.aspx
  68. ellipsoid: ellipsoids.Intl1924,
  69. transform: { tx: 89.5, ty: 93.8, tz: 123.1, // m
  70. rx: 0.0, ry: 0.0, rz: 0.156, // sec
  71. s: -1.2 } // ppm
  72. },
  73. Irl1975: { // osi.ie/OSI/media/OSI/Content/Publications/transformations_booklet.pdf
  74. ellipsoid: ellipsoids.AiryModified,
  75. transform: { tx: -482.530, ty: 130.596, tz: -564.557, // m
  76. rx: -1.042, ry: -0.214, rz: -0.631, // sec
  77. s: -8.150 } // ppm
  78. }, // TODO: many sources have opposite sign to rotations - to be checked!
  79. TokyoJapan: { // www.geocachingtoolbox.com?page=datumEllipsoidDetails
  80. ellipsoid: ellipsoids.Bessel1841,
  81. transform: { tx: 148, ty: -507, tz: -685, // m
  82. rx: 0, ry: 0, rz: 0, // sec
  83. s: 0 } // ppm
  84. },
  85. NAD27: { // en.wikipedia.org/wiki/Helmert_transformation
  86. ellipsoid: ellipsoids.Clarke1866,
  87. transform: { tx: 8, ty: -160, tz: -176, // m
  88. rx: 0, ry: 0, rz: 0, // sec
  89. s: 0 } // ppm
  90. },
  91. WGS72: { // www.icao.int/safety/pbn/documentation/eurocontrol/eurocontrol wgs 84 implementation manual.pdf
  92. ellipsoid: ellipsoids.WGS72,
  93. transform: { tx: 0, ty: 0, tz: -4.5, // m
  94. rx: 0, ry: 0, rz: 0.554, // sec
  95. s: -0.22 } // ppm
  96. },
  97. };
  98. /* LatLonEllipsoidal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  99. /**
  100. * Latitude/longitude points on an ellipsoidal model earth, with ellipsoid parameters and methods
  101. * for converting between datums and to cartesian (ECEF) coordinates.
  102. */
  103. class LatLonEllipsoidal { // note prototype-based class not inheritance-based class
  104. /**
  105. * Creates lat/lon (polar) point with latitude & longitude values, on a specified datum.
  106. *
  107. * @param {number} lat - Geodetic latitude in degrees.
  108. * @param {number} lon - Longitude in degrees.
  109. * @param {number} [height=0] - Height above ellipsoid in metres.
  110. * @param {LatLon.datums} [datum=WGS84] - Datum this point is defined within.
  111. *
  112. * @example
  113. * import LatLon from 'latlon-ellipsoidal';
  114. * var p1 = new LatLon(51.4778, -0.0016, 0, LatLon.datums.WGS84);
  115. */
  116. constructor(lat, lon, height=0, datum=datums.WGS84) {
  117. this.lat = Number(lat);
  118. this.lon = Number(lon);
  119. this.height = Number(height);
  120. this.datum = datum;
  121. }
  122. /**
  123. * Ellipsoid parameters; major axis (a), minor axis (b), and flattening (f) for each ellipsoid.
  124. *
  125. * @example
  126. * var a = LatLon.ellipsoids.Airy1830.a; // 6377563.396
  127. */
  128. static get ellipsoids() {
  129. return ellipsoids;
  130. }
  131. /**
  132. * Datums; with associated ellipsoid and Helmert transform parameters to convert from WGS-84
  133. * into given datum.
  134. *
  135. * @example
  136. * var a = LatLon.datums.OSGB36.ellipsoid.a; // 6377563.396
  137. * var tx = LatLon.datums.OSGB36.transform; // to WGS-84
  138. */
  139. static get datums() {
  140. return datums;
  141. }
  142. /**
  143. * Converts ‘this’ lat/lon coordinate to new coordinate system.
  144. *
  145. * @param {LatLon.datums} toDatum - Datum this coordinate is to be converted to.
  146. * @returns {LatLon} This point converted to new datum.
  147. *
  148. * @example
  149. * var pWGS84 = new LatLon(51.4778, -0.0016, 0, LatLon.datums.WGS84);
  150. * var pOSGB = pWGS84.convertDatum(LatLon.datums.OSGB36); // 51.4773°N, 000.0000°E
  151. */
  152. convertDatum(toDatum) {
  153. var oldLatLon = this;
  154. var transform;
  155. if (oldLatLon.datum == datums.WGS84) {
  156. // converting from WGS 84
  157. transform = toDatum.transform;
  158. }
  159. if (toDatum == datums.WGS84) {
  160. // converting to WGS 84; use inverse transform (don't overwrite original!)
  161. transform = {};
  162. for (var param in oldLatLon.datum.transform) {
  163. if (oldLatLon.datum.transform.hasOwnProperty(param)) {
  164. transform[param] = -oldLatLon.datum.transform[param];
  165. }
  166. }
  167. }
  168. if (transform === undefined) {
  169. // neither this.datum nor toDatum are WGS84: convert this to WGS84 first
  170. oldLatLon = this.convertDatum(datums.WGS84);
  171. transform = toDatum.transform;
  172. }
  173. var oldCartesian = oldLatLon.toCartesian(); // convert polar to cartesian...
  174. var newCartesian = oldCartesian.applyTransform(transform); // ...apply transform...
  175. var newLatLon = newCartesian.toLatLon(toDatum); // ...and convert cartesian to polar
  176. return newLatLon;
  177. }
  178. /**
  179. * Converts ‘this’ point from (geodetic) latitude/longitude coordinates to (geocentric) cartesian
  180. * (x/y/z) coordinates.
  181. *
  182. * @returns {Cartesian} Cartesian point equivalent to lat/lon point, with x, y, z in metres from
  183. * earth centre.
  184. */
  185. toCartesian() {
  186. var φ = this.lat.toRadians(), λ = this.lon.toRadians();
  187. var h = this.height; // height above ellipsoid
  188. var a = this.datum.ellipsoid.a, f = this.datum.ellipsoid.f;
  189. var sinφ = Math.sin(φ), cosφ = Math.cos(φ);
  190. var sinλ = Math.sin(λ), cosλ = Math.cos(λ);
  191. var eSq = 2*f - f*f; // 1st eccentricity squared ≡ (a²-b²)/a²
  192. var ν = a / Math.sqrt(1 - eSq*sinφ*sinφ); // radius of curvature in prime vertical
  193. var x = (ν+h) * cosφ * cosλ;
  194. var y = (ν+h) * cosφ * sinλ;
  195. var z = (ν*(1-eSq)+h) * sinφ;
  196. var p = new Cartesian(x, y, z);
  197. return p;
  198. }
  199. /**
  200. * Returns a string representation of ‘this’ point, formatted as degrees, degrees+minutes, or
  201. * degrees+minutes+seconds.
  202. *
  203. * @param {string} [format=dms] - Format point as 'd', 'dm', 'dms'.
  204. * @param {number} [dp=0|2|4] - Number of decimal places to use: default 0 for dms, 2 for dm, 4 for d.
  205. * @param {number} [heightDp=null] - Number of decimal places to use for height; default is no height display.
  206. * @returns {string} Comma-separated formatted latitude/longitude.
  207. */
  208. toString(format='dms', dp=undefined, heightDp=null) {
  209. var height = heightDp==null ? '' : ' ' + (this.height>0 ? '+' : '') + this.height.toFixed(Number(heightDp)) + 'm';
  210. return Dms.toLat(this.lat, format, dp) + ', ' + Dms.toLon(this.lon, format, dp) + height;
  211. }
  212. }
  213. /* Cartesian - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
  214. /**
  215. * Converts ECEF (earth-centered earth-fixed) cartesian coordinates to LatLon points, applies
  216. * Helmert transformations.
  217. */
  218. class Cartesian { // note prototype-based class not inheritance-based class
  219. /**
  220. * Creates cartesian coordinate representing ECEF (earth-centric earth-fixed) point.
  221. *
  222. * @param {number} x - x coordinate in metres (=> 0°N,0°E).
  223. * @param {number} y - y coordinate in metres (=> 0°N,90°E).
  224. * @param {number} z - z coordinate in metres (=> 90°N).
  225. *
  226. * @example
  227. * import { Cartesian } from 'latlon-ellipsoidal';
  228. * var coord = new Cartesian(3980581.210, -111.159, 4966824.522);
  229. */
  230. constructor(x, y, z) {
  231. this.x = Number(x);
  232. this.y = Number(y);
  233. this.z = Number(z);
  234. }
  235. /**
  236. * Converts ‘this’ (geocentric) cartesian (x/y/z) coordinate to (ellipsoidal geodetic)
  237. * latitude/longitude point on specified datum.
  238. *
  239. * Uses Bowring’s (1985) formulation for μm precision in concise form.
  240. *
  241. * @param {LatLon.datums} [datum=WGS84] - Datum to use when converting point.
  242. */
  243. toLatLon(datum=LatLonEllipsoidal.datums.WGS84) {
  244. var x = this.x, y = this.y, z = this.z;
  245. var a = datum.ellipsoid.a, b = datum.ellipsoid.b, f = datum.ellipsoid.f;
  246. var e2 = 2*f - f*f; // 1st eccentricity squared ≡ (a²-b²)/a²
  247. var ε2 = e2 / (1-e2); // 2nd eccentricity squared ≡ (a²-b²)/b²
  248. var p = Math.sqrt(x*x + y*y); // distance from minor axis
  249. var R = Math.sqrt(p*p + z*z); // polar radius
  250. // parametric latitude (Bowring eqn 17, replacing tanβ = z·a / p·b)
  251. var tanβ = (b*z)/(a*p) * (12*b/R);
  252. var sinβ = tanβ / Math.sqrt(1+tanβ*tanβ);
  253. var cosβ = sinβ / tanβ;
  254. // geodetic latitude (Bowring eqn 18: tanφ = z+ε²bsin³β / p−e²cos³β)
  255. var φ = isNaN(cosβ) ? 0 : Math.atan2(z + ε2*b*sinβ*sinβ*sinβ, p - e2*a*cosβ*cosβ*cosβ);
  256. // longitude
  257. var λ = Math.atan2(y, x);
  258. // height above ellipsoid (Bowring eqn 7)
  259. var sinφ = Math.sin(φ), cosφ = Math.cos(φ);
  260. var ν = a / Math.sqrt(1-e2*sinφ*sinφ); // length of the normal terminated by the minor axis
  261. var h = p*cosφ + z*sinφ - (a*a/ν);
  262. var point = new LatLonEllipsoidal(φ.toDegrees(), λ.toDegrees(), h, datum);
  263. return point;
  264. }
  265. /**
  266. * Applies Helmert (seven-parameter) transformation to ‘this’ coordinate using transform
  267. * parameters t.
  268. *
  269. * @param {LatLon.datums.transform} t - Transformation to apply to this coordinate.
  270. */
  271. applyTransform(t) {
  272. var x1 = this.x, y1 = this.y, z1 = this.z;
  273. var tx = t.tx, ty = t.ty, tz = t.tz;
  274. var rx = (t.rx/3600).toRadians(); // normalise seconds to radians
  275. var ry = (t.ry/3600).toRadians(); // normalise seconds to radians
  276. var rz = (t.rz/3600).toRadians(); // normalise seconds to radians
  277. var s1 = t.s/1e6 + 1; // normalise ppm to (s+1)
  278. // apply transform
  279. var x2 = tx + x1*s1 - y1*rz + z1*ry;
  280. var y2 = ty + x1*rz + y1*s1 - z1*rx;
  281. var z2 = tz - x1*ry + y1*rx + z1*s1;
  282. var point = new Cartesian(x2, y2, z2);
  283. return point;
  284. }
  285. /**
  286. * Returns a string representation of ‘this’ cartesian point.
  287. *
  288. * @param {number} [dp=0] - Number of decimal places to use.
  289. * @returns {string} Comma-separated latitude/longitude.
  290. */
  291. toString(dp=0) {
  292. dp = Number(dp);
  293. return '['+this.x.toFixed(dp)+','+this.y.toFixed(dp)+','+this.z.toFixed(dp)+']';
  294. }
  295. }
  296. // Extend Number object with methods to convert between degrees & radians
  297. Number.prototype.toRadians = function() { return this * Math.PI / 180; };
  298. Number.prototype.toDegrees = function() { return this * 180 / Math.PI; };
  299. export { LatLonEllipsoidal as default, Cartesian };
  300. /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */