Initial Vibe coded project

This commit is contained in:
Olaf committed 2026-10-20 14:18:35 +02:00
commit 0891cf12a5
21 files changed
+2830

No files matched your search

+284
View File
@@ -0,0 +1,284 @@
# Flight Map
A Python command-line application that **records OpenSky aircraft positions to
SQLite**, then **independently generates a pen-plotter SVG** from a saved session.
Collection never downloads maps or terrain; plotting never contacts OpenSky.
## Install
Python 3.11 or newer:
```sh
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e .
flight-map --help
```
Alternatively, use `uv venv .venv` and `uv pip install -e .`.
## 1. Collect flights
The default map rectangle is in Uithoorn, Netherlands:
| Corner | Latitude | Longitude |
| --- | --- | --- |
| Southwest | 52.232025 | 4.784498 |
| Northeast | 52.253985 | 4.863525 |
OpenSky supports OAuth client credentials. Obtain your own credentials through
OpenSky and set them in your shell, **not in source code**:
```sh
export OPENSKY_CLIENT_ID='your-client-id'
export OPENSKY_CLIENT_SECRET='your-client-secret'
flight-map collect --db flights.sqlite --duration 3600
```
An existing token can instead be supplied through `OPENSKY_ACCESS_TOKEN`.
Anonymous access is attempted when no credentials are supplied, with a warning;
availability and daily quotas depend on OpenSky's current policy. Client
credentials automatically obtain and refresh tokens; a manually provided token
does not refresh.
Collection prints the session ID immediately. Omit `--duration` to run until
Ctrl-C. Observations and progress are committed after each successful poll.
Ctrl-C preserves the observations and marks the session interrupted. Service
errors stop collection with a nonzero exit status and mark the session failed;
HTTP rate-limit responses include `Retry-After` when available.
Custom bounds are **longitude, latitude, longitude, latitude**, not latitude first:
```sh
flight-map collect --db flights.sqlite \
--bounds 4.784498 52.232025 4.863525 52.253985 \
--interval 30 --collection-margin 2000 --duration 3600
flight-map sessions --db flights.sqlite
flight-map collect --db flights.sqlite --resume SESSION_ID --duration 1800
```
Reversed corners are normalized. The requested map rectangle, expanded collection
rectangle, interval, source, session times, status, and observation associations
are saved in the database. Resume uses those saved settings; it does not change
the map area or interval. After an abrupt process termination, the last committed
checkpoint remains available even if the status still says `recording`.
Do not run two collectors against the same session simultaneously.
The default poll interval is 30 seconds, with a minimum of 10 seconds. **A valid
interval does not guarantee enough daily API credits**: select an interval and
duration appropriate to your account's current quota. No historical observations
from before collection starts are fetched.
## Purge a recording
To permanently delete one session, including its saved map bounds and all flight
observations:
```sh
flight-map sessions --db flights.sqlite
flight-map purge --db flights.sqlite --session SESSION_ID --yes
```
Stop any collector writing to that session first. The exact session ID and
`--yes` confirmation flag are required. Deletion is transactional: either both
observations and session metadata are removed, or neither is. Other sessions,
map cache entries, and previously exported SVGs are unchanged. Missing sessions
or databases produce an error; a mistyped database path does not create a file.
Deletion cannot be undone without a backup. This is logical deletion, not secure
erasure or database-file compaction.
## 2. Plot a recorded session
```sh
flight-map plot --db flights.sqlite --session SESSION_ID --output map.svg
```
The map area is read from the session, not from current application defaults.
Flight data is opened read-only. Supply a shorter recording interval if needed:
```sh
flight-map plot --db flights.sqlite --session SESSION_ID --output morning.svg \
--from 2026-10-20T08:00:00Z --to 2026-10-20T08:30:00Z
```
Timestamps require an explicit time zone; labels are UTC. Both endpoints must be
within the saved recording. Only observations within the selected interval are
used. At least two usable consecutive airborne observations are required per
track. Observations are separated into tracks at callsign changes, ground
observations, and gaps over `--max-gap` seconds (default 120); unknown missing
positions are not bridged beyond that threshold. Callsigns are not verified
passenger-facing flight numbers. Missing callsigns are labeled explicitly, with
the aircraft ICAO identifier retained.
Flight paths use smooth **centripetal Catmull-Rom interpolation**, exported as
cubic Bezier SVG paths passing through every recorded position. Two-point tracks
remain straight; duplicate positions are handled without invalid coordinates.
Smoothing never joins separate tracks or fills long recording gaps. Curves are
interpolations, not evidence of the aircraft's precise motion between samples.
The actual curves are clipped geometrically to the output rectangle, and their
in-map portions determine the time-weighted height calculation.
### Map cache and offline plotting
The first plot downloads OpenStreetMap **vector features**, not raster tiles,
using Overpass. Roads, waterways, buildings, and water outlines are rendered as
unfilled lines. Minor pedestrian/cycle paths, steps, proposed roads, and roads
under construction are omitted from the drawing. Building outlines whose shorter
dimension would be smaller than three pen widths are omitted to prevent dense
housing from turning into ink blobs; larger paper or a finer pen retains more
buildings without another download. Multipolygon relations are not assembled in this initial version;
only returned way geometry is drawn.
Downloaded map data is cached in `.flight-map-cache/`. The cache key includes
the service URL, exact map bounds, and feature query. Different time ranges,
page sizes, and flight selections for the same map reuse that data, including
when offline. Cache entries do not expire automatically:
```sh
flight-map plot --db flights.sqlite --session SESSION_ID --output map.svg \
--cache /path/to/shared-map-cache
flight-map plot --db flights.sqlite --session SESSION_ID --output updated.svg \
--cache /path/to/shared-map-cache --refresh-map
```
Refresh validates the response before atomically replacing the cached entry.
A failed refresh leaves the previous entry intact and reports an error. A damaged
entry is reported rather than silently ignored; use `--refresh-map` to replace it.
Cached responses have no embedded credentials.
For entirely offline plotting, pass a local Overpass JSON export containing
`elements` with way `geometry`, produced by an `out geom;` query:
```sh
flight-map plot --db flights.sqlite --session SESSION_ID --output offline.svg \
--map-data local-map.json
```
### Height above the map's average ground level
**Without suitable elevation inputs the map still plots, but heights are `N/A`
and the program emits warnings.** It never substitutes pressure altitude, zero
ground elevation, or sea-level altitude.
The calculation is:
```text
height = time-weighted mean aircraft altitude inside the saved map bounds
minus area-weighted mean ground elevation inside those same bounds
```
Outside-map extensions do not contribute. Heights are in metres, not terrain
clearance under the aircraft.
Supply a terrain GeoTIFF whose first band contains ground elevation in metres,
has a CRS, and covers the entire map rectangle. Its declared vertical reference
must be known. Sampling uses an equal-area 64 by 64 geographic grid and honors
nodata, raster band scale, and offset. Any missing sampled ground value makes
the mean unavailable. Choose terrain resolution appropriate to the map; the
grid is an approximation, not a survey-grade terrain integral.
For terrain already referenced to the WGS84 ellipsoid:
```sh
flight-map plot --db flights.sqlite --session SESSION_ID --output heights.svg \
--terrain terrain-ellipsoid.tif --terrain-reference wgs84-ellipsoid \
--confirm-geometric-ellipsoid
```
For orthometric terrain, also supply a geoid separation GeoTIFF in metres,
representing **N = h - H**, matching the terrain's vertical datum and covering
the track observations used for conversion:
```sh
flight-map plot --db flights.sqlite --session SESSION_ID --output heights.svg \
--terrain terrain.tif --terrain-reference orthometric \
--geoid matching-geoid.tif --confirm-geometric-ellipsoid
```
The application computes `H = h - N` for each geometric aircraft altitude.
OpenSky documents its field as geometric altitude but does not establish every
observation's datum or accuracy. The confirmation flag is a deliberate assertion
by the operator that treating these heights as WGS84 ellipsoidal is appropriate.
Do not set it without verifying your source's interpretation. A missing geometric
altitude or geoid value on a contributing segment makes that track's height
unavailable. Pressure altitude is recorded for provenance, not used for this
calculation. For Dutch terrain in NAP, use a conversion consistent with NAP;
do not assume an arbitrary global geoid matches it.
### Pen-plotter settings
Defaults: A4 landscape, 12 mm page margin, 0.3 mm pen, 1.8 mm lettering, and up to
4 mm of recorded flight extension beyond the map border.
```sh
flight-map plot --db flights.sqlite --session SESSION_ID --output large.svg \
--width 420 --height 297 --pen-width 0.3 --font-height 2 --extension 5
```
SVG dimensions are in millimetres. A local equirectangular projection preserves
the rectangular geographic bounds and uses a common scale for map and tracks;
it is intended for local maps, not large geographic extents. Geometry is
physically clipped, not hidden with masks. Groups `map`, `flights`, and
`lettering` can be selected separately. Text, including attribution, is drawn
with an original single-stroke alphabet: no installed fonts, raster images,
filled glyphs, or SVG clipping support are required.
Each track has a numeric identifier and a legend entry with UTC dates/times,
callsign, ICAO identifier, and average height. For dense recordings, select
shorter periods or larger paper. The program fails explicitly if the legend
does not fit or a track identifier cannot be placed legibly. Flight identifiers
avoid each other; intersections with map geometry or flight paths are not fully
optimized. Map lines are simplified at 0.05 mm tolerance and identical paths
deduplicated; physical pen-path travel optimization is left to plotter software.
Existing output is protected unless `--force` is supplied. Map bounds crossing
the antimeridian, widths above 180 degrees, and latitudes outside [-85, 85] are
rejected.
## Offline synthetic demo
This creates **invented** tracks, map features, terrain, and geoid data, for
checking the complete workflow without network access:
```sh
python examples/make_demo.py demo
```
Run the plotting command printed by the script. Its heights use synthetic,
known-compatible references; these inputs are not a real Uithoorn dataset.
## Development
```sh
python -m pip install -e '.[dev]'
python -m pytest
ruff check flight_map tests examples
ruff format --check flight_map tests examples
mypy flight_map
```
Tests cover recording and resume settings, persistence, deduplication, bounds,
projection, track gaps, clipping, time-weighted heights, missing terrain, vertical
references, cache reuse/refresh/failure, and repeatable offline SVG generation
without mutating the database.
## Data policies and attribution
- Map: [OpenStreetMap contributors](https://www.openstreetmap.org/copyright),
licensed under ODbL. Respect the chosen Overpass service's
[usage guidance](https://dev.overpass-api.de/overpass-doc/en/preface/commons.html);
cache and avoid unnecessary requests.
- Flights: [The OpenSky Network](https://opensky-network.org). This initial
application is intended for non-commercial use, subject to its current
[terms](https://opensky-network.org/about/terms-of-use) and
[API documentation](https://openskynetwork.github.io/opensky-api/rest.html).
Coverage and fields vary; collection does not guarantee complete tracks.
Cite Matthias Schaefer, Martin Strohmeier, Vincent Lenders, Ivan Martinovic,
and Matthias Wilhelm, "Bringing Up OpenSky: A Large-scale ADS-B Sensor Network
for Research", IPSN 2014, pp. 83-94, when publishing derived work.
- Terrain and geoid: supplied by the operator; follow their separate licenses
and attribution requirements when distributing outputs.
Map and flight attribution is included as plotter paths, with further provenance
in SVG metadata. This is an artistic recording/plotting tool, not a navigation
or aviation safety product.