misc.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. # misc.py - miscellaneous geodesy and time functions
  2. "miscellaneous geodesy and time functions"
  3. #
  4. # This file is Copyright 2010 by the GPSD project
  5. # SPDX-License-Identifier: BSD-2-Clause
  6. # This code runs compatibly under Python 2 and 3.x for x >= 2.
  7. # Preserve this property!
  8. from __future__ import absolute_import, print_function, division
  9. import calendar
  10. import io
  11. import math
  12. import time
  13. def monotonic():
  14. """return monotonic seconds, of unknown epoch.
  15. Python 2 to 3.7 has time.clock(), deprecates in 3.3+, removed in 3.8
  16. Python 3.5+ has time.monotonic()
  17. This always works
  18. """
  19. if hasattr(time, 'monotonic'):
  20. return time.monotonic()
  21. # else
  22. return time.clock()
  23. # Determine a single class for testing "stringness"
  24. try:
  25. STR_CLASS = basestring # Base class for 'str' and 'unicode' in Python 2
  26. except NameError:
  27. STR_CLASS = str # In Python 3, 'str' is the base class
  28. # We need to be able to handle data which may be a mixture of text and binary
  29. # data. The text in this context is known to be limited to US-ASCII, so
  30. # there aren't any issues regarding character sets, but we need to ensure
  31. # that binary data is preserved. In Python 2, this happens naturally with
  32. # "strings" and the 'str' and 'bytes' types are synonyms. But in Python 3,
  33. # these are distinct types (with 'str' being based on Unicode), and conversions
  34. # are encoding-sensitive. The most straightforward encoding to use in this
  35. # context is 'latin-1' (a.k.a.'iso-8859-1'), which directly maps all 256
  36. # 8-bit character values to Unicode page 0. Thus, if we can enforce the use
  37. # of 'latin-1' encoding, we can preserve arbitrary binary data while correctly
  38. # mapping any actual text to the proper characters.
  39. BINARY_ENCODING = 'latin-1'
  40. if bytes is str: # In Python 2 these functions can be null transformations
  41. polystr = str
  42. polybytes = bytes
  43. def make_std_wrapper(stream):
  44. "Dummy stdio wrapper function."
  45. return stream
  46. def get_bytes_stream(stream):
  47. "Dummy stdio bytes buffer function."
  48. return stream
  49. else: # Otherwise we do something real
  50. def polystr(o):
  51. "Convert bytes or str to str with proper encoding."
  52. if isinstance(o, str):
  53. return o
  54. if isinstance(o, (bytes, bytearray)):
  55. return str(o, encoding=BINARY_ENCODING)
  56. if isinstance(o, int):
  57. return str(o)
  58. raise ValueError
  59. def polybytes(o):
  60. "Convert bytes or str to bytes with proper encoding."
  61. if isinstance(o, bytes):
  62. return o
  63. if isinstance(o, str):
  64. return bytes(o, encoding=BINARY_ENCODING)
  65. raise ValueError
  66. def make_std_wrapper(stream):
  67. "Standard input/output wrapper factory function"
  68. # This ensures that the encoding of standard output and standard
  69. # error on Python 3 matches the binary encoding we use to turn
  70. # bytes to Unicode in polystr above.
  71. #
  72. # newline="\n" ensures that Python 3 won't mangle line breaks
  73. # line_buffering=True ensures that interactive command sessions
  74. # work as expected
  75. return io.TextIOWrapper(stream.buffer, encoding=BINARY_ENCODING,
  76. newline="\n", line_buffering=True)
  77. def get_bytes_stream(stream):
  78. "Standard input/output bytes buffer function"
  79. return stream.buffer
  80. # some multipliers for interpreting GPS output
  81. # Note: A Texas Foot is ( meters * 3937/1200)
  82. # (Texas Natural Resources Code, Subchapter D, Sec 21.071 - 79)
  83. # not the same as an international fooot.
  84. FEET_TO_METERS = 0.3048 # U.S./British feet to meters, exact
  85. METERS_TO_FEET = (1 / FEET_TO_METERS) # Meters to U.S./British feet, exact
  86. MILES_TO_METERS = 1.609344 # Miles to meters, exact
  87. METERS_TO_MILES = (1 / MILES_TO_METERS) # Meters to miles, exact
  88. FATHOMS_TO_METERS = 1.8288 # Fathoms to meters, exact
  89. METERS_TO_FATHOMS = (1 / FATHOMS_TO_METERS) # Meters to fathoms, exact
  90. KNOTS_TO_MPH = (1852 / 1609.344) # Knots to miles per hour, exact
  91. KNOTS_TO_KPH = 1.852 # Knots to kilometers per hour, exact
  92. MPS_TO_KPH = 3.6 # Meters per second to klicks/hr, exact
  93. KNOTS_TO_MPS = (KNOTS_TO_KPH / MPS_TO_KPH) # Knots to meters per second, exact
  94. MPS_TO_MPH = (1 / 0.44704) # Meters/second to miles per hour, exact
  95. MPS_TO_KNOTS = (3600.0 / 1852.0) # Meters per second to knots, exact
  96. def Deg2Rad(x):
  97. "Degrees to radians."
  98. return x * (math.pi / 180)
  99. def Rad2Deg(x):
  100. "Radians to degrees."
  101. return x * (180 / math.pi)
  102. def CalcRad(lat):
  103. "Radius of curvature in meters at specified latitude WGS-84."
  104. # the radius of curvature of an ellipsoidal Earth in the plane of a
  105. # meridian of latitude is given by
  106. #
  107. # R' = a * (1 - e^2) / (1 - e^2 * (sin(lat))^2)^(3/2)
  108. #
  109. # where
  110. # a is the equatorial radius (surface to center distance),
  111. # b is the polar radius (surface to center distance),
  112. # e is the first eccentricity of the ellipsoid
  113. # e2 is e^2 = (a^2 - b^2) / a^2
  114. # es is the second eccentricity of the ellipsoid (UNUSED)
  115. # es2 is es^2 = (a^2 - b^2) / b^2
  116. #
  117. # for WGS-84:
  118. # a = 6378.137 km (3963 mi)
  119. # b = 6356.752314245 km (3950 mi)
  120. # e2 = 0.00669437999014132
  121. # es2 = 0.00673949674227643
  122. a = 6378.137
  123. e2 = 0.00669437999014132
  124. sc = math.sin(math.radians(lat))
  125. x = a * (1.0 - e2)
  126. z = 1.0 - e2 * pow(sc, 2)
  127. y = pow(z, 1.5)
  128. r = x / y
  129. r = r * 1000.0 # Convert to meters
  130. return r
  131. def EarthDistance(c1, c2):
  132. """
  133. Vincenty's formula (inverse method) to calculate the distance (in
  134. kilometers or miles) between two points on the surface of a spheroid
  135. WGS 84 accurate to 1mm!
  136. """
  137. (lat1, lon1) = c1
  138. (lat2, lon2) = c2
  139. # WGS 84
  140. a = 6378137 # meters
  141. f = 1 / 298.257223563
  142. b = 6356752.314245 # meters; b = (1 - f)a
  143. # MILES_PER_KILOMETER = 1000.0 / (.3048 * 5280.0)
  144. MAX_ITERATIONS = 200
  145. CONVERGENCE_THRESHOLD = 1e-12 # .000,000,000,001
  146. # short-circuit coincident points
  147. if lat1 == lat2 and lon1 == lon2:
  148. return 0.0
  149. U1 = math.atan((1 - f) * math.tan(math.radians(lat1)))
  150. U2 = math.atan((1 - f) * math.tan(math.radians(lat2)))
  151. L = math.radians(lon1 - lon2)
  152. Lambda = L
  153. sinU1 = math.sin(U1)
  154. cosU1 = math.cos(U1)
  155. sinU2 = math.sin(U2)
  156. cosU2 = math.cos(U2)
  157. for _ in range(MAX_ITERATIONS):
  158. sinLambda = math.sin(Lambda)
  159. cosLambda = math.cos(Lambda)
  160. sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 +
  161. (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2)
  162. if sinSigma == 0:
  163. return 0.0 # coincident points
  164. cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda
  165. sigma = math.atan2(sinSigma, cosSigma)
  166. sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma
  167. cosSqAlpha = 1 - sinAlpha ** 2
  168. try:
  169. cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha
  170. except ZeroDivisionError:
  171. cos2SigmaM = 0
  172. C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha))
  173. LambdaPrev = Lambda
  174. Lambda = L + (1 - C) * f * sinAlpha * (sigma + C * sinSigma *
  175. (cos2SigmaM + C * cosSigma *
  176. (-1 + 2 * cos2SigmaM ** 2)))
  177. if abs(Lambda - LambdaPrev) < CONVERGENCE_THRESHOLD:
  178. break # successful convergence
  179. else:
  180. # failure to converge
  181. # fall back top EarthDistanceSmall
  182. return EarthDistanceSmall(c1, c2)
  183. uSq = cosSqAlpha * (a ** 2 - b ** 2) / (b ** 2)
  184. A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)))
  185. B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)))
  186. deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (
  187. cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM *
  188. (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)))
  189. s = b * A * (sigma - deltaSigma)
  190. # return meters to 6 decimal places
  191. return round(s, 6)
  192. def EarthDistanceSmall(c1, c2):
  193. "Distance in meters between two close points specified in degrees."
  194. # This calculation is known as an Equirectangular Projection
  195. # fewer numeric issues for small angles that other methods
  196. # the main use here is for when Vincenty's fails to converge.
  197. (lat1, lon1) = c1
  198. (lat2, lon2) = c2
  199. avglat = (lat1 + lat2) / 2
  200. phi = math.radians(avglat) # radians of avg latitude
  201. # meters per degree at this latitude, corrected for WGS84 ellipsoid
  202. # Note the wikipedia numbers are NOT ellipsoid corrected:
  203. # https://en.wikipedia.org/wiki/Decimal_degrees#Precision
  204. m_per_d = (111132.954 - 559.822 * math.cos(2 * phi) +
  205. 1.175 * math.cos(4 * phi))
  206. dlat = (lat1 - lat2) * m_per_d
  207. dlon = (lon1 - lon2) * m_per_d * math.cos(phi)
  208. dist = math.sqrt(math.pow(dlat, 2) + math.pow(dlon, 2))
  209. return dist
  210. def MeterOffset(c1, c2):
  211. "Return offset in meters of second arg from first."
  212. (lat1, lon1) = c1
  213. (lat2, lon2) = c2
  214. dx = EarthDistance((lat1, lon1), (lat1, lon2))
  215. dy = EarthDistance((lat1, lon1), (lat2, lon1))
  216. if lat1 < lat2:
  217. dy = -dy
  218. if lon1 < lon2:
  219. dx = -dx
  220. return (dx, dy)
  221. def isotime(s):
  222. "Convert timestamps in ISO8661 format to and from Unix time."
  223. if isinstance(s, int):
  224. return time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(s))
  225. if isinstance(s, float):
  226. date = int(s)
  227. msec = s - date
  228. date = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(s))
  229. return date + "." + repr(msec)[3:]
  230. if isinstance(s, STR_CLASS):
  231. if s[-1] == "Z":
  232. s = s[:-1]
  233. if "." in s:
  234. (date, msec) = s.split(".")
  235. else:
  236. date = s
  237. msec = "0"
  238. # Note: no leap-second correction!
  239. return calendar.timegm(
  240. time.strptime(date, "%Y-%m-%dT%H:%M:%S")) + float("0." + msec)
  241. # else:
  242. raise TypeError
  243. # End
  244. # vim: set expandtab shiftwidth=4