/** * Offline stub situations, shaped like mobile/globe/API.md (band, archetype, * telemetry, headline, media, series [{t,v}]). Everything here is labelled * simulated; Eye cards point at the dev server's real public-camera frames * (`/api/cctv/frame/:id`) and fall back to imagery when that is unreachable. */ import { esriThumb } from './dom.js'; export const SIM = 'Simulated · offline stub engine'; /** Public traffic cameras the dev server proxies (ids from /api/cctv/sources). */ export const STUB_CAMS = [ { id: '354', title: '5th St / Congress Ave', city: 'Austin', provider: 'Austin Transportation & Public Works', lat: 30.267119, lon: -97.743172, headingDeg: 67.5, }, { id: 'tfl-00001.06502', title: 'Trafalgar Square', city: 'London', provider: 'Transport for London', lat: 51.5074, lon: -0.12765, headingDeg: 135, }, { id: 'wsdot-9520', title: 'I-5 at 5th Ave / Columbia', city: 'Seattle', provider: 'WSDOT', lat: 47.605166, lon: -122.330316, headingDeg: 202.5, }, { id: 'ca-d4-tv301', title: 'US-101 at Octavia St', city: 'San Francisco', provider: 'Caltrans', lat: 37.77146, lon: -122.42323, headingDeg: 180, }, { id: 'no-vegvesen-3000051_1', title: 'F862 Sentrumstangenten', city: 'Tromsø', provider: 'Statens vegvesen', lat: 69.64469, lon: 18.947695, headingDeg: 270, }, { id: 'ca-d11-x1248', title: 'I-5 at First Avenue', city: 'San Diego', provider: 'Caltrans', lat: 32.72285, lon: -117.16356, headingDeg: 180, }, { id: 'fi-c1455101', title: 'vt4 Rovaniemi Revontuli', city: 'Rovaniemi', provider: 'Fintraffic', lat: 66.503858, lon: 25.725196, headingDeg: 202.5, }, ]; export const camFrameUrl = (id) => `/api/cctv/frame/${encodeURIComponent(id)}?ts=${Math.floor(Date.now() / 10000)}`; function series(values, stepMin, now) { return values.map((v, i) => ({ t: now - (values.length - 1 - i) * stepMin * 60e3, v, })); } export function buildStubSituations(now) { const min = 60e3; const S = (o) => ({ live: true, source: SIM, section: 'now', severity: 0, updatedAt: now - (o.age ?? 2) * min, time: o.time ?? { start: now - 6 * 3600e3, end: now + 24 * 3600e3 }, camera: o.camera ?? { lon: o.lon, lat: o.lat, height: o.height ?? 400e3, heading: 0, pitch: -90, }, thumbnail: esriThumb(o.lon, o.lat, o.span ?? 1, 480, o.th ?? 600), ...o, }); const eye = (cam, o) => S({ kind: 'camera', family: 'eyes', band: 'ground', archetype: 'eye', lon: cam.lon, lat: cam.lat, span: 0.01, height: 1500, camera: { lon: cam.lon, lat: cam.lat, height: 900, heading: cam.headingDeg, pitch: -30, }, layers: ['eyes.public-cctv', 'ground.traffic-flow'], media: { kind: 'image', url: camFrameUrl(cam.id), refreshMs: 15000, cameraId: cam.id, headingDeg: cam.headingDeg, provider: cam.provider, city: cam.city, }, telemetry: `GROUND · CAM ${cam.id.slice(-6).toUpperCase()} · ${cam.title.toUpperCase()}`, source: `${cam.provider} public camera`, ...o, }); const C = Object.fromEntries(STUB_CAMS.map((c) => [c.id, c])); return [ S({ id: 'cyclone-wpac', kind: 'cyclone', family: 'hazards', band: 'orbit', archetype: 'sensor', severity: 5, title: 'Typhoon tracking toward Luzon', subtitle: 'Category 4 · eyewall replacement under way', place: 'Philippine Sea', lon: 126.5, lat: 15.4, span: 9, th: 640, height: 2200e3, layers: ['hazards.tropical-cyclones', 'weather.clouds', 'weather.wind'], metrics: [ { label: 'Wind', value: 185, unit: 'km/h' }, { label: 'Pressure', value: 935, unit: 'hPa' }, { label: 'Moving', value: 'WNW 18', unit: 'km/h' }, ], headline: { value: 185, unit: 'km/h', label: 'sustained wind' }, telemetry: 'ORBIT · GOES IR · 185 KM/H · 935 HPA · WNW 18', series: series( [120, 130, 140, 150, 165, 175, 185, 185, 180, 185], 360, now, ), seriesLabel: 'sustained wind, km/h', age: 2, time: { start: now - 72 * 3600e3, end: now + 120 * 3600e3 }, events: [{ at: now + 30 * 3600e3, label: 'Landfall (fcst)' }], }), eye(C['354'], { id: 'eye-austin', title: 'Saturday night on Congress Ave', subtitle: 'Downtown crowds after the game', place: 'Downtown Austin, Texas', severity: 2, age: 0, }), S({ id: 'squawk-7700', kind: 'squawk', family: 'sky', band: 'sky', archetype: 'track', severity: 4, title: 'Squawk 7700 · descending toward Halifax', subtitle: 'Simulated emergency track, diverting', place: 'Over Nova Scotia', lon: -62.4, lat: 45.1, span: 2.4, th: 520, height: 350e3, layers: ['sky.emergency-squawks', 'sky.commercial-flights'], metrics: [ { label: 'Altitude', value: 'FL100', unit: '' }, { label: 'Speed', value: 312, unit: 'kt' }, { label: 'Squawk', value: '7700', unit: '' }, ], headline: { value: '7700', unit: '', label: 'general emergency' }, telemetry: 'SKY · FL100 ↓ · 312 KT · SQ 7700', track: { headingDeg: 310 }, age: 1, }), S({ id: 'lightning-gulf', kind: 'lightning', family: 'weather', band: 'sky', archetype: 'number', severity: 3, title: 'Lightning over the Gulf of Mexico', subtitle: 'A squall line pushing toward the Louisiana coast', place: 'Gulf of Mexico', lon: -90.2, lat: 27.4, span: 5, th: 460, height: 900e3, layers: ['weather.lightning', 'weather.radar'], metrics: [ { label: 'Strikes / h', value: 4812, unit: '' }, { label: 'Peak density', value: 38, unit: '/km²' }, ], headline: { value: 4812, unit: '', label: 'strikes in the last hour' }, telemetry: 'SKY · GLM · 4,812 STR/H · 27.4N 90.2W', series: series([620, 900, 1400, 2100, 2900, 3600, 4100, 4812], 15, now), seriesLabel: 'strikes / 15 min', age: 1, }), S({ id: 'quake-24h', kind: 'earthquake', family: 'hazards', band: 'ground', archetype: 'sensor', severity: 4, title: 'M6.4 · largest quake in 24 h', subtitle: 'Shallow thrust event, aftershocks continuing', place: 'Off Hokkaido, Japan', lon: 144.2, lat: 42.1, span: 3.5, th: 480, height: 900e3, layers: ['hazards.earthquakes'], metrics: [ { label: 'Magnitude', value: 6.4, unit: 'Mw' }, { label: 'Depth', value: 38, unit: 'km' }, { label: 'Aftershocks', value: 27, unit: '' }, ], headline: { value: 6.4, unit: 'Mw', label: 'magnitude' }, telemetry: 'GROUND · USGS · M6.4 · 38 KM DEEP', age: 11, events: [{ at: now - 14 * 3600e3, label: 'M6.4' }], }), S({ id: 'iss-pass', kind: 'iss', family: 'orbit', band: 'orbit', archetype: 'track', severity: 2, title: 'ISS crosses your sky at 21:42', subtitle: 'Bright pass · max elevation 71°', place: 'Low Earth orbit', lon: -40, lat: 30, span: 30, th: 520, height: 9000e3, layers: ['orbit.iss-crewed', 'orbit.ground-tracks'], metrics: [ { label: 'Altitude', value: 418, unit: 'km' }, { label: 'Speed', value: 7.66, unit: 'km/s' }, { label: 'Next pass', value: '21:42', unit: '' }, ], headline: { value: '21:42', unit: '', label: 'next pass · 71° max' }, telemetry: 'ORBIT · 418 KM · 7.66 KM/S · PASS 21:42', track: { headingDeg: 52 }, age: 1, }), S({ id: 'fires-sierra', kind: 'fires', family: 'hazards', band: 'ground', archetype: 'sensor', severity: 4, title: 'Fire cluster in the Sierra foothills', subtitle: 'Hotspots doubling since dawn, smoke drifting east', place: 'Nevada County, California', lon: -120.9, lat: 39.05, span: 1.6, th: 560, height: 260e3, layers: [ 'hazards.active-fires', 'hazards.fire-perimeters', 'weather.wind', ], metrics: [ { label: 'Hotspots', value: 214, unit: '' }, { label: 'FRP', value: 1840, unit: 'MW' }, { label: 'Wind', value: 32, unit: 'km/h' }, ], headline: { value: 214, unit: '', label: 'hotspots' }, telemetry: 'GROUND · FIRMS VIIRS · 214 HOTSPOTS · WIND W 32', series: series([12, 18, 30, 44, 61, 88, 120, 160, 190, 214], 60, now), seriesLabel: 'hotspots / h', age: 4, }), S({ id: 'gnss-baltic', kind: 'gnss', family: 'infra', band: 'sky', archetype: 'number', severity: 3, title: 'GNSS jamming over the Baltic', subtitle: 'Aircraft reporting degraded navigation integrity', place: 'Baltic Sea', lon: 21.0, lat: 56.5, span: 6, th: 480, height: 1500e3, layers: ['infra.gnss-interference', 'sky.commercial-flights'], metrics: [ { label: 'Affected', value: 312, unit: 'aircraft' }, { label: 'Area', value: 84, unit: 'k km²' }, ], headline: { value: 312, unit: '', label: 'aircraft affected' }, telemetry: 'SKY · ADS-B NIC<7 · 312 AC · 84K KM²', series: series([120, 180, 240, 260, 300, 312], 60, now), seriesLabel: 'aircraft with NIC < 7', age: 9, }), eye(C['tfl-00001.06502'], { id: 'eye-trafalgar', title: 'Trafalgar Square, live', subtitle: 'Looking south-east down Whitehall', place: 'Westminster, London', severity: 1, age: 0, }), S({ id: 'circling-midtown', kind: 'circling', family: 'sky', band: 'sky', archetype: 'track', severity: 4, live: false, title: '3 helicopters circled Midtown Manhattan', subtitle: 'Detected from orbiting tracks · replay the Moment', place: 'Empire State Building, New York', lon: -73.98566, lat: 40.74844, span: 0.05, th: 520, height: 6000, layers: ['sky.helicopters', 'sky.commercial-flights'], metrics: [ { label: 'Rotorcraft', value: 3, unit: '' }, { label: 'Orbit radius', value: '450–650', unit: 'm' }, { label: 'Altitude', value: '1,050–1,450', unit: 'ft' }, ], headline: { value: 3, unit: 'helis', label: 'circling one point' }, telemetry: 'SKY · 3 ROTORCRAFT · R 450–650 M · 1,050–1,450 FT', momentId: 'empire-state', illustrative: true, track: { headingDeg: 0, circuit: true }, age: 76, }), S({ id: 'novosel', kind: 'helicopters', family: 'sky', band: 'sky', archetype: 'track', title: 'Rotor school: 14th circuit of the day', subtitle: 'A training helicopter flying the pattern', place: 'Fort Novosel, Alabama', lon: -85.72, lat: 31.36, span: 0.22, th: 420, height: 40e3, layers: ['sky.helicopters'], metrics: [ { label: 'Airborne', value: 23, unit: 'rotorcraft' }, { label: 'Pattern alt', value: 700, unit: 'ft' }, ], headline: { value: 14, unit: 'th', label: 'circuit today' }, telemetry: 'SKY · 700 FT · 88 KT · CIRCUIT 14', track: { headingDeg: 140, circuit: true }, age: 3, alwaysOn: true, }), S({ id: 'bhote-koshi-2026', kind: 'reconstruction', family: 'hazards', band: 'inside', archetype: 'sensor', title: 'Bhote Koshi outburst flood', subtitle: '16 sourced observations · 3 open hypotheses', place: 'Rasuwa, Nepal ↔ Gyirong, Tibet', section: 'reconstructions', lon: 85.33, lat: 28.15, span: 0.3, th: 640, height: 30e3, camera: { lon: 85.3548, lat: 28.207, height: 14e3, heading: 344, pitch: -39, }, layers: [ 'hazards.floods', 'infra.recent-imagery', 'eyes.signed-captures', ], metrics: [ { label: 'Observations', value: 16, unit: '' }, { label: 'Corridor', value: 145, unit: 'pts' }, { label: 'Hypotheses', value: 3, unit: '' }, ], telemetry: 'INSIDE · 4D · 16 OBS · 22 KM CORRIDOR', live: false, age: 60 * 24 * 7, source: 'GeoPera reconstruction · Vantor Open Data (CC BY-NC 4.0)', time: { start: Date.UTC(2026, 7, 26, 0), end: Date.UTC(2026, 7, 27, 12) }, thread: 'bhote-koshi-2026', thumbnail: '/events/bhote-koshi-2026/post.webp', }), S({ id: 'rain-houston', kind: 'rain', family: 'weather', band: 'sky', archetype: 'sensor', severity: 3, title: 'Heavy rain cell over Houston', subtitle: '60 dBZ core over the Ship Channel, moving east', place: 'Houston, Texas', lon: -95.25, lat: 29.74, span: 0.9, th: 540, height: 120e3, layers: ['weather.radar', 'weather.lightning'], metrics: [ { label: 'Peak', value: 60, unit: 'dBZ' }, { label: 'Rate', value: 76, unit: 'mm/h' }, ], headline: { value: 76, unit: 'mm/h', label: 'rain rate' }, telemetry: 'SKY · MRMS · 60 DBZ · 76 MM/H · E 22 KM/H', age: 2, }), S({ id: 'starbase', kind: 'launch', family: 'orbit', band: 'orbit', archetype: 'track', title: 'Launch window opens at Starbase', subtitle: 'Trajectory east over the Gulf', place: 'Boca Chica, Texas', lon: -97.157, lat: 25.997, span: 0.06, th: 380, height: 12e3, layers: ['orbit.launches', 'orbit.ground-tracks'], metrics: [ { label: 'T−', value: '06:12:00', unit: '' }, { label: 'Weather go', value: 80, unit: '%' }, ], headline: { value: 'T−06:12', unit: '', label: 'to window' }, telemetry: 'ORBIT · PAD B · AZ 097° · T−06:12:00', track: { headingDeg: 97, launch: true }, age: 6, events: [{ at: now + 6.2 * 3600e3, label: 'T-0' }], }), eye(C['wsdot-9520'], { id: 'eye-seattle', title: 'I-5 through downtown Seattle', subtitle: 'Southbound into the convention center tunnel', place: 'Seattle, Washington', age: 0, }), S({ id: 'suez-queue', kind: 'queue', family: 'sea', band: 'ground', archetype: 'number', title: '41 ships waiting at Suez', subtitle: 'Southbound convoy holding in the anchorage', place: 'Gulf of Suez, Egypt', lon: 32.55, lat: 29.88, span: 0.35, th: 560, height: 60e3, layers: ['sea.ais-vessels', 'sea.tankers-cargo'], metrics: [ { label: 'Waiting', value: 41, unit: 'vessels' }, { label: 'Median wait', value: 19, unit: 'h' }, ], headline: { value: 41, unit: '', label: 'vessels at anchor' }, telemetry: 'GROUND · AIS · 41 AT ANCHOR · WAIT 19 H', series: series([28, 30, 33, 35, 39, 41], 120, now), seriesLabel: 'vessels at anchor', age: 5, }), S({ id: 'north-sea-mil', kind: 'military', family: 'sky', band: 'sky', archetype: 'sensor', title: 'Tanker orbits over the North Sea', subtitle: 'Two refuelling tracks and a fighter CAP', place: 'North Sea', lon: 3.5, lat: 55.5, span: 5, th: 520, height: 900e3, layers: ['sky.military-aircraft'], metrics: [ { label: 'Aircraft', value: 14, unit: '' }, { label: 'Tankers', value: 3, unit: '' }, ], telemetry: 'SKY · FL260 · 3 TANKERS · 14 MIL', age: 7, }), eye(C['no-vegvesen-3000051_1'], { id: 'eye-tromso', kind: 'camera', title: 'Tromsø under a Kp 6 sky', subtitle: 'Road camera on Sentrumstangenten, aurora nowcast high', place: 'Tromsø, Norway', age: 0, }), S({ id: 'jet-stream', kind: 'jet', family: 'weather', band: 'sky', archetype: 'sensor', title: 'Jet stream at 210 kt over the Atlantic', subtitle: 'Eastbound flights riding it, westbound avoiding it', place: 'North Atlantic', lon: -38, lat: 49, span: 22, th: 460, height: 4500e3, layers: ['weather.wind', 'sky.commercial-flights'], metrics: [{ label: 'Core', value: 210, unit: 'kt' }], headline: { value: 210, unit: 'kt', label: 'jet core at FL340' }, telemetry: 'SKY · GFS 250 HPA · 210 KT · FL340', age: 20, }), S({ id: 'atl-rush', kind: 'airport', family: 'sky', band: 'sky', archetype: 'number', title: 'Busiest airport right now', subtitle: 'Hartsfield–Jackson arrival bank', place: 'Atlanta, Georgia', lon: -84.428, lat: 33.6407, span: 0.09, th: 600, height: 18e3, layers: [ 'sky.commercial-flights', 'sky.holding-patterns', 'sky.airport-boards', ], metrics: [ { label: 'Movements', value: 182, unit: '/h' }, { label: 'Holding', value: 6, unit: '' }, ], headline: { value: 182, unit: '/h', label: 'movements' }, telemetry: 'SKY · KATL · 182 MVT/H · 6 HOLDING', series: series([120, 140, 166, 170, 182, 176, 181, 182], 30, now), seriesLabel: 'movements / h', age: 1, alwaysOn: true, }), eye(C['ca-d4-tv301'], { id: 'eye-sf', title: 'US-101 at Octavia, San Francisco', subtitle: 'Central Freeway merging into Mission', place: 'San Francisco, California', age: 0, alwaysOn: true, }), S({ id: 'tokyo-rush', kind: 'transit', family: 'ground', band: 'ground', archetype: 'sensor', title: 'Rush hour on the Yamanote line', subtitle: 'Headways at 2 min 30 s through Shinjuku', place: 'Shinjuku, Tokyo', lon: 139.7, lat: 35.69, span: 0.05, th: 600, height: 9e3, layers: ['ground.transit-vehicles', 'ground.rail'], metrics: [ { label: 'Trains moving', value: 1840, unit: '' }, { label: 'On time', value: 98.6, unit: '%' }, ], telemetry: 'GROUND · GTFS-RT · 1,840 TRAINS · 98.6% OT', age: 2, alwaysOn: true, }), S({ id: 'aurora', kind: 'aurora', family: 'weather', band: 'orbit', archetype: 'sensor', title: 'Aurora oval reaching 60°N tonight', subtitle: 'Kp 6 storm, clear skies after 22:00', place: 'Northern Scandinavia', lon: 18.95, lat: 69.65, span: 8, th: 600, height: 1200e3, layers: ['weather.clouds'], metrics: [ { label: 'Kp', value: 6, unit: '' }, { label: 'Cloud', value: 20, unit: '%' }, ], headline: { value: 'Kp 6', unit: '', label: 'geomagnetic storm' }, telemetry: 'ORBIT · OVATION · KP 6 · OVAL 60N', series: series([2, 3, 3, 4, 5, 6, 6], 180, now), seriesLabel: 'Kp index', age: 15, }), S({ id: 'singapore', kind: 'strait', family: 'sea', band: 'ground', archetype: 'sensor', title: 'Singapore Strait at capacity', subtitle: 'Eastbound lane full, 318 ships at anchor', place: 'Singapore Strait', lon: 103.85, lat: 1.22, span: 0.4, th: 420, height: 70e3, layers: ['sea.ais-vessels'], metrics: [ { label: 'Transits', value: 22, unit: '/h' }, { label: 'At anchor', value: 318, unit: '' }, ], telemetry: 'GROUND · AIS · 22 TRANSITS/H · 318 ANCHORED', age: 3, alwaysOn: true, }), ]; }