Files
FlightPlot/flight_map/tracks.py
T
2026-10-20 14:18:35 +02:00

86 lines
3.1 KiB
Python

import math
from dataclasses import dataclass
from itertools import groupby, pairwise
from flight_map.curves import Cubic, interpolate
from flight_map.geo import Projection
from flight_map.storage import Observation
from flight_map.terrain import Elevation
@dataclass
class Track:
observations: list[Observation]
curves: list[Cubic]
average_height: float | None
start: float
end: float
def split_tracks(observations: list[Observation], max_gap: float) -> list[list[Observation]]:
if not math.isfinite(max_gap) or max_gap <= 0:
raise ValueError("Maximum track gap must be positive.")
result = []
for _, aircraft in groupby(
sorted(observations, key=lambda o: (o.icao24, o.timestamp)), key=lambda o: o.icao24
):
current: list[Observation] = []
for item in aircraft:
if item.on_ground:
if len(current) >= 2:
result.append(current)
current = []
continue
if current and (
item.timestamp - current[-1].timestamp > max_gap
or item.timestamp <= current[-1].timestamp
or item.callsign != current[-1].callsign
):
if len(current) >= 2:
result.append(current)
current = []
current.append(item)
if len(current) >= 2:
result.append(current)
return result
def make_tracks(
observations: list[Observation],
projection: Projection,
elevation: Elevation,
ground: float | None,
max_gap: float,
) -> list[Track]:
result = []
for items in split_tracks(observations, max_gap):
points = [projection.point(o.longitude, o.latitude) for o in items]
curves = interpolate(points)
if all(curve.start == curve.end for curve in curves):
continue
total_time = 0.0
altitude_integral = 0.0
missing_height = ground is None
start, end = math.inf, -math.inf
for index, (first, last) in enumerate(pairwise(items)):
duration = last.timestamp - first.timestamp
h0, h1 = elevation.aircraft_altitude(first), elevation.aircraft_altitude(last)
for f0, f1 in curves[index].inside_intervals(projection.rectangle.bounds):
inside_duration = duration * (f1 - f0)
total_time += inside_duration
start = min(start, first.timestamp + f0 * duration)
end = max(end, first.timestamp + f1 * duration)
if h0 is None or h1 is None:
missing_height = True
else:
entry = h0 + (h1 - h0) * f0
exit_height = h0 + (h1 - h0) * f1
altitude_integral += (entry + exit_height) / 2 * inside_duration
if total_time == 0:
continue
average = None
if not missing_height and ground is not None:
average = altitude_integral / total_time - ground
result.append(Track(items, curves, average, start, end))
return sorted(result, key=lambda track: (track.start, track.observations[0].icao24))