Add Live Flights tab: real-time aircraft map with filters + tap detail

New top-level TabView (RootView) splits the app into:
  Tab 1 (Search): existing RoutePlannerView home
  Tab 2 (Live):   live flight tracker

Live tab features:
- MapKit map showing every aircraft in the visible viewport, rotated
  to true heading. Color-coded by vertical state: climbing/level/
  descending/on-ground.
- Auto-refresh every 15s + on map pan/zoom (debounced); manual
  refresh button. Rate-limit aware (60s backoff on HTTP 429).
- Tap any aircraft → modal sheet with live state grid (altitude,
  speed, heading, vertical rate, squawk, last-contact), current
  route (lazily fetched per-aircraft from OpenSky's /flights/
  aircraft endpoint, mapped from ICAO to IATA airport codes), and
  recent flight history (up to 8 prior legs).
- Filters: airline (multi-select from currently visible callsigns,
  with counts), aircraft type (ADS-B emitter category), airborne-
  only toggle. All filters render as horizontal chips and clear
  with a single tap.
- Search bar: callsign/flight number — submitting centers the map
  on the match and opens its detail sheet.

Data source: OpenSky Network REST API. Free, anonymous (~100 req/
day cap), JSON. Same ADS-B data FR24 starts with — without satellite
ADS-B coverage but more than enough for the in-flight tracker use
case. Reviewed FR24's APK and confirmed they migrated their live
feed to gRPC+protobuf with anti-bot device-id headers; OpenSky's
plain JSON is the right tradeoff for our build.

Implementation:
- LiveAircraft model: decodes OpenSky's mixed-type position arrays
  into a typed struct; computed properties for ft/knots/heading and
  airline ICAO extracted from callsign.
- OpenSkyClient: actor with /states/all + /flights/aircraft. Bbox
  query, throttle-aware errors.
- AircraftRegistry: ~80 ICAO → (IATA, name) entries for the major
  carriers; everything else falls through to the raw ICAO code.
- LiveFlightsView: the main map + filter UI.
- LiveFlightDetailSheet: tap modal with live state + route history.
- RootView: TabView wrapping RoutePlannerView (Search) and the new
  LiveFlightsView (Live).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-05-27 06:08:58 -05:00
parent 92a69cf16c
commit 888943deb4
8 changed files with 1296 additions and 3 deletions
+24
View File
@@ -46,6 +46,12 @@
RE7700007777000077770001 /* ConnectionLoadDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = RE7700007777000077770002 /* ConnectionLoadDetailView.swift */; };
RE8800008888000088880001 /* SearchRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = RE8800008888000088880002 /* SearchRoute.swift */; };
T1000000000000000000001A /* AirlineLoadIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = T1000000000000000000001B /* AirlineLoadIntegrationTests.swift */; };
LV1100001111000011110001 /* LiveAircraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = LV1100001111000011110002 /* LiveAircraft.swift */; };
LV2200002222000022220001 /* OpenSkyClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = LV2200002222000022220002 /* OpenSkyClient.swift */; };
LV3300003333000033330001 /* AircraftRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = LV3300003333000033330002 /* AircraftRegistry.swift */; };
LV4400004444000044440001 /* LiveFlightsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = LV4400004444000044440002 /* LiveFlightsView.swift */; };
LV5500005555000055550001 /* LiveFlightDetailSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = LV5500005555000055550002 /* LiveFlightDetailSheet.swift */; };
LV6600006666000066660001 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = LV6600006666000066660002 /* RootView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -100,6 +106,12 @@
RE8800008888000088880002 /* SearchRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRoute.swift; sourceTree = "<group>"; };
T1000000000000000000001B /* AirlineLoadIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirlineLoadIntegrationTests.swift; sourceTree = "<group>"; };
T1000000000000000000003A /* FlightsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FlightsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
LV1100001111000011110002 /* LiveAircraft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveAircraft.swift; sourceTree = "<group>"; };
LV2200002222000022220002 /* OpenSkyClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSkyClient.swift; sourceTree = "<group>"; };
LV3300003333000033330002 /* AircraftRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AircraftRegistry.swift; sourceTree = "<group>"; };
LV4400004444000044440002 /* LiveFlightsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveFlightsView.swift; sourceTree = "<group>"; };
LV5500005555000055550002 /* LiveFlightDetailSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveFlightDetailSheet.swift; sourceTree = "<group>"; };
LV6600006666000066660002 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -133,6 +145,9 @@
BB1100001111000011110006 /* FlightLoadDetailView.swift */,
RE3300003333000033330002 /* RoutePlannerView.swift */,
RE7700007777000077770002 /* ConnectionLoadDetailView.swift */,
LV4400004444000044440002 /* LiveFlightsView.swift */,
LV5500005555000055550002 /* LiveFlightDetailSheet.swift */,
LV6600006666000066660002 /* RootView.swift */,
AA5555555555555555555555 /* Styles */,
AA6666666666666666666666 /* Components */,
);
@@ -208,6 +223,8 @@
BB1100001111000011110004 /* AirlineLoadService.swift */,
BB2200002222000022220002 /* JSXWebViewFetcher.swift */,
RE2200002222000022220002 /* RouteExplorerClient.swift */,
LV2200002222000022220002 /* OpenSkyClient.swift */,
LV3300003333000033330002 /* AircraftRegistry.swift */,
);
path = Services;
sourceTree = "<group>";
@@ -235,6 +252,7 @@
BB1100001111000011110002 /* FlightLoad.swift */,
RE1100001111000011110002 /* RouteExplorerModels.swift */,
RE8800008888000088880002 /* SearchRoute.swift */,
LV1100001111000011110002 /* LiveAircraft.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -366,6 +384,12 @@
RE6600006666000066660001 /* ConnectionRow.swift in Sources */,
RE7700007777000077770001 /* ConnectionLoadDetailView.swift in Sources */,
RE8800008888000088880001 /* SearchRoute.swift in Sources */,
LV1100001111000011110001 /* LiveAircraft.swift in Sources */,
LV2200002222000022220001 /* OpenSkyClient.swift in Sources */,
LV3300003333000033330001 /* AircraftRegistry.swift in Sources */,
LV4400004444000044440001 /* LiveFlightsView.swift in Sources */,
LV5500005555000055550001 /* LiveFlightDetailSheet.swift in Sources */,
LV6600006666000066660001 /* RootView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};