"""Great-circle geometry helpers for projecting estimated flight paths.""" from __future__ import annotations import math EARTH_RADIUS_KM = 6371.0088 KNOTS_TO_KMH = 1.852 def destination_point(lat: float, lon: float, bearing_deg: float, distance_km: float): """Great-circle destination given a start point, bearing and distance.""" ang = distance_km / EARTH_RADIUS_KM brg = math.radians(bearing_deg) lat1 = math.radians(lat) lon1 = math.radians(lon) lat2 = math.asin( math.sin(lat1) * math.cos(ang) + math.cos(lat1) * math.sin(ang) * math.cos(brg) ) lon2 = lon1 + math.atan2( math.sin(brg) * math.sin(ang) * math.cos(lat1), math.cos(ang) - math.sin(lat1) * math.sin(lat2), ) return math.degrees(lat2), (math.degrees(lon2) + 540) % 360 - 180 def haversine_km(lat1, lon1, lat2, lon2): p1, p2 = math.radians(lat1), math.radians(lat2) dphi = math.radians(lat2 - lat1) dlmb = math.radians(lon2 - lon1) a = math.sin(dphi / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dlmb / 2) ** 2 return 2 * EARTH_RADIUS_KM * math.asin(math.sqrt(a)) def initial_bearing(lat1, lon1, lat2, lon2): p1, p2 = math.radians(lat1), math.radians(lat2) dl = math.radians(lon2 - lon1) y = math.sin(dl) * math.cos(p2) x = math.cos(p1) * math.sin(p2) - math.sin(p1) * math.cos(p2) * math.cos(dl) return (math.degrees(math.atan2(y, x)) + 360) % 360 def estimate_endpoint(lat, lon, track_deg, gspeed_knots, seconds_remaining, dest_lat=None, dest_lon=None): """Return (end_lat, end_lon, distance_km, used_dest). If we know the destination airport coords, the arc goes there. Otherwise we dead-reckon forward along the current track for the remaining ETA. """ if dest_lat is not None and dest_lon is not None: d = haversine_km(lat, lon, dest_lat, dest_lon) return dest_lat, dest_lon, d, True if not gspeed_knots or not seconds_remaining or seconds_remaining <= 0: # Fall back to a short forward stub so the arc is still visible. seconds_remaining = 1800 gspeed_knots = gspeed_knots or 450 dist_km = gspeed_knots * KNOTS_TO_KMH * (seconds_remaining / 3600.0) elat, elon = destination_point(lat, lon, track_deg or 0, dist_km) return elat, elon, dist_km, False