Ask a service that knows which leg it is, and fetch a map worth the screen

A callsign is a flight number rather than a leg, and the free registers hold
one route per number, so an aircraft over Arizona kept being handed a hop
between two airports in Texas.  Nothing on the air settles it: ADS-B carries
no origin or destination.  A commercial schedule service does know, because
it holds the day's actual movements.

Four are wired up and all four are optional: FlightAware AeroAPI,
Flightradar24, OAG and Cirium.  Each is asked before the free databases and
each answers for the moment the aircraft was overhead rather than for the
flight number in general, so the leg chosen is the one that was in the air.
With no keys set nothing changes at all: a source with no key is skipped
rather than asked and refused, and the free databases answer as before.

Keys come from the environment and are never written to the settings file,
because a settings file is meant to be copied between machines and pasted
into a message asking for help, and an API key is not.  There is a test that
holds that line.

None of the four has been run against its live service, since each wants a
paid account.  They were written from the published response shapes and are
tested against those shapes, so each reader finds what it recognises and
returns nothing otherwise: a service that has changed since costs a route
rather than a scan.  Cirium's plain departureTime is local and carries no
offset, so the UTC field is preferred where it is there -- reading the local
one as UTC is up to half a day out, which is exactly far enough to pick the
wrong leg of the same number.  Reading now happens inside the same guard as
asking, as an answer shaped differently from the documented one is the
failure most likely to actually happen.

And the map.  The zoom is now chosen from how wide the picture is rather
than from the area alone, with half again over the width fetched and
averaged down, since a downscaled tile is sharp and an upscaled one is not.
The window fetches a little more world than it shows so panning does not
leave the ground blank, and now fetches that bigger piece at the bigger
piece's own size: rendering it into the window's own pixels and stretching
it back was a fifth of an upscale over the whole map, which is what a sharp
map looks like when it looks blurred.  The comment in the fetcher said the
opposite of what the code did, which is how it stayed hidden.

At 1920 by 1080 over a hundred miles the tiles now hold about 1.6 times the
pixels the window wants.  At 3840 by 2160 the tile budget is reached, the
zoom stops climbing and the map is enlarged after all; a smaller radius buys
the detail back, and somebody else's tile server is not a thing to fetch a
thousand tiles from for one picture.  The README says so rather than
implying otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
The Dust Council 2026-09-04 20:19:53 -07:00
parent 87b0954f0c
commit 8eb3bdbb86
20 changed files with 1758 additions and 66 deletions

102
README.md
View file

