geo.py 1.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. from __future__ import annotations
  2. import math
  3. from typing import Iterable, Optional, Tuple
  4. def haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
  5. """Great-circle distance in meters."""
  6. r = 6371000.0
  7. phi1 = math.radians(lat1)
  8. phi2 = math.radians(lat2)
  9. d_phi = math.radians(lat2 - lat1)
  10. d_lam = math.radians(lon2 - lon1)
  11. a = (
  12. math.sin(d_phi / 2.0) ** 2
  13. + math.cos(phi1) * math.cos(phi2) * math.sin(d_lam / 2.0) ** 2
  14. )
  15. c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
  16. return r * c
  17. def clamp(n: int, lo: int, hi: int) -> int:
  18. return max(lo, min(hi, n))
  19. def nearest_point_index(
  20. points_latlon: Iterable[Tuple[float, float]],
  21. lat: float,
  22. lon: float,
  23. *,
  24. start_idx: Optional[int] = None,
  25. window: int = 80,
  26. ) -> Tuple[int, float]:
  27. """Return (idx, distance_m) of the nearest point.
  28. If start_idx is given, search a window around it for speed.
  29. """
  30. pts = list(points_latlon)
  31. if not pts:
  32. raise ValueError("empty points")
  33. if start_idx is None:
  34. search_lo = 0
  35. search_hi = len(pts) - 1
  36. else:
  37. search_lo = clamp(start_idx - window, 0, len(pts) - 1)
  38. search_hi = clamp(start_idx + window, 0, len(pts) - 1)
  39. best_i = search_lo
  40. best_d = float("inf")
  41. for i in range(search_lo, search_hi + 1):
  42. p_lat, p_lon = pts[i]
  43. d = haversine_m(lat, lon, p_lat, p_lon)
  44. if d < best_d:
  45. best_d = d
  46. best_i = i
  47. return best_i, best_d