misc.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. # misc.py - miscellaneous geodesy and time functions
  2. # -*- coding: utf-8 -*-
  3. """miscellaneous geodesy and time functions."""
  4. #
  5. # This file is Copyright 2010 by the GPSD project
  6. # SPDX-License-Identifier: BSD-2-Clause
  7. # This code runs compatibly under Python 2 and 3.x for x >= 2.
  8. # Preserve this property!
  9. # A good more complete 3d math implementation:
  10. # https://github.com/geospace-code/pymap3d/
  11. #
  12. from __future__ import absolute_import, print_function, division
  13. import calendar
  14. import io
  15. import math
  16. import time
  17. def monotonic():
  18. """return monotonic seconds, of unknown epoch.
  19. Python 2 to 3.7 has time.clock(), deprecates in 3.3+, removed in 3.8
  20. Python 3.5+ has time.monotonic()
  21. This always works
  22. """
  23. if hasattr(time, 'monotonic'):
  24. return time.monotonic()
  25. # else
  26. return time.clock()
  27. # Determine a single class for testing "stringness"
  28. try:
  29. STR_CLASS = basestring # Base class for 'str' and 'unicode' in Python 2
  30. except NameError:
  31. STR_CLASS = str # In Python 3, 'str' is the base class
  32. # We need to be able to handle data which may be a mixture of text and binary
  33. # data. The text in this context is known to be limited to US-ASCII, so
  34. # there aren't any issues regarding character sets, but we need to ensure
  35. # that binary data is preserved. In Python 2, this happens naturally with
  36. # "strings" and the 'str' and 'bytes' types are synonyms. But in Python 3,
  37. # these are distinct types (with 'str' being based on Unicode), and conversions
  38. # are encoding-sensitive. The most straightforward encoding to use in this
  39. # context is 'latin-1' (a.k.a.'iso-8859-1'), which directly maps all 256
  40. # 8-bit character values to Unicode page 0. Thus, if we can enforce the use
  41. # of 'latin-1' encoding, we can preserve arbitrary binary data while correctly
  42. # mapping any actual text to the proper characters.
  43. BINARY_ENCODING = 'latin-1'
  44. if bytes is str: # In Python 2 these functions can be null transformations
  45. polystr = str
  46. polybytes = bytes
  47. def make_std_wrapper(stream):
  48. """Dummy stdio wrapper function."""
  49. return stream
  50. def get_bytes_stream(stream):
  51. """Dummy stdio bytes buffer function."""
  52. return stream
  53. else: # Otherwise we do something real
  54. def polystr(o):
  55. """Convert bytes or str to str with proper encoding."""
  56. if isinstance(o, str):
  57. return o
  58. if isinstance(o, (bytes, bytearray)):
  59. return str(o, encoding=BINARY_ENCODING)
  60. if isinstance(o, int):
  61. return str(o)
  62. raise ValueError
  63. def polybytes(o):
  64. """Convert bytes or str to bytes with proper encoding."""
  65. if isinstance(o, bytes):
  66. return o
  67. if isinstance(o, str):
  68. return bytes(o, encoding=BINARY_ENCODING)
  69. raise ValueError
  70. def make_std_wrapper(stream):
  71. """Standard input/output wrapper factory function."""
  72. # This ensures that the encoding of standard output and standard
  73. # error on Python 3 matches the binary encoding we use to turn
  74. # bytes to Unicode in polystr above.
  75. #
  76. # newline="\n" ensures that Python 3 won't mangle line breaks
  77. # line_buffering=True ensures that interactive command sessions
  78. # work as expected
  79. return io.TextIOWrapper(stream.buffer, encoding=BINARY_ENCODING,
  80. newline="\n", line_buffering=True)
  81. def get_bytes_stream(stream):
  82. """Standard input/output bytes buffer function"""
  83. return stream.buffer
  84. # WGS84(G1674) defining parameters
  85. # https://en.wikipedia.org/wiki/Geodetic_datum
  86. # Section #World_Geodetic_System_1984_(WGS_84)
  87. #
  88. # http://www.unoosa.org/pdf/icg/2012/template/WGS_84.pdf
  89. # 8-Jul-2014:
  90. # ftp://ftp.nga.mil/pub2/gandg/website/wgs84/NGA.STND.0036_1.0.0_WGS84.pdf
  91. WGS84A = 6378137.0 # equatorial radius (semi-major axis), meters
  92. WGS84F = 298.257223563 # flattening
  93. WGS84B = 6356752.314245 # polar radius (semi-minor axis)
  94. # 1st eccentricity squared = (WGS84A ** 2 + WGS84B **^ 2) / (WGS84A **^ 2)
  95. # valid 8-Jul-2014:
  96. WGS84E = 6.694379990141e-3 # 1st eccentricity squared
  97. # 2nd eccentricity squared = ((WGS84A **^ 2 - WGS84B **^ 2) / (WGS84B **^ 2)
  98. # valid 8-Jul-2014:
  99. WGS84E2 = 6.739496742276e-3 # 2nd eccentricy squared
  100. # WGS 84 value of the earth's gravitational constant for GPS user
  101. # GMgpsnav, valid 8-JUl-2014
  102. # Galileo uses μ = 3.986004418 × 1014 m3/s2
  103. # GLONASS uses 3.986004418e14 м3/s2
  104. WGS84GM = 3.9860050e14 # m^3/second^2
  105. # Earth's Angular Velocity, Omega dot e
  106. # valid 8-Jul-2014:
  107. # also Galileo
  108. # GLONASS uses 7.292115x10-5
  109. WGS84AV = 7.2921151467e-5 # rad/sec
  110. # GLONASS
  111. # ICD_GLONASS_5.1_(2008)_en.pdf
  112. # Table 3.2 Geodesic constants and parametres uniearth ellipsoid ПЗ 90.02
  113. # Earth rotation rate 7,292115x10-5 rad/s
  114. # Gravitational constant 398 600,4418×109 м3/s2
  115. # Gravitational constant of atmosphere( fMa ) 0.35×109 м3/s2
  116. # Speed of light 299 792 458 м/s
  117. # Semi-major axis 6 378 136 м
  118. # Flattening 1/298,257 84
  119. # Equatorial acceleration of gravity 978 032,84 мGal
  120. # Correction to acceleration of gravity at sea-level due to Atmosphere
  121. # 0,87 мGal
  122. # Second zonal harmonic of the geopotential ( J2 0 ) 1082625,75×10-9
  123. # Fourth zonal harmonic of the geopotential ( J4 0 ) (- 2370,89×10-9)
  124. # Sixth zonal harmonic of the geopotential( J6 0 ) 6,08×10-9
  125. # Eighth zonal harmonic of the geopotential ( J8 0 ) 1,40×10-11
  126. # Normal potential at surface of common terrestrial ellipsoid (U0)
  127. # 62 636 861,4 м2/s2
  128. # speed of light (m/s), exact
  129. # same as GLONASS
  130. CLIGHT = 299792458.0
  131. # GPS_PI. Exact! The GPS and Galileo say so.
  132. GPS_PI = 3.1415926535898
  133. # GPS F, sec/sqrt(m), == -2*sqrt(WGS*$M)/c^2
  134. GPS_F = -4.442807633e-10
  135. # GPS L1 Frequency Hz (1575.42 MHz)
  136. GPS_L1_FR = 1575420000
  137. # GPS L1 Wavelength == C / GPS_L1_FR meters
  138. GPS_L1_WL = CLIGHT / GPS_L1_FR
  139. # GPS L2 Frequency Hz (1227.60 MHz)
  140. GPS_L2_FR = 1227600000
  141. # GPS L2 Wavelength == C / GPS_L2_FR meters
  142. GPS_L2_WL = CLIGHT / GPS_L2_FR
  143. # GPS L3 (1381.05 MHz) and L4 (1379.9133) unused as of 2020
  144. # GPS L5 Frequency Hz (1176.45 MHz)
  145. GPS_L5_FR = 1176450000
  146. # GPS L5 Wavelength == C / GPS_L2_FR meters
  147. GPS_L5_WL = CLIGHT / GPS_L5_FR
  148. RAD_2_DEG = 57.2957795130823208767981548141051703
  149. DEG_2_RAD = 0.0174532925199432957692369076848861271
  150. # some multipliers for interpreting GPS output
  151. # Note: A Texas Foot is ( meters * 3937/1200)
  152. # (Texas Natural Resources Code, Subchapter D, Sec 21.071 - 79)
  153. # not the same as an international fooot.
  154. FEET_TO_METERS = 0.3048 # U.S./British feet to meters, exact
  155. METERS_TO_FEET = (1 / FEET_TO_METERS) # Meters to U.S./British feet, exact
  156. MILES_TO_METERS = 1.609344 # Miles to meters, exact
  157. METERS_TO_MILES = (1 / MILES_TO_METERS) # Meters to miles, exact
  158. FATHOMS_TO_METERS = 1.8288 # Fathoms to meters, exact
  159. METERS_TO_FATHOMS = (1 / FATHOMS_TO_METERS) # Meters to fathoms, exact
  160. KNOTS_TO_MPH = (1852 / 1609.344) # Knots to miles per hour, exact
  161. KNOTS_TO_KPH = 1.852 # Knots to kilometers per hour, exact
  162. MPS_TO_KPH = 3.6 # Meters per second to klicks/hr, exact
  163. KNOTS_TO_MPS = (KNOTS_TO_KPH / MPS_TO_KPH) # Knots to meters per second, exact
  164. MPS_TO_MPH = (1 / 0.44704) # Meters/second to miles per hour, exact
  165. MPS_TO_KNOTS = (3600.0 / 1852.0) # Meters per second to knots, exact
  166. def Deg2Rad(x):
  167. """Degrees to radians."""
  168. return x * (math.pi / 180)
  169. def Rad2Deg(x):
  170. """Radians to degrees."""
  171. return x * (180 / math.pi)
  172. def lla2ecef(lat, lon, altHAE):
  173. """Convert Lat, lon (in degrees) and altHAE in meters
  174. to ECEF x, y and z in meters."""
  175. # convert degrees to radians
  176. lat *= DEG_2_RAD
  177. lon *= DEG_2_RAD
  178. sin_lat = math.sin(lat)
  179. cos_lat = math.cos(lat)
  180. n = WGS84A / math.sqrt(1 - WGS84E * (sin_lat ** 2))
  181. x = (n + altHAE) * cos_lat * math.cos(lon)
  182. y = (n + altHAE) * cos_lat * math.sin(lon)
  183. z = (n * (1 - WGS84E) + altHAE) * sin_lat
  184. return (x, y, z)
  185. def ecef2lla(x, y, z):
  186. """Convert ECEF x, y and z in meters to
  187. Lat, lon in degrees and altHAE in meters"""
  188. longitude = math.atan2(y, x) * RAD_2_DEG
  189. p = math.sqrt((x ** 2) + (y ** 2))
  190. theta = math.atan2(z * WGS84A, p * WGS84B)
  191. # sadly Python has no sincos()
  192. sin_theta = math.sin(theta)
  193. cos_theta = math.cos(theta)
  194. phi = math.atan2(z + WGS84E2 * WGS84B * (sin_theta ** 3),
  195. p - WGS84E * WGS84A * (cos_theta ** 3))
  196. latitude = phi * RAD_2_DEG
  197. sin_phi = math.sin(phi)
  198. cos_phi = math.cos(phi)
  199. n = WGS84A / math.sqrt(1.0 - WGS84E * (sin_phi ** 2))
  200. # altitude is WGS84
  201. altHAE = (p / cos_phi) - n
  202. return (latitude, longitude, altHAE)
  203. # FIXME: needs tests
  204. def ecef2enu(x, y, z, lat, lon, altHAE):
  205. """Calculate ENU from lat/lon/altHAE to ECEF
  206. ECEF in meters, lat/lon in degrees, altHAE in meters.
  207. Returns ENU in meters"""
  208. # Grr, lambda is a reserved name in Python...
  209. lambd = lat * DEG_2_RAD
  210. phi = lon * DEG_2_RAD
  211. sin_lambd = math.sin(lambd)
  212. cos_lambd = math.cos(lambd)
  213. n = WGS84A / math.sqrt(1 - WGS84E * sin_lambd ** 2)
  214. sin_phi = math.sin(phi)
  215. cos_phi = math.cos(phi)
  216. # ECEF of observer
  217. x0 = (altHAE + n) * cos_lambd * cos_phi
  218. y0 = (altHAE + n) * cos_lambd * sin_phi
  219. z0 = (altHAE + (1 - WGS84E) * n) * sin_lambd
  220. xd = x - x0
  221. yd = y - y0
  222. zd = z - z0
  223. E = -sin_phi * xd + cos_phi * yd
  224. N = -cos_phi * sin_lambd * xd - sin_lambd * sin_phi * yd + cos_lambd * zd
  225. U = cos_phi * cos_lambd * xd + cos_lambd * sin_phi * yd + sin_lambd * zd
  226. return E, N, U
  227. # FIXME: needs tests.
  228. def enu2aer(E, N, U):
  229. """Convert ENU to Azimuth, Elevation and Range.
  230. ENU is in meters. Returns Azimuth and Elevation in degrees, range in meters"""
  231. enr = math.hypot(E, N)
  232. rng = math.hypot(enr, U)
  233. az = math.atan2(E, N) % (math.pi * 2) * RAD_2_DEG
  234. el = math.atan2(U, enr) * RAD_2_DEG
  235. return az, el, rng
  236. # FIXME: needs tests
  237. def ecef2aer(x, y, z, lat, lon, altHAE):
  238. """Calculate az, el and range to ECEF from lat/lon/altHAE.
  239. ECEF in meters, lat/lon in degrees, altHAE in meters.
  240. Returns Azimuth and Elevation in degrees, range in meters"""
  241. (E, N, U) = ecef2enu(x, y, z, lat, lon, altHAE)
  242. return enu2aer(E, N, U)
  243. def CalcRad(lat):
  244. """Radius of curvature in meters at specified latitude WGS-84."""
  245. # the radius of curvature of an ellipsoidal Earth in the plane of a
  246. # meridian of latitude is given by
  247. #
  248. # R' = a * (1 - e^2) / (1 - e^2 * (sin(lat))^2)^(3/2)
  249. #
  250. # where
  251. # a is the equatorial radius (surface to center distance),
  252. # b is the polar radius (surface to center distance),
  253. # e is the first eccentricity of the ellipsoid
  254. # e2 is e^2 = (a^2 - b^2) / a^2
  255. # es is the second eccentricity of the ellipsoid (UNUSED)
  256. # es2 is es^2 = (a^2 - b^2) / b^2
  257. #
  258. # for WGS-84:
  259. # a = WGS84A/1000 = 6378.137 km (3963 mi)
  260. # b = 6356.752314245 km (3950 mi)
  261. # e2 = WGS84E = 0.00669437999014132
  262. # es2 = 0.00673949674227643
  263. sc = math.sin(math.radians(lat))
  264. x = (WGS84A / 1000) * (1.0 - WGS84E)
  265. z = 1.0 - WGS84E * (sc ** 2)
  266. y = pow(z, 1.5)
  267. r = x / y
  268. r = r * 1000.0 # Convert to meters
  269. return r
  270. def EarthDistance(c1, c2):
  271. """
  272. Vincenty's formula (inverse method) to calculate the distance (in
  273. kilometers or miles) between two points on the surface of a spheroid
  274. WGS 84 accurate to 1mm!
  275. """
  276. (lat1, lon1) = c1
  277. (lat2, lon2) = c2
  278. # WGS 84
  279. a = 6378137 # meters
  280. f = 1 / 298.257223563
  281. b = 6356752.314245 # meters; b = (1 - f)a
  282. # MILES_PER_KILOMETER = 1000.0 / (.3048 * 5280.0)
  283. MAX_ITERATIONS = 200
  284. CONVERGENCE_THRESHOLD = 1e-12 # .000,000,000,001
  285. # short-circuit coincident points
  286. if lat1 == lat2 and lon1 == lon2:
  287. return 0.0
  288. U1 = math.atan((1 - f) * math.tan(math.radians(lat1)))
  289. U2 = math.atan((1 - f) * math.tan(math.radians(lat2)))
  290. L = math.radians(lon1 - lon2)
  291. Lambda = L
  292. sinU1 = math.sin(U1)
  293. cosU1 = math.cos(U1)
  294. sinU2 = math.sin(U2)
  295. cosU2 = math.cos(U2)
  296. for _ in range(MAX_ITERATIONS):
  297. sinLambda = math.sin(Lambda)
  298. cosLambda = math.cos(Lambda)
  299. sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 +
  300. (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2)
  301. if sinSigma == 0:
  302. return 0.0 # coincident points
  303. cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda
  304. sigma = math.atan2(sinSigma, cosSigma)
  305. sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma
  306. cosSqAlpha = 1 - sinAlpha ** 2
  307. try:
  308. cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha
  309. except ZeroDivisionError:
  310. cos2SigmaM = 0
  311. C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha))
  312. LambdaPrev = Lambda
  313. Lambda = L + (1 - C) * f * sinAlpha * (sigma + C * sinSigma *
  314. (cos2SigmaM + C * cosSigma *
  315. (-1 + 2 * cos2SigmaM ** 2)))
  316. if abs(Lambda - LambdaPrev) < CONVERGENCE_THRESHOLD:
  317. break # successful convergence
  318. else:
  319. # failure to converge
  320. # fall back top EarthDistanceSmall
  321. return EarthDistanceSmall(c1, c2)
  322. uSq = cosSqAlpha * (a ** 2 - b ** 2) / (b ** 2)
  323. A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)))
  324. B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)))
  325. deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (
  326. cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM *
  327. (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)))
  328. s = b * A * (sigma - deltaSigma)
  329. # return meters to 6 decimal places
  330. return round(s, 6)
  331. def EarthDistanceSmall(c1, c2):
  332. """Distance in meters between two close points specified in degrees."""
  333. # This calculation is known as an Equirectangular Projection
  334. # fewer numeric issues for small angles that other methods
  335. # the main use here is for when Vincenty's fails to converge.
  336. (lat1, lon1) = c1
  337. (lat2, lon2) = c2
  338. avglat = (lat1 + lat2) / 2
  339. phi = math.radians(avglat) # radians of avg latitude
  340. # meters per degree at this latitude, corrected for WGS84 ellipsoid
  341. # Note the wikipedia numbers are NOT ellipsoid corrected:
  342. # https://en.wikipedia.org/wiki/Decimal_degrees#Precision
  343. m_per_d = (111132.954 - 559.822 * math.cos(2 * phi) +
  344. 1.175 * math.cos(4 * phi))
  345. dlat = (lat1 - lat2) * m_per_d
  346. dlon = (lon1 - lon2) * m_per_d * math.cos(phi)
  347. dist = math.sqrt(math.pow(dlat, 2) + math.pow(dlon, 2))
  348. return dist
  349. def MeterOffset(c1, c2):
  350. """Return offset in meters of second arg from first."""
  351. (lat1, lon1) = c1
  352. (lat2, lon2) = c2
  353. dx = EarthDistance((lat1, lon1), (lat1, lon2))
  354. dy = EarthDistance((lat1, lon1), (lat2, lon1))
  355. if lat1 < lat2:
  356. dy = -dy
  357. if lon1 < lon2:
  358. dx = -dx
  359. return (dx, dy)
  360. def isotime(s):
  361. """Convert timestamps in ISO8661 format to and from Unix time."""
  362. if isinstance(s, int):
  363. return time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(s))
  364. if isinstance(s, float):
  365. date = int(s)
  366. msec = s - date
  367. date = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(s))
  368. return date + "." + repr(msec)[3:]
  369. if isinstance(s, STR_CLASS):
  370. if s[-1] == "Z":
  371. s = s[:-1]
  372. if "." in s:
  373. (date, msec) = s.split(".")
  374. else:
  375. date = s
  376. msec = "0"
  377. # Note: no leap-second correction!
  378. return calendar.timegm(
  379. time.strptime(date, "%Y-%m-%dT%H:%M:%S")) + float("0." + msec)
  380. # else:
  381. raise TypeError
  382. def posix2gps(posix, leapseconds):
  383. """Convert POSIX time in seconds, using leapseconds, to gps time.
  384. Return (gps_time, gps_week, gps_tow)
  385. """
  386. # GPS Epoch starts: Jan 1980 00:00:00 UTC, POSIX/Unix time: 315964800
  387. gps_time = posix - 315964800
  388. gps_time += leapseconds
  389. # 604,800 in a GPS week
  390. (gps_week, gps_tow) = divmod(gps_time, 604800)
  391. return (gps_time, gps_week, gps_tow)
  392. # End
  393. # vim: set expandtab shiftwidth=4