1
0

Lumbridge Simulate Engine — the city, and the licence it can actually ship under

LSE is the third of the three, beside lumbridge-compute and lumbridge-bench: a
3D engine for walkable places. This first commit is the outside of the world —
San Francisco — plus the seams the inside will attach to.

The engine renders a City and a list of Markers and knows nothing else. It does
not know markers are usually companies and it will never learn that "rejected"
is red; that mapping lives in an adapter. Which is what lets one renderer serve
a private map, a public one, and a self-hoster with no Lumbridge account, none
of them a fork of the others.

Three things were designed around the licence rather than discovered after it,
because each one is a promise Apache 2.0 makes that is easy to break by
accident. No trademarks in the repo — logos are fetched at runtime, and
public/logos/ is gitignored. No OpenStreetMap-derived coordinates, which is why
every coastline in cities/sf.ts was traced by hand: Nominatim output is ODbL,
share-alike, and would attach to the whole pack. And no FlightRadar24 client —
their terms forbid scraping and redistribution, so flights are an interface
with a simulator and open community ADS-B behind it.

The privacy constraint and the licence constraint turned out to want the same
thing. Geocoded company positions and pipeline status both stay behind Workie's
API; the open repo holds the city and the renderer. The tempting shortcut —
commit an sf-companies.json — breaks both at once.

Ported out of Workie, where a 3D city engine had no business living. Workie's
/live is deleted rather than deprecated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Karti Tripathi
2026-08-04 21:49:08 -07:00
commit 67f8df8d53
20 changed files with 4789 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
/**
* Aircraft over the city.
*
* The engine takes a `FlightSource` rather than talking to any particular
* service, because the obvious one cannot ship here. FlightRadar24's terms
* forbid scraping and forbid redistributing their 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. Commercial sources are
* adapters in a private deployment; this file holds what we can actually give
* away. See ARCHITECTURE.md §4.
*
* `SimulatedFlights` is the default and is genuinely enough for the map — what
* a city view wants is convincing motion in the right corridors, not a
* spotter's log.
*/
import * as THREE from "three";
import type { Aircraft, FlightSource } from "./types.ts";
import { seededRandom, type World } from "./world.ts";
/** A route the simulator flies: great-circle-ish, with a climb or descent. */
export interface SimRoute {
callsign: string;
from: [number, number];
to: [number, number];
/** Metres at the start and end of the leg. */
fromAlt: number;
toAlt: number;
/** Seconds for a full traversal. */
duration: number;
}
/**
* 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
* the sky is never empty and never synchronised.
*/
export class SimulatedFlights implements FlightSource {
readonly interval = 1;
private readonly routes: SimRoute[];
private readonly phase: number[];
private t = 0;
private last = 0;
constructor(routes: SimRoute[], seed = 4711) {
this.routes = routes;
const rand = seededRandom(seed);
this.phase = routes.map(() => rand());
this.last = nowSeconds();
}
poll(): Aircraft[] {
const now = nowSeconds();
this.t += Math.min(now - this.last, 5);
this.last = now;
return this.routes.map((route, i) => {
const p = ((this.t / route.duration + (this.phase[i] ?? 0)) % 1 + 1) % 1;
const lat = route.from[0] + (route.to[0] - route.from[0]) * p;
const lng = route.from[1] + (route.to[1] - route.from[1]) * p;
// Ease the altitude so departures climb steeply and level off.
const ease = 1 - (1 - p) ** 2;
const altitude = route.fromAlt + (route.toAlt - route.fromAlt) * ease;
const heading =
(Math.atan2(route.to[1] - route.from[1], route.to[0] - route.from[0]) * 180) / Math.PI;
return { id: `sim-${route.callsign}`, callsign: route.callsign, lat, lng, altitude, heading };
});
}
}
function nowSeconds(): number {
return (typeof performance !== "undefined" ? performance.now() : 0) / 1000;
}
/**
* Community ADS-B, for when real traffic is wanted.
*
* `adsb.lol` and `airplanes.live` both serve open, key-free feeds of
* volunteer-fed ADS-B and are the sources this project can point at without a
* licence problem. The best answer long-term is an RTL-SDR on a fleet box:
* first-party data, nothing to comply with.
*/
export class AdsbFlights implements FlightSource {
readonly interval = 8;
constructor(
private readonly endpoint: string,
private readonly radiusNm = 25,
private readonly center: { lat: number; lng: number } = { lat: 37.77, lng: -122.42 },
) {}
async poll(): Promise<Aircraft[]> {
const url = `${this.endpoint}/v2/point/${this.center.lat}/${this.center.lng}/${this.radiusNm}`;
try {
const res = await fetch(url);
if (!res.ok) return [];
const body = (await res.json()) as { ac?: RawAircraft[] };
return (body.ac ?? [])
.filter((a) => typeof a.lat === "number" && typeof a.lon === "number")
.map((a) => ({
id: a.hex ?? `${a.flight ?? "?"}`,
callsign: a.flight?.trim(),
lat: a.lat as number,
lng: a.lon as number,
// Feed reports feet; the scene works in metres.
altitude: typeof a.alt_baro === "number" ? a.alt_baro * 0.3048 : 3000,
heading: typeof a.track === "number" ? a.track : 0,
}));
} catch {
// A dead feed must not take the render loop with it.
return [];
}
}
}
interface RawAircraft {
hex?: string;
flight?: string;
lat?: number;
lon?: number;
alt_baro?: number;
track?: number;
}
// ---- Rendering ------------------------------------------------------------
export interface FlightLayer {
group: THREE.Group;
update(aircraft: Aircraft[]): void;
dispose(): void;
}
/**
* Aircraft as small darts with a shadow-less trail. Rendered at true altitude
* through the world's vertical exaggeration, so a jet on approach sits visibly
* below one at cruise.
*/
export function createFlightLayer(world: World): FlightLayer {
const group = new THREE.Group();
group.name = "flights";
const geo = new THREE.ConeGeometry(0.1, 0.42, 5);
geo.rotateX(Math.PI / 2); // point along +z, so heading maps to a Y rotation
const material = new THREE.MeshLambertMaterial({ color: 0xf2f5f8 });
const meshes = new Map<string, THREE.Mesh>();
function update(aircraft: Aircraft[]) {
const seen = new Set<string>();
for (const a of aircraft) {
seen.add(a.id);
let mesh = meshes.get(a.id);
if (!mesh) {
mesh = new THREE.Mesh(geo, material);
meshes.set(a.id, mesh);
group.add(mesh);
}
const [x, z] = world.project(a.lat, a.lng);
mesh.position.set(x, world.metres(a.altitude), z);
mesh.rotation.y = -(a.heading * Math.PI) / 180;
}
for (const [id, mesh] of meshes) {
if (seen.has(id)) continue;
group.remove(mesh);
meshes.delete(id);
}
}
return {
group,
update,
dispose() {
geo.dispose();
material.dispose();
meshes.clear();
group.clear();
},
};
}