@ -1116,6 +1116,29 @@ and `A835AF` is American with no network at all), and the first three letters
of an airline callsign are its ICAO designator, so `RYR1234` is Ryanair.
`--no-lookup` stops at that.
**A callsign is a flight number, not a leg.** An airline runs the same number
over several legs in a day — Southwest especially — and a register holds one
route for it, so an aircraft crossing Arizona is quite often handed a
half-hour hop between two airports in Texas. The two databases routinely
disagree with each other about the same flight number, and both are snapshots
years old.
Nothing on the air settles it: **ADS-B carries no origin or destination.** An
aircraft broadcasts who and where it is, not where it is going. So what can be
done is checked rather than trusted:
- A route the aircraft **cannot** be flying is left off the map and out of the
window. The two ends are known, and an aircraft on a route is never much
further along it than the route is long. It is still written in the report
with a note saying so, because it is what the register holds for that flight
number and worth having — it is just not a statement about where this
aeroplane was going.
- Where a source lists a **whole day's stops** rather than a leg — hexdb
answers `KORD-KEWR-KORD` for some flight numbers — the aircraft's own
position picks the leg out. Reading the ends off that string instead gives
Chicago to Chicago, which is not a flight.
- Where no leg fits, none is claimed.
```
4008F6 BAW49
registration: G-VROS
@ -1131,6 +1154,46 @@ of an airline callsign are its ICAO designator, so `RYR1234` is Ryanair.
speed: up to 480 kt
```
### Knowing the leg for certain
That needs live schedule data, which none of the free sources carry. Four
commercial services are wired up, and all four are **optional**:
| service | keys |
| --- | --- |
| [FlightAware AeroAPI](https://www.flightaware.com/commercial/aeroapi/) | `BANDSAUNTER_AEROAPI_KEY` |
| [Flightradar24](https://fr24api.flightradar24.com/) | `BANDSAUNTER_FR24_TOKEN` |
| [OAG Flight Info](https://developer.oag.com/) | `BANDSAUNTER_OAG_KEY` |
| [Cirium (FlightStats)](https://developer.cirium.com/) | `BANDSAUNTER_CIRIUM_APP_ID` and `BANDSAUNTER_CIRIUM_APP_KEY` |
Each holds the timetable and the day's movements, so each can answer the
question the registers cannot: which leg of that flight number was in the air
at the moment the aircraft was overhead. A schedule service is asked first and
the free databases pick the question back up where it does not answer, so a
program with no keys set behaves exactly as it did before.
**Keys are read from the environment, never the settings file** — a settings
file is meant to be copied between machines and pasted into a message asking
for help, and an API key is not. There is a test that enforces it.
```sh
export BANDSAUNTER_AEROAPI_KEY=...
bandsaunter flights # ask every service with a key
bandsaunter flights --schedules flightaware # ask only that one
bandsaunter adsb --schedules oag,cirium # ask those two, in that order
```
A service with no key is skipped rather than asked and refused, since a
request is only a slow way of finding out there is no key. The callsign and
the moment are all that is sent. The same setting lives in the ADS-B options
menu under **Schedule services**.
One caveat, stated plainly: **each reader was written from its service's
published response shape and tested against that shape; none has been run
against a live service**, because each wants a paid account. So each is
written to find what it recognises and return nothing at all otherwise — a
service that has changed since costs a route, not a scan.
### The moving map
```bash
@ -1278,16 +1341,37 @@ are met rather than assumed:
- **the attribution is drawn onto the picture**, because a GIF travels
without the readme that would otherwise carry it.
A drawing is capped at a few dozen tiles — past that the zoom drops, since a
coarser map still says where the coastline is. `--no-basemap` draws the tracks
on their own, `--tiles URL` points at another server (your own, if you run
one), and when there is no network and nothing cached the picture falls back
to the plain grid it drew before.
`--no-basemap` draws the tracks on their own, `--tiles URL` points at another
server (your own, if you run one), and when there is no network and nothing
cached the picture falls back to the plain grid it drew before.
The map is drawn at the resolution it was fetched at, not stretched: tiles are
averaged down to the picture rather than point-sampled, so lettering and roads
stay whole instead of breaking up, and the window fetches enough pixels to
cover its margin at full detail rather than enlarging what it has.
**The zoom is chosen from how wide the picture is, not from the area alone.**
A map asked for at 1920 pixels fetches finer tiles than the same map asked for
at 960, and about 1.4× the width is fetched deliberately and then averaged
down — a downscaled tile is sharp and an upscaled one is not, so it is better
to fetch too much and shrink it than to fetch too little and stretch it.
The window fetches a little more world than it shows, so that panning does not
leave the ground blank, and it fetches that bigger piece **at the bigger
piece's own size**: what the window then shows comes out pixel for pixel with
the screen. Rendering the wider piece into the window's own pixels and
stretching it back is an upscale of a fifth applied to the whole map, which is
what a sharp map looks like when it looks blurred.
Measured, at a 100-mile radius:
```
animation at 960 px zoom 9 28 tiles 1792 px 1.9x oversampled
animation at 1920 px zoom 10 91 tiles 3328 px 1.7x oversampled
window at 1920x1080 zoom 10 135 tiles 3840 px 1.6x oversampled
window at 3840x2160 zoom 10 135 tiles 3840 px 0.8x — upscaled
```
A drawing is still capped, at a couple of hundred tiles, and the last line is
what that cap looks like: at 4K the zoom has already stopped climbing and the
map is enlarged after all. Somebody else's tile server is not a thing to fetch
a thousand tiles from for one picture. A smaller `--radius` buys the detail
back, since the same budget then covers less ground.
`--map-brightness PERCENT` (70 by default) is how far up its range the map is
drawn. The ground has to stay dark enough that the aircraft are the brightest