83 lines
3.0 KiB
Python
83 lines
3.0 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
|
|
|
|
|
|
@dataclass
|
|
class Track:
|
|
observations: list[Observation]
|
|
curves: list[Cubic]
|
|
average_altitude: 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,
|
|
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_altitude = False
|
|
start, end = math.inf, -math.inf
|
|
for index, (first, last) in enumerate(pairwise(items)):
|
|
duration = last.timestamp - first.timestamp
|
|
h0, h1 = first.baro_altitude, last.baro_altitude
|
|
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_altitude = 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_altitude:
|
|
average = altitude_integral / total_time
|
|
result.append(Track(items, curves, average, start, end))
|
|
return sorted(result, key=lambda track: (track.start, track.observations[0].icao24))
|