mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(mobile): recorder time machine, detections, Moments, dossiers and instant start
Records every live contact into an IndexedDB ring buffer so scrubbing back shows real past positions with ghost trails; playback at up to 600x; timeline pips from every layer. Detects circling aircraft and quake clusters on live data and opens them as Moments. Adds the illustrative Empire State hero Moment over real NYC footprints, entity dossiers, search and link graphs, geolocated reactions, skyNow, and a bundled real-data snapshot for an instant first frame. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
This commit is contained in:
8 files changed
+92
-13
No files matched your search
+1
-1
@@ -170,7 +170,7 @@ Entity ids are layer-qualified: `flights:<icao24>`, `military:<hex>`, `satellite
|
||||
|
||||
Detections run on recorder data every poll. They appear only when real data shows them.
|
||||
|
||||
- `circling`: two or more aircraft (rotorcraft and low-and-slow preferred, below about 8,000 ft and 155 kt), each turning through at least 360° within about 1.5 km of a common centre in the last 20 min. Card: `{ kind:'circling', band:'sky', archetype:'track', title:'3 helicopters circling Midtown Manhattan', momentId:'live-circling-…', members:[{ key, layer, callsign, helicopter }], entityIds, detected:true }`. Its thumbnail draws the recorded orbit tracks.
|
||||
- `circling`: two or more aircraft (rotorcraft and low-and-slow preferred, below about 8,000 ft and 155 kt), each turning through at least 360° within about 1.5 km of a common centre in the last 20 min. Card: `{ kind:'circling', band:'sky', archetype:'track', title:'3 helicopters circling Midtown Manhattan', momentId:'live-circling-…', members:[{ key, layer, callsign, helicopter }], entityIds, detected:true }`. Its thumbnail draws the recorded orbit tracks. A single aircraft on its second full orbit is a lesser detection (severity 4.5, `single: true`, for example "Helicopter circling Las Vegas Strip"). `momentOfTheDay()` ignores single orbits.
|
||||
- `quake-cluster`: three or more M2.5+ earthquakes within 50 km in 24 h. Archetype `number`, with a magnitude series.
|
||||
- `squawk` cards now carry `momentId: 'live-squawk-<icao24>'`.
|
||||
|
||||
|
||||
+12
-1
@@ -198,8 +198,19 @@ export function createDetector(recorder) {
|
||||
: helis
|
||||
? 'aircraft'
|
||||
: 'low aircraft';
|
||||
const id = `circling-${lat.toFixed(2)}_${lon.toFixed(2)}`;
|
||||
let id = `circling-${lat.toFixed(2)}_${lon.toFixed(2)}`;
|
||||
const firstOrbit = Math.min(...g.map((m) => m.orbit.from));
|
||||
// The same aircraft drifting a few hundred metres is the same
|
||||
// detection: keep its id and start time.
|
||||
for (const [oid, d] of live)
|
||||
if (
|
||||
oid !== id &&
|
||||
d.members.some((m) => g.some((x) => x.key === m.key)) &&
|
||||
haversineKm(d.lat, d.lon, lat, lon) < 3
|
||||
) {
|
||||
id = oid;
|
||||
break;
|
||||
}
|
||||
const prev = live.get(id);
|
||||
const det = {
|
||||
id,
|
||||
|
||||
@@ -13,6 +13,13 @@ const globe = await createGlobe(document.getElementById('globe'), {
|
||||
preserveDrawingBuffer: params.has('capture'),
|
||||
});
|
||||
window.globe = globe;
|
||||
window.__globeReady = performance.now();
|
||||
{
|
||||
const off = globe.viewer.scene.postRender.addEventListener(() => {
|
||||
window.__firstFrame = performance.now();
|
||||
off();
|
||||
});
|
||||
}
|
||||
|
||||
let quality = globe.quality();
|
||||
let lastPick = null;
|
||||
|
||||
@@ -289,6 +289,13 @@ export function createEntities(env) {
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
// One link per target (the first relation wins).
|
||||
const seenIds = new Set();
|
||||
links.splice(
|
||||
0,
|
||||
links.length,
|
||||
...links.filter((l) => !seenIds.has(l.id) && seenIds.add(l.id)),
|
||||
);
|
||||
if (here)
|
||||
links.push({
|
||||
id: `place:${slug(localPlace(here.lat, here.lon).name)}`,
|
||||
@@ -307,6 +314,7 @@ export function createEntities(env) {
|
||||
const callsign = (r?.callsign || meta.callsign || '').trim();
|
||||
const t = env.timeMs();
|
||||
const recState = recorder.stateAt(key, t);
|
||||
if (!live && !recorder.has(key)) throw new Error(`Unknown aircraft ${key}`);
|
||||
const here = r
|
||||
? { lon: r.lon, lat: r.lat }
|
||||
: recState
|
||||
@@ -415,7 +423,13 @@ export function createEntities(env) {
|
||||
...(r?.squawk || meta.squawk
|
||||
? { squawk: r?.squawk || meta.squawk }
|
||||
: {}),
|
||||
category: isHeli ? 'rotorcraft' : r?.cls || meta.kind || null,
|
||||
category: isHeli
|
||||
? 'rotorcraft'
|
||||
: r?.category != null && r?.cls
|
||||
? r.cls
|
||||
: isMil
|
||||
? 'military'
|
||||
: null,
|
||||
military: isMil,
|
||||
},
|
||||
live:
|
||||
|
||||
@@ -112,6 +112,36 @@ export const WORLD_PLACES = Object.freeze([
|
||||
['Asunción', -57.58, -25.26],
|
||||
['Reykjavík', -21.9, 64.15],
|
||||
['London', -0.13, 51.51],
|
||||
['Birmingham, UK', -1.89, 52.48],
|
||||
['Coventry', -1.51, 52.41],
|
||||
['Manchester', -2.24, 53.48],
|
||||
['Liverpool', -2.98, 53.41],
|
||||
['Leeds', -1.55, 53.8],
|
||||
['Sheffield', -1.47, 53.38],
|
||||
['Newcastle upon Tyne', -1.61, 54.98],
|
||||
['Bristol', -2.59, 51.45],
|
||||
['Cardiff', -3.18, 51.48],
|
||||
['Oxford', -1.26, 51.75],
|
||||
['Cambridge, UK', 0.12, 52.2],
|
||||
['Nottingham', -1.15, 52.95],
|
||||
['Leicester', -1.13, 52.64],
|
||||
['Southampton', -1.4, 50.9],
|
||||
['Brighton', -0.14, 50.82],
|
||||
['Norwich', 1.3, 52.63],
|
||||
['Glasgow', -4.25, 55.86],
|
||||
['Edinburgh', -3.19, 55.95],
|
||||
['Belfast', -5.93, 54.6],
|
||||
['Dublin', -6.26, 53.35],
|
||||
['Lyon', 4.84, 45.76],
|
||||
['Marseille', 5.37, 43.3],
|
||||
['Hamburg', 9.99, 53.55],
|
||||
['Munich', 11.58, 48.14],
|
||||
['Cologne', 6.96, 50.94],
|
||||
['Frankfurt', 8.68, 50.11],
|
||||
['Rotterdam', 4.48, 51.92],
|
||||
['Brussels', 4.35, 50.85],
|
||||
['Zürich', 8.54, 47.37],
|
||||
['Milan', 9.19, 45.46],
|
||||
['Paris', 2.35, 48.86],
|
||||
['Madrid', -3.7, 40.42],
|
||||
['Lisbon', -9.14, 38.72],
|
||||
@@ -290,6 +320,14 @@ export const NEIGHBORHOODS = Object.freeze([
|
||||
['Compton', -118.2201, 33.8958, 2.5],
|
||||
['LAX', -118.4085, 33.9416, 2.5],
|
||||
['Anaheim', -117.9145, 33.8366, 3.5],
|
||||
['the San Gabriel Valley', -118.03, 34.08, 9.0],
|
||||
['Glendale, California', -118.2551, 34.1425, 3.0],
|
||||
['Riverside', -117.3962, 33.9533, 6.0],
|
||||
['San Bernardino', -117.2898, 34.1083, 6.0],
|
||||
['Santa Ana', -117.8678, 33.7455, 4.0],
|
||||
['Irvine', -117.8265, 33.6846, 5.0],
|
||||
['Downtown San Diego', -117.1611, 32.7157, 2.5],
|
||||
['Henderson, Nevada', -114.9817, 36.0395, 5.0],
|
||||
['Westminster, London', -0.1357, 51.4975, 1.4],
|
||||
['the City of London', -0.0918, 51.5155, 1.2],
|
||||
['Canary Wharf', -0.0235, 51.5054, 1.2],
|
||||
|
||||
@@ -1502,12 +1502,16 @@ export async function createGlobe(container, options = {}) {
|
||||
},
|
||||
|
||||
async momentOfTheDay() {
|
||||
const live = liveMoments()
|
||||
.filter(
|
||||
(m) => m.tracks.length >= 2 || m.time.end - m.time.start > 5 * 60_000,
|
||||
// Worth a daily replay: several aircraft circling one place, or an
|
||||
// emergency with at least 5 min of track. A lone orbit is not enough.
|
||||
const best = liveMoments()
|
||||
.filter((m) =>
|
||||
m.situationId
|
||||
? m.tracks.length >= 2
|
||||
: m.time.end - m.time.start > 5 * 60_000,
|
||||
)
|
||||
.sort((a, b) => b.tracks.length - a.tracks.length);
|
||||
return live[0] || hero();
|
||||
return best[0] || hero();
|
||||
},
|
||||
|
||||
async playMoment(
|
||||
|
||||
@@ -190,7 +190,11 @@ export function createRecorder({
|
||||
prev.callsign !== meta.callsign ||
|
||||
(meta.registration && prev.registration !== meta.registration)
|
||||
) {
|
||||
// Flags learned from any source stick (one feed may omit the
|
||||
// rotorcraft category another one reported).
|
||||
tr.meta = { ...prev, ...meta };
|
||||
for (const flag of ['helicopter', 'military', 'pia', 'ladd'])
|
||||
if (prev?.[flag]) tr.meta[flag] = true;
|
||||
if (!fromStore) dirtyMeta.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,16 +585,18 @@ export function createStage({ scene, requestRender, emit }) {
|
||||
}),
|
||||
);
|
||||
fill.show = false;
|
||||
const edgeMat = Cesium.Material.fromType('Color', {
|
||||
color: css(color, c.firstEyes ? 0.95 : 0.6),
|
||||
});
|
||||
// One material per polyline: a collection destroys each on removal.
|
||||
const edgeMat = () =>
|
||||
Cesium.Material.fromType('Color', {
|
||||
color: css(color, c.firstEyes ? 0.95 : 0.6),
|
||||
});
|
||||
const edges = [];
|
||||
for (let k = 0; k < 4; k++)
|
||||
edges.push(
|
||||
lines.add({
|
||||
positions: [origin, corners[k]],
|
||||
width: c.firstEyes ? 1.8 : 1.1,
|
||||
material: edgeMat,
|
||||
material: edgeMat(),
|
||||
show: false,
|
||||
}),
|
||||
);
|
||||
@@ -602,7 +604,7 @@ export function createStage({ scene, requestRender, emit }) {
|
||||
lines.add({
|
||||
positions: [...corners, corners[0]],
|
||||
width: c.firstEyes ? 2 : 1.2,
|
||||
material: edgeMat,
|
||||
material: edgeMat(),
|
||||
show: false,
|
||||
}),
|
||||
);
|
||||
@@ -631,7 +633,6 @@ export function createStage({ scene, requestRender, emit }) {
|
||||
c,
|
||||
fill,
|
||||
edges,
|
||||
edgeMat,
|
||||
pin,
|
||||
flash,
|
||||
origin,
|
||||
|
||||
Reference in new issue
Block a user