geo.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. from __future__ import annotations
  2. import math
  3. from typing import Iterable, Optional, Tuple
  4. EARTH_RADIUS_M = 6371000.0
  5. _PI = math.pi
  6. _X_PI = _PI * 3000.0 / 180.0
  7. _A = 6378245.0
  8. _EE = 0.00669342162296594323
  9. _VALID_COORD_TYPES = {"WGS84", "GCJ02", "BD09"}
  10. def normalize_coord_type(coord_type: object, *, default: str = "WGS84") -> str:
  11. raw = str(coord_type or "").strip().upper()
  12. if not raw:
  13. return default
  14. aliases = {
  15. "GPS": "WGS84",
  16. "GNSS": "WGS84",
  17. "WGS-84": "WGS84",
  18. "WGS_84": "WGS84",
  19. "GCJ": "GCJ02",
  20. "GCJ-02": "GCJ02",
  21. "GCJ_02": "GCJ02",
  22. "BD-09": "BD09",
  23. "BD_09": "BD09",
  24. "BD09LL": "BD09",
  25. "BAIDU": "BD09",
  26. }
  27. normalized = aliases.get(raw, raw)
  28. if normalized not in _VALID_COORD_TYPES:
  29. raise ValueError("invalid_coord_type")
  30. return normalized
  31. def haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
  32. """Great-circle distance in meters."""
  33. phi1 = math.radians(lat1)
  34. phi2 = math.radians(lat2)
  35. d_phi = math.radians(lat2 - lat1)
  36. d_lam = math.radians(lon2 - lon1)
  37. a = (
  38. math.sin(d_phi / 2.0) ** 2
  39. + math.cos(phi1) * math.cos(phi2) * math.sin(d_lam / 2.0) ** 2
  40. )
  41. c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
  42. return EARTH_RADIUS_M * c
  43. def clamp(n: int, lo: int, hi: int) -> int:
  44. return max(lo, min(hi, n))
  45. def nearest_point_index(
  46. points_latlon: Iterable[Tuple[float, float]],
  47. lat: float,
  48. lon: float,
  49. *,
  50. start_idx: Optional[int] = None,
  51. window: int = 80,
  52. ) -> Tuple[int, float]:
  53. """Return (idx, distance_m) of the nearest point.
  54. Kept for compatibility with older call sites.
  55. """
  56. pts = list(points_latlon)
  57. if not pts:
  58. raise ValueError("empty points")
  59. if start_idx is None:
  60. search_lo = 0
  61. search_hi = len(pts) - 1
  62. else:
  63. search_lo = clamp(start_idx - window, 0, len(pts) - 1)
  64. search_hi = clamp(start_idx + window, 0, len(pts) - 1)
  65. best_i = search_lo
  66. best_d = float("inf")
  67. for i in range(search_lo, search_hi + 1):
  68. p_lat, p_lon = pts[i]
  69. d = haversine_m(lat, lon, p_lat, p_lon)
  70. if d < best_d:
  71. best_d = d
  72. best_i = i
  73. return best_i, best_d
  74. def wgs84_to_bd09(lat: float, lon: float) -> Tuple[float, float]:
  75. gcj_lat, gcj_lon = _wgs84_to_gcj02(lat, lon)
  76. return _gcj02_to_bd09(gcj_lat, gcj_lon)
  77. def wgs84_to_gcj02(lat: float, lon: float) -> Tuple[float, float]:
  78. return _wgs84_to_gcj02(lat, lon)
  79. def gcj02_to_wgs84(lat: float, lon: float) -> Tuple[float, float]:
  80. if _out_of_china(lat, lon):
  81. return lat, lon
  82. wgs_lat = lat
  83. wgs_lon = lon
  84. for _ in range(8):
  85. guessed_lat, guessed_lon = _wgs84_to_gcj02(wgs_lat, wgs_lon)
  86. delta_lat = guessed_lat - lat
  87. delta_lon = guessed_lon - lon
  88. wgs_lat -= delta_lat
  89. wgs_lon -= delta_lon
  90. if abs(delta_lat) < 1e-7 and abs(delta_lon) < 1e-7:
  91. break
  92. return wgs_lat, wgs_lon
  93. def bd09_to_gcj02(lat: float, lon: float) -> Tuple[float, float]:
  94. x = lon - 0.0065
  95. y = lat - 0.006
  96. z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * _X_PI)
  97. theta = math.atan2(y, x) - 0.000003 * math.cos(x * _X_PI)
  98. gcj_lon = z * math.cos(theta)
  99. gcj_lat = z * math.sin(theta)
  100. return gcj_lat, gcj_lon
  101. def bd09_to_wgs84(lat: float, lon: float) -> Tuple[float, float]:
  102. gcj_lat, gcj_lon = bd09_to_gcj02(lat, lon)
  103. return gcj02_to_wgs84(gcj_lat, gcj_lon)
  104. def convert_to_wgs84(
  105. lat: float,
  106. lon: float,
  107. coord_type: object,
  108. ) -> Tuple[float, float]:
  109. normalized = normalize_coord_type(coord_type, default="WGS84")
  110. if normalized == "WGS84":
  111. return lat, lon
  112. if normalized == "GCJ02":
  113. return gcj02_to_wgs84(lat, lon)
  114. return bd09_to_wgs84(lat, lon)
  115. def convert_from_wgs84(
  116. lat: float,
  117. lon: float,
  118. coord_type: object,
  119. ) -> Tuple[float, float]:
  120. normalized = normalize_coord_type(coord_type, default="WGS84")
  121. if normalized == "WGS84":
  122. return lat, lon
  123. if normalized == "GCJ02":
  124. return wgs84_to_gcj02(lat, lon)
  125. return wgs84_to_bd09(lat, lon)
  126. def crosses_alert_line(
  127. prev_lat: float,
  128. prev_lon: float,
  129. curr_lat: float,
  130. curr_lon: float,
  131. start_lat: float,
  132. start_lon: float,
  133. end_lat: float,
  134. end_lon: float,
  135. *,
  136. buffer_m: float = 2.5,
  137. ) -> bool:
  138. ref_lat = (prev_lat + curr_lat + start_lat + end_lat) / 4.0
  139. ref_lon = (prev_lon + curr_lon + start_lon + end_lon) / 4.0
  140. p0 = _latlon_to_xy(prev_lat, prev_lon, ref_lat, ref_lon)
  141. p1 = _latlon_to_xy(curr_lat, curr_lon, ref_lat, ref_lon)
  142. a = _latlon_to_xy(start_lat, start_lon, ref_lat, ref_lon)
  143. b = _latlon_to_xy(end_lat, end_lon, ref_lat, ref_lon)
  144. if _segment_length_m(a, b) < 0.5:
  145. return False
  146. if _segments_intersect(p0, p1, a, b):
  147. return True
  148. side0 = _orientation(a, b, p0)
  149. side1 = _orientation(a, b, p1)
  150. if side0 * side1 > 0:
  151. return False
  152. return _segment_distance_m(p0, p1, a, b) <= max(0.5, buffer_m)
  153. def _out_of_china(lat: float, lon: float) -> bool:
  154. return lon < 72.004 or lon > 137.8347 or lat < 0.8293 or lat > 55.8271
  155. def _transform_lat(x: float, y: float) -> float:
  156. ret = (
  157. -100.0
  158. + 2.0 * x
  159. + 3.0 * y
  160. + 0.2 * y * y
  161. + 0.1 * x * y
  162. + 0.2 * math.sqrt(abs(x))
  163. )
  164. ret += (
  165. (20.0 * math.sin(6.0 * x * _PI) + 20.0 * math.sin(2.0 * x * _PI))
  166. * 2.0
  167. / 3.0
  168. )
  169. ret += (
  170. (20.0 * math.sin(y * _PI) + 40.0 * math.sin(y / 3.0 * _PI))
  171. * 2.0
  172. / 3.0
  173. )
  174. ret += (
  175. (160.0 * math.sin(y / 12.0 * _PI) + 320 * math.sin(y * _PI / 30.0))
  176. * 2.0
  177. / 3.0
  178. )
  179. return ret
  180. def _transform_lon(x: float, y: float) -> float:
  181. ret = (
  182. 300.0
  183. + x
  184. + 2.0 * y
  185. + 0.1 * x * x
  186. + 0.1 * x * y
  187. + 0.1 * math.sqrt(abs(x))
  188. )
  189. ret += (
  190. (20.0 * math.sin(6.0 * x * _PI) + 20.0 * math.sin(2.0 * x * _PI))
  191. * 2.0
  192. / 3.0
  193. )
  194. ret += (
  195. (20.0 * math.sin(x * _PI) + 40.0 * math.sin(x / 3.0 * _PI))
  196. * 2.0
  197. / 3.0
  198. )
  199. ret += (
  200. (150.0 * math.sin(x / 12.0 * _PI) + 300.0 * math.sin(x / 30.0 * _PI))
  201. * 2.0
  202. / 3.0
  203. )
  204. return ret
  205. def _wgs84_to_gcj02(lat: float, lon: float) -> Tuple[float, float]:
  206. if _out_of_china(lat, lon):
  207. return lat, lon
  208. d_lat = _transform_lat(lon - 105.0, lat - 35.0)
  209. d_lon = _transform_lon(lon - 105.0, lat - 35.0)
  210. rad_lat = lat / 180.0 * _PI
  211. magic = math.sin(rad_lat)
  212. magic = 1 - _EE * magic * magic
  213. sqrt_magic = math.sqrt(magic)
  214. d_lat = (d_lat * 180.0) / (((_A * (1 - _EE)) / (magic * sqrt_magic)) * _PI)
  215. d_lon = (d_lon * 180.0) / ((_A / sqrt_magic * math.cos(rad_lat)) * _PI)
  216. return lat + d_lat, lon + d_lon
  217. def _gcj02_to_bd09(lat: float, lon: float) -> Tuple[float, float]:
  218. z = math.sqrt(lon * lon + lat * lat) + 0.00002 * math.sin(lat * _X_PI)
  219. theta = math.atan2(lat, lon) + 0.000003 * math.cos(lon * _X_PI)
  220. bd_lon = z * math.cos(theta) + 0.0065
  221. bd_lat = z * math.sin(theta) + 0.006
  222. return bd_lat, bd_lon
  223. def _latlon_to_xy(
  224. lat: float,
  225. lon: float,
  226. ref_lat: float,
  227. ref_lon: float,
  228. ) -> Tuple[float, float]:
  229. x = math.radians(lon - ref_lon) * EARTH_RADIUS_M * math.cos(math.radians(ref_lat))
  230. y = math.radians(lat - ref_lat) * EARTH_RADIUS_M
  231. return x, y
  232. def _segment_length_m(a: Tuple[float, float], b: Tuple[float, float]) -> float:
  233. return math.hypot(b[0] - a[0], b[1] - a[1])
  234. def _orientation(
  235. a: Tuple[float, float],
  236. b: Tuple[float, float],
  237. c: Tuple[float, float],
  238. ) -> float:
  239. return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
  240. def _on_segment(
  241. a: Tuple[float, float],
  242. b: Tuple[float, float],
  243. p: Tuple[float, float],
  244. *,
  245. eps: float = 1e-6,
  246. ) -> bool:
  247. return (
  248. min(a[0], b[0]) - eps <= p[0] <= max(a[0], b[0]) + eps
  249. and min(a[1], b[1]) - eps <= p[1] <= max(a[1], b[1]) + eps
  250. )
  251. def _segments_intersect(
  252. p1: Tuple[float, float],
  253. q1: Tuple[float, float],
  254. p2: Tuple[float, float],
  255. q2: Tuple[float, float],
  256. *,
  257. eps: float = 1e-6,
  258. ) -> bool:
  259. o1 = _orientation(p1, q1, p2)
  260. o2 = _orientation(p1, q1, q2)
  261. o3 = _orientation(p2, q2, p1)
  262. o4 = _orientation(p2, q2, q1)
  263. if (o1 > eps and o2 < -eps or o1 < -eps and o2 > eps) and (
  264. o3 > eps and o4 < -eps or o3 < -eps and o4 > eps
  265. ):
  266. return True
  267. if abs(o1) <= eps and _on_segment(p1, q1, p2, eps=eps):
  268. return True
  269. if abs(o2) <= eps and _on_segment(p1, q1, q2, eps=eps):
  270. return True
  271. if abs(o3) <= eps and _on_segment(p2, q2, p1, eps=eps):
  272. return True
  273. if abs(o4) <= eps and _on_segment(p2, q2, q1, eps=eps):
  274. return True
  275. return False
  276. def _point_to_segment_distance_m(
  277. p: Tuple[float, float],
  278. a: Tuple[float, float],
  279. b: Tuple[float, float],
  280. ) -> float:
  281. ax, ay = a
  282. bx, by = b
  283. px, py = p
  284. dx = bx - ax
  285. dy = by - ay
  286. if abs(dx) < 1e-9 and abs(dy) < 1e-9:
  287. return math.hypot(px - ax, py - ay)
  288. t = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)
  289. t = max(0.0, min(1.0, t))
  290. proj_x = ax + t * dx
  291. proj_y = ay + t * dy
  292. return math.hypot(px - proj_x, py - proj_y)
  293. def _segment_distance_m(
  294. p1: Tuple[float, float],
  295. q1: Tuple[float, float],
  296. p2: Tuple[float, float],
  297. q2: Tuple[float, float],
  298. ) -> float:
  299. if _segments_intersect(p1, q1, p2, q2):
  300. return 0.0
  301. return min(
  302. _point_to_segment_distance_m(p1, p2, q2),
  303. _point_to_segment_distance_m(q1, p2, q2),
  304. _point_to_segment_distance_m(p2, p1, q1),
  305. _point_to_segment_distance_m(q2, p1, q1),
  306. )