Take-off airfield on Android
Summary
Section titled “Summary”Take-off place and time zone is partial on
Android. The time zone lookup is ported. The take-off airfield is not, because Android has no airports
table (airports were out of scope for v1). Android has the columns and syncs them, but never fills
them. Only flights that came from iOS show an airfield.
The result today:
- A flight imported on Android shows no take-off airfield or country in the detail.
- Search by airfield name or code finds nothing for those flights.
- The logbook’s countries filter is built but stays hidden, because no flight has a country.
- The Charron scoreboard shows
EBZWinstead of “Genk 🇧🇪” (CharronAirports.NONE).
This analysis ports the airports table and the take-off lookup only. The airport screens
(nearest airports, airport detail, frequencies) stay out
(Nearest airports remains not-planned).
Behaviour
Section titled “Behaviour”The rule is in Take-off airfield. In short: once per flight,
at its first save to the logbook, the nearest airfield to the take-off fix, if it is 3 km away or
closer, gives takeoffCode, takeoffName and takeoffCountryCode, and isTakeoffProcessed
becomes true.
What the pilot sees afterwards, all of it already built on Android and only waiting for data:
| Place | Shows |
|---|---|
| Flight detail | Take-off code and location (name + flag). Android shows the name without the flag today (see the Android table). |
| Logbook search | Matches the code and the name |
| Logbook filter | The Countries section, one row per take-off country |
| Charron scoreboard | Airfield name and flag in the start column |
Existing flights
Section titled “Existing flights”Flights imported on Android before this change keep no airfield (decision 3). Only flights imported from now on get one, as on iOS, where the lookup only runs when a flight is created.
So the lookup runs on a flight’s first analysis only (isFirstAnalysis, analysisVersion == 0),
not whenever isTakeoffProcessed is false. Otherwise a later re-analysis (an ANALYSIS_VERSION
bump, or a forced one) would fill old flights after all. Old flights keep
isTakeoffProcessed = false; nothing marks them.
Edge cases
Section titled “Edge cases”| Case | Result |
|---|---|
| No take-off detected (a ground track, a broken file) | No airfield; processed. |
| Out-landing field, hill site, ridge launch | Usually no airfield within 3 km; processed. |
| Two airfields near the same spot | The nearest wins. On a tie, the first in the file wins. |
| Flight synced from iOS | Already processed there; Android doesn’t touch it. |
| Pilot later edits the flight | The lookup never runs again. |
| Flight imported on Android before this change | Stays without an airfield, also after a re-analysis. |
- Airports table: copy
vulture-ios/Packages/Airports/Sources/Airports/Resources/airports.jsonbyte for byte, as was done fortimezones.json. It adds about 1.3 MB to the APK (compressed). Load it lazily, off the main thread, once per process. Decode only the fields used here (a–e); frequencies and elevation can be skipped. - Database: no change.
FlightEntityalready hastakeoffCode,takeoffName,takeoffCountryCode,isTakeoffProcessedand an index ontakeoffCountryCode. - Sync: no change to the sync schema. Filling the fields is a local change, so the row is marked dirty and goes up like any other edit.
None.
Strings
Section titled “Strings”None new. Country names and flags come from the platform (Android already has flag() and
countryName() in :feature:serviceflights).
Decisions
Section titled “Decisions”| # | Question | Decision | Date |
|---|---|---|---|
| 1 | How much of the airports feature? | The table and the take-off lookup (plus the Charron code lookup it unlocks). No airport screens. | 2026-10-20 |
| 2 | Which data? | iOS’s airports.json, byte for byte, refreshed only from vulture-ios. | 2026-10-20 |
| 3 | Existing Android flights? | Not filled: only flights imported from now on get an airfield. No backfill, not on re-analysis either. | 2026-10-20 |
| 4 | Charron scoreboard? | Same as iOS: the start code is looked up in the same table and shows name and flag; ZZZZ and unknown codes stay as sent. Part of this work. | 2026-10-20 |
Open questions
Section titled “Open questions”None.
Platforms
Section titled “Platforms”Nothing to build. Read:
| What | Path (vulture-ios) |
|---|---|
| Table loading, nearest, 3 km limit, code lookup | Packages/Airports/Sources/Airports/AirportManager.swift |
| JSON keys | Packages/Airports/Sources/Airports/Models/Airport.swift |
| Once-per-flight lookup | Packages/Persistence/Sources/Persistence/Extensions/CoreData/CoreFlight+CoreData.swift (processTakeoff) |
| Countries filter | Modules/App/Sources/Shell/Sidebar/List/Filter/ListFilterItem.swift, ListConfiguration+Data.swift |
| Charron start column | Packages/Charron/Sources/Charron/Models/CharronFlight.swift |
| Data script | Scripts/prepare_airports.rb |
Android
Section titled “Android”| Area | Existing code (vulture-android) | Change |
|---|---|---|
| Table and lookup | domain/flight/.../timezone/TimeZoneLocator.kt is the pattern (bundled JSON, lazy decode); the distance is core.geo.distance, see the caveat below | A JVM module (for example domain/airports) with airports.json in its resources, nearest(coordinate) with the 3 km limit and byCode(icao). Unit tests against iOS results for a set of coordinates (at least EBZW → “Genk”/BE, an out-landing, a tie, 2,999 m and 3,001 m). |
| Lookup at save | data/db/.../FlightRows.kt (withAnalysis, its KDoc says “a later phase ports” it), FlightRepository.kt | On the first analysis only (isFirstAnalysis and !isTakeoffProcessed), look up flight.takeoffFix and set the three fields and the flag, in the same transaction (decision 3). |
| Charron | data/charron/.../CharronAirports.kt (NONE), di/CharronModule | Bind an implementation over the table (decision 4). |
| Detail | feature/flightdetail/.../FlightDetailViewModel.kt (toRecord: takeoffLocation = takeoffName) | Add the flag, as iOS CoreFlight.takeoffLocation (takeoffName + flag, space-joined). Reuse the flag helper from :feature:serviceflights (move it to a shared module). |
| Filter | feature/flightlist/.../filter/ | Nothing: it shows the Countries section once there are countries. Update the comments that say it waits for airports. |
| Docs | docs/analysis/00-roadmap.md, decisions §7 | Note that the airports table is ported, the screens are not. |
| Changelog | CHANGELOG.md | “Added: the take-off airfield and country of flights imported on this phone, and the countries filter in the Library.” |
Android caveats:
- Don’t use
android.location.Locationdistances (CLAUDE.md). Unlike the time zone lookup, iOS’s airport lookup measures with theMappackage’s haversine (R = 6,371 km), notCLLocation.distance, so Android uses its portcore.geo.distance, notellipsoidalDistance(found while building, 2026-10-20; Take-off airfield corrected). - Scanning 48,000 airfields per flight is fine for an import. Load the table once and reuse it (a multi-file import looks up many flights in a row).
No changes.
- vulture-android: airports table module with the lookup and its tests.
- vulture-android: lookup on the first analysis in
withAnalysis, marked dirty for sync; a test that a re-analysis of an old flight leaves it empty. - vulture-android: Charron
CharronAirportsbinding (decision 4). - vulture-android: flag after the take-off name in the flight detail.
- vulture-android: roadmap, decisions note, comments, changelog.
- vulture-docs: when built, set Android
doneon Take-off place and time zone andstatus: shippedhere.
