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))