1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/adapters
karti a229fb2721 The sky gets the things above the aeroplanes
Satellites, end to end: CelesTrak element sets behind the same TTL cache
the weather and the flights use, served as TLEs rather than as positions,
and propagated in the browser with SGP4.

Sending elements is the same trick `flights/plan.ts` plays and it has a
better excuse here — a TLE *is* the closed form, valid for days either
side of its epoch, so one cacheable fetch every six hours replaces a poll
and every viewer agrees about where everything is.

Two things are worth knowing about the shape of it:

  - There is no region parameter. An aeroplane at 10,000 m is local and
    a satellite at 550 km is above the horizon for a circle two thousand
    kilometres across, so one catalogue serves both boards and the client
    decides what is above its own horizon. Only the observer is per-city,
    which is why `main.ts` shares the elements and rebuilds the catalogue.
  - The layer draws on a dome, because it cannot draw anywhere else.
    `world.metres(550_000)` is 21,000 scene units against a far plane at
    3,000. Azimuth and elevation are real; the radius carries nothing.

Off by default: a clone that started pulling CelesTrak on `npm run dev`
would have volunteered somebody else's bandwidth for its onboarding.

Godmode gets the two dials that point at the sky rather than at the
light — fabricated traffic, which composes with a live ADS-B feed instead
of replacing it, and a switch for the satellite layer with a count beside
it. Both are god-only lies about the inputs, in the manner of the weather
override.

`satellite.js` is the second runtime dependency this package has taken.
Its entry point star-exports an Emscripten build that cannot be shaken
out, so `noWasmPropagator` in the Vite config cuts it: 308 kB of WASM
loader for a bulk propagator nothing calls, against 26 kB for the SGP4
that does the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 20:57:14 -07:00
..

Adapters

Everything in src/engine/ renders data and takes no position on what it means. A Marker is a point with a colorKey; the engine looks that key up in a palette the caller supplies and will never learn that rejected is red. A FlightSource is an interface with a poll() on it. A WeatherObservation is six numbers.

This directory is where those become somebody's actual data. It is the only part of the browser build that knows an API exists, and keeping it here is what lets one renderer serve a private career map, a public sector map and whatever anyone else builds without any of them being a fork. See ARCHITECTURE.md §3.3.

file what it is
sample.ts fabricated demo markers and flight routes, so a fresh clone has something on it
http.ts the real adapter — the Tera API described in src/server/wire.ts, falling back to sample.ts

The fallback is the product, not the safety net

http.ts never throws and never leaves the map empty. No server, a 404, a timeout, a static host answering /api/v1/markers with its own index.html — all of it lands on the sample data, and the city keeps rendering.

That is deliberate and it is the acceptance test the whole repo is held to: a stranger clones this, runs one command, and gets a city, with no account, no key and no network (CONTRACT.md §0). npm run build deployed to any static host is a working Tera. Pointing it at a server is an upgrade.

Every response carries live: boolean so the difference is visible to the app above. An interface that shows invented companies exactly the way it shows real ones is the one failure mode this arrangement can have, and the flag is there so it does not have to happen.

const tera = createTeraClient();               // same-origin /api/v1
const markers = await tera.markers();
const scene = await createScene(stage, {
  city: SAN_FRANCISCO,
  markerPalette: markers.palette,
  flights: tera.flights(regionOf(SAN_FRANCISCO), sampleRoutesFor("sf")),
});
scene.setMarkers(markers.value);
if (!markers.live) showSampleDataNotice();

Three things about that call are load-bearing.

createScene is async and takes a Stage rather than a canvas: the heightfield is built in a Worker, and the renderer outlives any one city, so the stage is created once for the page and handed to each scene in turn.

flights takes a region, not a client-wide origin. The Bay Area and SoCal are six hundred kilometres apart and a single configured origin served one of them and lied to the other. regionOf(city) derives it from the city's own bounds, so a city pack added later needs no configuration to get its own sky. The second argument is the simulated traffic to fly when the API has none, which is what keeps the zero-config case from showing an empty sky.

And note the ordering: markerPalette is fixed when the scene is built, so the markers have to be awaited first. markers.palette is the sample palette when the feed is the sample set and the palette you passed in TeraApiOptions when it is real — the sample keys are not your keys.

No real company data ships in this repo

Two separate constraints want the same thing here, which is the reason this arrangement is worth the indirection rather than just committing a JSON file.

Privacy. Pipeline status — who is talking to whom, and who said no — is private. A public repo is the wrong place for it.

Licence, which is the sharper one. Real positions are geocoded, and a geocoder built on OpenStreetMap returns ODbL data. ODbL is share-alike, and serving a snapshot of those coordinates from a public endpoint is Publicly Using a Derivative Database — §4.3 attribution and §4.4 share-alike attach to the served data whether or not the rows live in the repo. Keeping the table off-disk hides that obligation; it does not discharge it. See ARCHITECTURE.md §3.2 and the correction in CONTRACT.md §8.

So the rule is about the geocoder, not about storage:

  • Geography in src/cities/ is traced by hand. Original expression, ours, Apache-2.0. Never OSM, never Nominatim.
  • Real markers arrive over the API at runtime, each carrying a CoordinateProvenance, and the server refuses any row whose provenance is not on a non-ODbL allowlist — us-census, hand-placed, synthetic.
  • http.ts passes refused straight through rather than swallowing it, because a gate that drops rows silently is indistinguishable from an empty database. It does not re-run the gate in the browser: the allowlist is the server's, and a self-hoster who added their own provenance value should not watch their own rows vanish client-side.
  • Everything in sample.ts is invented. The companies do not exist, the positions were typed by hand from a general sense of where San Francisco's neighbourhoods are, and the names are absurd on purpose so that none of them can be mistaken for a real business.

Writing your own

http.ts is one adapter, not the adapter. Anything that can produce Marker[], a WeatherObservation and a FlightSource is one — a local JSON file, a Postgres query through your own backend, an SDR on the windowsill. The engine imports nothing from this directory, so an adapter can be deleted, forked or replaced without touching a line of the renderer.

Two things to keep if you write one:

Traffic must not block the render loop. poll() may be synchronous, and HttpFlights is: it answers from whatever is in hand and refreshes on the body's own TTL in the background. A poll() that awaits a slow fetch puts a frame behind a round trip.

FlightRadar24 is not an option. Their terms forbid scraping and forbid redistributing the data, so an Apache-2.0 repo containing an FR24 client would be publishing instructions for breaking a ToS and shipping data it has no right to relicense. src/engine/flights.ts ships a simulator and points at the open community ADS-B feeds instead; anything commercial belongs in an adapter in a private deployment. See ARCHITECTURE.md §4.