1
0

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>
This commit is contained in:
2026-08-06 20:57:14 -07:00
parent 0cc2126e85
commit a229fb2721
23 changed files with 2062 additions and 12 deletions
+89
View File
@@ -181,6 +181,95 @@ export function syntheticRoutes(region: SkyRegion, count = 6, seed = 20_617): Si
return routes;
}
/**
* A source with a dial on it: whatever it was going to draw, plus N invented
* aircraft.
*
* This exists for one control in the godmode panel — "how busy would this look
* with three times the traffic" — and the shape it takes is chosen to make that
* question answerable without corrupting the answer to any other one.
*
* **It composes rather than substitutes.** The base source is polled unchanged
* and its aircraft are passed through untouched; the fabricated ones are a
* second list concatenated onto the end. That is what lets the dial work over a
* *live* ADS-B feed as well as over the simulator — the real traffic stays real
* and stays complete, and turning the dial back to zero returns exactly the
* sky that was there before, because nothing was ever taken away.
*
* The alternative was to mutate the simulator's route list, and it is worse in
* both directions: it does nothing at all when the server is serving its own
* plan (`HttpFlights` ignores its fallback in that mode, so the slider would be
* inert on every deployment that has an API), and it is destructive when it does
* work, because the authored corridors would have to be rebuilt to get back.
*
* ### On fabricating traffic at all
*
* The same argument as `weatherOverride` in `main.ts`: a god-only lie about the
* inputs, told to see what the renderer does with it. It is deliberately **not**
* available to anyone else, and the invented aircraft carry a callsign prefix of
* their own so that a screenshot of a busy sky can be told from a screenshot of
* a real one. Note what this breaks while it is on — every viewer agreeing about
* where the aircraft are, which is the property the server's plan exists to buy.
* That is acceptable for a debug dial and would not be for a feature.
*/
export interface TrafficDial {
/** The source to hand `createScene`. Stable for the dial's whole life. */
source: FlightSource;
/** Fabricate this many additional aircraft. `0` turns the dial off entirely. */
setExtra(count: number): void;
extra(): number;
}
/**
* Callsign prefix for fabricated traffic.
*
* Distinct from `syntheticRoutes`'s own `SIM`, and it has to be: `sampleRoute`
* derives an aircraft's id from its callsign, `createFlightLayer` keys its
* tracks on that id, and a deployment with no API is already flying `SIM 1`
* through `SIM 6` from the fallback. Reuse the prefix and every fabricated
* aircraft would land on an existing track, teleporting it across the board on
* alternate polls.
*/
const FABRICATED_PREFIX = "GOD";
/** As many as the dial goes to. Past this the sky is soup and the point is made. */
export const MAX_EXTRA_TRAFFIC = 400;
export function withTrafficDial(base: FlightSource, region: SkyRegion): TrafficDial {
let extra: SimulatedFlights | null = null;
let count = 0;
return {
source: {
interval: base.interval,
poll(): Aircraft[] | Promise<Aircraft[]> {
const theirs = base.poll();
if (extra === null) return theirs;
const mine = extra.poll();
// `poll` is synchronous on every source in this build, but the interface
// permits a promise and `HttpFlights` documents its synchrony as a
// deliberate property rather than an accident. Handling both here costs
// one branch and means the dial cannot be what breaks that.
return theirs instanceof Promise ? theirs.then((a) => [...a, ...mine]) : [...theirs, ...mine];
},
dispose: () => base.dispose?.(),
},
setExtra(next: number) {
count = Math.max(0, Math.min(MAX_EXTRA_TRAFFIC, Math.round(next)));
if (count === 0) {
extra = null;
return;
}
const routes = syntheticRoutes(region, count).map((route, i) => ({
...route,
callsign: `${FABRICATED_PREFIX} ${i + 1}`,
}));
extra = new SimulatedFlights(routes);
},
extra: () => count,
};
}
/**
* Traffic that behaves like the real thing without being it: aircraft move
* along fixed legs at fixed speeds, looping, with each one offset in phase so