1
0

SoCal, the whole bay, a moon, and gates that actually run

Six agents in parallel, and the two city packs independently reported the same
blocker: `focusRegions` and `coarseFactor` existed on the `City` type and
nothing implemented them. Uniform lattices would have been 2.9M points for
Southern California and 3.7M for the expanded bay. Both packs were unloadable
as written.

`buildAxis` is the answer, and it is honest about its limits: refinement is per
axis, not per rectangle, so a focus region sharpens its whole row *and* its
whole column. Two regions at opposite corners refine nearly everything between
them. Measured, not guessed — the bay went 0.53M points with one region and
1.64M with three, for detail nobody is looking at from a board this wide. One
region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s.

Then three things that were only ever right because San Francisco was the only
city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a
230-unit board; the bay is 1003 units across and the camera physically could
not retreat far enough to frame it. Fog distances were scene units pinned to
the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest
weather, which over ninety-four kilometres of bay correctly hides three
quarters of it — the night view was a black rectangle for a completely
reasonable reason. All three now derive from the board.

The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a
physical ratio of one to four hundred thousand. What is being reproduced is
what a moonlit night looks like on a screen in a lit room.

The CI gate caught itself, which is the part worth keeping. Port 8431 was
already held by a server from an earlier session, so the boot check polled a
healthy stranger while the process it started died on EADDRINUSE. It now
refuses to run rather than pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Karti Tripathi
2026-08-05 03:13:32 -07:00
parent 8bcb391455
commit 44c5a79424
25 changed files with 9246 additions and 220 deletions
+105
View File
@@ -0,0 +1,105 @@
# 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.
```ts
const tera = createTeraClient(); // same-origin /api/v1
const markers = await tera.markers();
const scene = createScene(canvas, {
city: SAN_FRANCISCO,
markerPalette: markers.palette,
flights: tera.flights(),
});
scene.setMarkers(markers.value);
if (!markers.live) showSampleDataNotice();
```
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.
+333
View File
@@ -0,0 +1,333 @@
/**
* The real adapter: markers, weather and traffic from the Tera API, with the
* bundled sample data underneath it.
*
* This is the one place in the browser build that knows the API exists. The
* engine takes `Marker[]`, a `WeatherObservation` and a `FlightSource` and has
* no idea where any of them came from — that boundary is what lets one renderer
* serve a private career map and a public sector map without either being a
* fork (ARCHITECTURE.md §3.3), and it is why this file is an adapter rather than
* a client scattered through the scene.
*
* **Every call degrades instead of failing.** No server, a 404, a static host
* answering `/api/v1/markers` with its own index.html, a network that has gone
* away mid-session: all of it lands on the sample data in `sample.ts` and the
* synthetic clear day below, and the map keeps rendering. That is not defensive
* habit, 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). A `npm run build` deployed to any static host is a
* working Tera; pointing it at a server is an upgrade, not a requirement.
*
* The wire types live in `src/server/wire.ts` and are types only, so importing
* them costs the bundle nothing.
*/
import type { WeatherObservation } from "../engine/atmosphere.ts";
import { sampleRoute, SimulatedFlights, type SimRoute } from "../engine/flights.ts";
import type { Aircraft, FlightSource, Marker, MarkerPalette } from "../engine/types.ts";
import { seededRandom } from "../engine/world.ts";
import type {
FlightsBody,
FlightsPlanBody,
HealthBody,
MarkersBody,
OfficeDoc,
WeatherBody,
} from "../server/wire.ts";
import { SAMPLE_MARKERS, SAMPLE_PALETTE, SAMPLE_ROUTES } from "./sample.ts";
/** Where the API lives, per CONTRACT.md §5. Same-origin, behind the site's own proxy. */
const DEFAULT_BASE = "/api/v1";
/** How long any one request may take before the fallback is used instead. */
const DEFAULT_TIMEOUT_MS = 4000;
/** How long to wait before trying the flights endpoint again after it fails. */
const RETRY_SECONDS = 30;
export interface TeraApiOptions {
/**
* Base URL, with no trailing slash. Absolute is allowed and is what a
* self-hoster running the browser build and the API on different origins
* wants; the default assumes they are the same origin.
*/
base?: string;
/** Injected for tests. Absent means `globalThis.fetch`. */
fetch?: typeof fetch;
timeoutMs?: number;
/**
* The palette live markers are coloured by.
*
* The engine looks `colorKey` up and the wire does not carry colours, so
* somebody has to supply this and it cannot be the server: what a key *means*
* is the consuming app's business. When the API is absent this is ignored and
* `SAMPLE_PALETTE` is used instead, because sample keys are not the caller's
* keys.
*/
palette?: MarkerPalette;
}
/**
* A response, plus whether it is real.
*
* `live` is the field that stops a fallback from being a lie. A demo showing
* invented companies and a deployment showing real ones must not look identical
* to the code above them — the caller is expected to say so in the interface,
* and cannot if the adapter quietly papers over the difference.
*/
export interface Feed<T> {
value: T;
live: boolean;
}
export interface MarkerFeed extends Feed<Marker[]> {
/** The palette these markers are meant to be read with. */
palette: MarkerPalette;
/** ISO-8601 snapshot time, or `null` for the sample set, which has no date. */
generatedAt: string | null;
/**
* Rows the server's public-shape gate refused, by reason and count.
*
* Passed through rather than swallowed. A gate that drops rows silently is
* indistinguishable from an empty database, which is exactly the confusion
* `MarkersBody.refused` exists to prevent — and it is the visible end of the
* provenance rule in CONTRACT.md §8.
*/
refused: { reason: string; count: number }[];
attribution: string[];
}
export interface WeatherFeed extends Feed<WeatherObservation> {
attribution: string[];
}
export interface TeraClient {
/** What the deployment turned out to be, or `null` if there is no server. */
health(): Promise<HealthBody | null>;
markers(): Promise<MarkerFeed>;
weather(): Promise<WeatherFeed>;
/**
* The traffic source, built once. It fetches on its own schedule and never
* blocks the render loop; see `HttpFlights`.
*/
flights(): FlightSource;
/**
* One office pack. `null` for anything the server will not serve — including
* a private one, which answers 404 rather than 403 so the endpoint cannot be
* used to enumerate what exists (CONTRACT.md §6).
*
* There is deliberately no fallback here. A missing marker can be stood in for
* by a fictional one; a missing floorplan cannot be invented, and an app with
* a bundled office of its own already has the better answer.
*/
office(id: string): Promise<OfficeDoc | null>;
}
export function createTeraClient(options: TeraApiOptions = {}): TeraClient {
const base = (options.base ?? DEFAULT_BASE).replace(/\/+$/, "");
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
/**
* One GET, and `null` for every way it can go wrong.
*
* Deliberately undiscriminating. A 404, a timeout, a CORS refusal, a static
* host serving `index.html` with a 200 and an HTML content type — the caller's
* response to all of them is the same, and a taxonomy of failures nobody
* branches on is a taxonomy nobody maintains.
*/
async function get<T>(path: string): Promise<T | null> {
if (!doFetch) return null;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await doFetch(`${base}${path}`, {
signal: controller.signal,
headers: { accept: "application/json" },
});
if (!res.ok) return null;
// Checked rather than trusted: a static host answers an unknown path with
// the SPA shell and a 200, and `res.json()` on HTML throws where a content
// type check just returns.
const type = res.headers.get("content-type") ?? "";
if (!type.includes("json")) return null;
return (await res.json()) as T;
} catch {
return null;
} finally {
clearTimeout(timer);
}
}
let flightSource: FlightSource | null = null;
return {
health: () => get<HealthBody>("/health"),
async markers(): Promise<MarkerFeed> {
const body = await get<MarkersBody>("/markers");
if (!body || !Array.isArray(body.markers)) return sampleMarkerFeed();
return {
// A `WireMarker` *is* a `Marker` with a provenance field on it, so this
// is a widening and not a translation — which is the property
// `wire.ts` chose the shape for. The provenance itself is the server's
// to enforce and is deliberately not re-checked here: a browser
// silently dropping rows a self-hoster explicitly allowlisted would look
// exactly like an empty database.
value: body.markers,
live: true,
palette: options.palette ?? {},
generatedAt: body.generatedAt ?? null,
refused: body.refused ?? [],
attribution: body.attribution ?? [],
};
},
async weather(): Promise<WeatherFeed> {
const body = await get<WeatherBody>("/weather");
if (!body) return { value: CLEAR_DAY, live: false, attribution: [] };
// `WeatherBody` is structurally a `WeatherObservation` plus fields no
// renderer reads, which `atmosphere.ts` says in as many words. The extra
// fields ride along harmlessly and the engine never sees them.
return { value: body, live: !body.synthetic, attribution: body.attribution ?? [] };
},
flights(): FlightSource {
flightSource ??= new HttpFlights(get, SAMPLE_ROUTES);
return flightSource;
},
office: (id) => get<OfficeDoc>(`/offices/${encodeURIComponent(id)}`),
};
}
/**
* The clear day a zero-config box serves, restated in the browser.
*
* The server does this too — a weather source configured without what it needs
* is demoted rather than fatal, and it answers `synthetic: true` forever
* (CONTRACT.md §5.1). This is the same answer for the case where there is no
* server at all. Note what it does *not* do: `visibilityKm` stays null, which
* `atmosphere.ts` reads as "nobody measured" rather than as "unlimited", so San
* Francisco's marine layer still runs off its own climatology instead of being
* overruled by a fact nobody observed.
*/
const CLEAR_DAY: WeatherObservation = {
cloudCover: 0.1,
precipitation: 0,
visibilityKm: null,
windKph: null,
windDirDeg: null,
condition: "clear",
};
function sampleMarkerFeed(): MarkerFeed {
return {
value: SAMPLE_MARKERS,
live: false,
palette: SAMPLE_PALETTE,
generatedAt: null,
refused: [],
attribution: [],
};
}
// ---- Traffic --------------------------------------------------------------
/**
* Traffic over HTTP, in whichever of the two shapes the server chose.
*
* `poll()` is synchronous and never awaits the network, which is the whole
* design. `scene.ts` calls it from the render loop, and a source that returned a
* promise resolving on a slow fetch would put a frame's aircraft update behind a
* round trip; instead the network runs in the background on the body's own TTL
* and `poll` answers from whatever is currently in hand.
*
* The two modes are not symmetrical, and `wire.ts` explains why. A *plan* — the
* simulator's routes, a fixed epoch and a seed — is evaluated locally at one
* request per TTL, and because the epoch is fixed rather than the server's start
* time, two people on different machines see the same aircraft in the same
* places. *Live* traffic has no closed form, so it arrives as positions and is
* refetched.
*
* Until the first response lands, and after any failure, this is the simulator
* over `SAMPLE_ROUTES`. An empty sky is a worse answer than an invented one, and
* the invented one is labelled as such in `sample.ts`.
*/
class HttpFlights implements FlightSource {
/**
* One second, which is the *evaluation* cadence and not the request cadence.
* A plan is arithmetic and wants to be evaluated every frame or close to it;
* the network is on `nextFetchAt` and is a great deal slower.
*/
readonly interval = 1;
private readonly fallback: SimulatedFlights;
private plan: FlightsPlanBody | null = null;
private planPhase: number[] = [];
private live: Aircraft[] | null = null;
private nextFetchAt = 0;
private fetching = false;
constructor(
private readonly get: <T>(path: string) => Promise<T | null>,
fallbackRoutes: SimRoute[],
) {
this.fallback = new SimulatedFlights(fallbackRoutes);
}
poll(): Aircraft[] {
this.refreshIfStale();
if (this.plan) return evaluatePlan(this.plan, this.planPhase, Date.now());
if (this.live) return this.live;
return this.fallback.poll();
}
private refreshIfStale(): void {
const now = Date.now();
if (this.fetching || now < this.nextFetchAt) return;
this.fetching = true;
void this.get<FlightsBody>("/flights")
.then((body) => {
if (!body) {
// Hold whatever was already in hand rather than reverting to the
// simulator: a deployment that has been showing real traffic for an
// hour and drops one request should keep showing it, slightly stale,
// not silently swap in fiction.
this.nextFetchAt = now + RETRY_SECONDS * 1000;
return;
}
if (body.mode === "plan") {
this.plan = body;
this.planPhase = phasesFor(body);
this.live = null;
} else {
this.live = body.aircraft;
this.plan = null;
}
this.nextFetchAt = now + Math.max(1, body.ttlSeconds) * 1000;
})
.finally(() => {
this.fetching = false;
});
}
}
/**
* The per-route phase offsets, from the seed the server sent.
*
* Same generator and same order as `SimulatedFlights`, which is what makes the
* server's promise true: every viewer draws the seed once, in route order, and
* arrives at the same sky.
*/
function phasesFor(plan: FlightsPlanBody): number[] {
const rand = seededRandom(plan.seed);
return plan.routes.map(() => rand());
}
function evaluatePlan(plan: FlightsPlanBody, phase: number[], nowMs: number): Aircraft[] {
const seconds = (nowMs - plan.t0) / 1000;
// `WireSimRoute` is structurally `SimRoute`; the restatement in `wire.ts` is
// there so the server can build one without importing three.js.
return plan.routes.map((route, i) => sampleRoute(route, seconds / route.duration + (phase[i] ?? 0)));
}
+308
View File
@@ -0,0 +1,308 @@
/**
* Fabricated demo data, so that a clone of this repo has something on it.
*
* **Everything in this file is invented.** The companies do not exist, have
* never existed, and are named the way they are — Wobbegong, Nonsuch, Pennyfarthing
* — specifically so that nobody can mistake one for a real business. The
* coordinates were typed by hand from a general sense of where San Francisco's
* neighbourhoods are; none of them is anybody's address, and none of them came
* out of a geocoder.
*
* That last point is the licence rule and not a stylistic preference. No real
* company data ships in this repo, for two reasons that happen to want the same
* thing. The privacy one is obvious: pipeline status — who is talking to whom,
* who said no — is private, and a public repo is the wrong place for it. The
* licence one is sharper and is the subject of ARCHITECTURE.md §3.2 and
* CONTRACT.md §8: real positions are *geocoded*, and a geocoder built on
* OpenStreetMap returns ODbL data. Serving a snapshot of those coordinates is
* Publicly Using a Derivative Database, which drags share-alike onto everything
* served alongside it, whether or not the rows live in the repo. So real markers
* arrive at runtime over the API — see `http.ts` — carrying a provenance field
* the server checks, and the repo itself ships this: fiction, which owes nobody
* anything.
*
* The demo is worth having anyway. A map with no pins on it teaches nobody what
* the thing is for, and half the interesting behaviour in `markers.ts` — the
* ghost treatment for a marker that has no real position yet — is invisible
* without data that exercises it.
*/
import type { SimRoute } from "../engine/flights.ts";
import type { Marker, MarkerPalette } from "../engine/types.ts";
/**
* A small pipeline, as colours.
*
* Five states is about the fewest that still shows why `colorKey` is opaque to
* the engine: none of these words means anything to `markers.ts`, which looks
* the key up here and draws whatever it finds. A public sector map would supply
* an entirely different set against the same renderer.
*/
export const SAMPLE_PALETTE: MarkerPalette = {
watching: 0x7f8b99,
applied: 0x4f9cf2,
talking: 0x3fbf9a,
offer: 0xf2b134,
closed: 0xd2544f,
};
/**
* Where the unplaced markers float: out over the bay, east of the Ferry
* Building, in a short arc.
*
* A marker with `located: false` has no address yet, and the honest thing to do
* with a position you do not have is to not pretend you have one. Open water is
* the clearest way to say that on a map — nothing is there, the pin visibly
* stands on nothing, and it cannot be misread as a building. `markers.ts` gives
* these a different silhouette and a lower opacity as well, so the tell does not
* rest on placement alone.
*/
const UNPLACED_LNG = -122.3665;
/**
* Twenty-two invented companies across San Francisco.
*
* Placed by neighbourhood rather than by street: Hayes Valley and SoMa are
* crowded because that is the fact the map is usually drawing attention to, the
* Bayview and the Richmond have one each, and three have no position at all.
* The clustering is the point — a marker layer that only ever gets evenly
* scattered test data hides every overlap problem it has.
*/
export const SAMPLE_MARKERS: Marker[] = [
// Hayes Valley and the bowl below Buena Vista.
{
id: "sample-thimbleway",
label: "Thimbleway Systems",
colorKey: "talking",
lat: 37.7768,
lng: -122.4243,
located: true,
blurb: "Invented. Model-serving, allegedly, in a Victorian with bad wiring.",
},
{
id: "sample-nonsuch",
label: "Nonsuch Cartography Co.",
colorKey: "applied",
lat: 37.7752,
lng: -122.4262,
located: true,
blurb: "Invented. Maps of places that are not there.",
},
{
id: "sample-marmalade",
label: "Marmalade Interchange",
colorKey: "watching",
lat: 37.7781,
lng: -122.4218,
located: true,
blurb: "Invented. Moves data between two formats nobody uses.",
},
// SoMa, where the grid turns forty-six degrees.
{
id: "sample-kettle-anvil",
label: "Kettle & Anvil Compute",
colorKey: "offer",
lat: 37.7805,
lng: -122.4051,
located: true,
blurb: "Invented. Sells the shovels, or claims to.",
},
{
id: "sample-ninth-pelican",
label: "Ninth Pelican Labs",
colorKey: "applied",
lat: 37.7784,
lng: -122.4009,
located: true,
blurb: "Invented. There were never eight others.",
},
{
id: "sample-brassbound",
label: "Brassbound Telemetry",
colorKey: "closed",
lat: 37.7822,
lng: -122.4074,
located: true,
blurb: "Invented. Went quiet after the second call.",
},
// The Financial District and Jackson Square.
{
id: "sample-grimsby-doone",
label: "Grimsby & Doone Photonics",
colorKey: "watching",
lat: 37.7941,
lng: -122.4008,
located: true,
blurb: "Invented. Two surnames and a laser.",
},
{
id: "sample-tugboat",
label: "Tugboat Actuarial",
colorKey: "applied",
lat: 37.7958,
lng: -122.4032,
located: true,
blurb: "Invented. Insurance for things that have already happened.",
},
// Mission Bay: landfill, then biotech.
{
id: "sample-fogbank",
label: "Fogbank Freight",
colorKey: "talking",
lat: 37.7709,
lng: -122.3918,
located: true,
blurb: "Invented. Logistics, in a building younger than most of the staff.",
},
{
id: "sample-bittern",
label: "Bittern & Sons Biologics",
colorKey: "watching",
lat: 37.7688,
lng: -122.3894,
located: true,
blurb: "Invented. No sons.",
},
// The Mission, flat and sunny.
{
id: "sample-unlikely-weather",
label: "Bureau of Unlikely Weather",
colorKey: "offer",
lat: 37.7602,
lng: -122.4151,
located: true,
blurb: "Invented. Forecasts nobody asked for.",
},
{
id: "sample-perpetual-bagel",
label: "Perpetual Bagel Works",
colorKey: "closed",
lat: 37.7574,
lng: -122.4192,
located: true,
blurb: "Invented. The name was the whole pitch.",
},
// Potrero Hill and Dogpatch, the old industrial edge.
{
id: "sample-wobbegong",
label: "Wobbegong Robotics",
colorKey: "talking",
lat: 37.7589,
lng: -122.4002,
located: true,
blurb: "Invented. Named after a carpet shark, for reasons never explained.",
},
{
id: "sample-sourdough-semi",
label: "Sourdough Semiconductor",
colorKey: "applied",
lat: 37.7597,
lng: -122.3881,
located: true,
blurb: "Invented. A fab in a city with no fabs.",
},
{
id: "sample-pennyfarthing",
label: "Pennyfarthing Power",
colorKey: "watching",
lat: 37.7564,
lng: -122.3973,
located: true,
blurb: "Invented. Batteries, uphill.",
},
// The north side and the hills.
{
id: "sample-lamplighter",
label: "Lamplighter Aerostatics",
colorKey: "watching",
lat: 37.8004,
lng: -122.4086,
located: true,
blurb: "Invented. Airships, which are always about to come back.",
},
{
id: "sample-halfpenny",
label: "Halfpenny Optics",
colorKey: "applied",
lat: 37.7929,
lng: -122.4147,
located: true,
blurb: "Invented. Lenses, four hundred feet above the water they look at.",
},
// One each in the parts of the city the map usually forgets.
{
id: "sample-cormorant",
label: "Cormorant Freight Systems",
colorKey: "talking",
lat: 37.7357,
lng: -122.3908,
located: true,
blurb: "Invented. The only pin south of Islais Creek, which is the point.",
},
{
id: "sample-tidewrack",
label: "Tidewrack Instruments",
colorKey: "watching",
lat: 37.7802,
lng: -122.4638,
located: true,
blurb: "Invented. Sensors, in the fog, on purpose.",
},
// Three with no position yet. See `UNPLACED_LNG`.
{
id: "sample-quibble",
label: "Quibble Quantum",
colorKey: "applied",
lat: 37.7965,
lng: UNPLACED_LNG,
located: false,
blurb: "Invented, and unplaced: no address on file, so the map does not invent one.",
},
{
id: "sample-antelope-foundry",
label: "Antelope Foundry",
colorKey: "watching",
lat: 37.7905,
lng: UNPLACED_LNG,
located: false,
blurb: "Invented, and unplaced.",
},
{
id: "sample-mudlark",
label: "Mudlark Instruments",
colorKey: "closed",
lat: 37.7845,
lng: UNPLACED_LNG,
located: false,
blurb: "Invented, and unplaced.",
},
];
/**
* Sample traffic, for when the API is not there to send a flight plan.
*
* The corridors are roughly the real ones — arrivals down the peninsula from
* the north, departures turning out over the Pacific, a slow light aircraft
* crossing the bay — because that is what makes the sky read as this city's sky
* rather than as random motion. The callsigns are not: no real operator uses
* these prefixes, which keeps a demo from looking like a feed of actual
* traffic. Nothing here is observed, and `flights.ts` explains at length why
* this project ships a simulator instead of a client for somebody's live data.
*/
export const SAMPLE_ROUTES: SimRoute[] = [
{ callsign: "NIMBUS 4", from: [37.95, -122.36], to: [37.66, -122.4], fromAlt: 2400, toAlt: 500, duration: 190 },
{ callsign: "NIMBUS 17", from: [37.93, -122.31], to: [37.65, -122.38], fromAlt: 2100, toAlt: 450, duration: 210 },
{ callsign: "PELICAN 2", from: [37.64, -122.39], to: [37.9, -122.62], fromAlt: 700, toAlt: 5200, duration: 165 },
{ callsign: "PELICAN 31", from: [37.7, -122.21], to: [37.88, -122.55], fromAlt: 1800, toAlt: 6100, duration: 230 },
{ callsign: "CORMORANT 8", from: [37.62, -122.6], to: [37.95, -122.28], fromAlt: 6800, toAlt: 8200, duration: 260 },
{ callsign: "KESTREL 5", from: [37.83, -122.56], to: [37.7, -122.22], fromAlt: 1100, toAlt: 1300, duration: 300 },
{ callsign: "NIMBUS 40", from: [37.96, -122.48], to: [37.63, -122.36], fromAlt: 3100, toAlt: 600, duration: 205 },
];