mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
feat(cyclones): publish explicit selection to shared context
This commit is contained in:
1 parent
e7707d9a0f
commit
28f3243090
5 files changed
+138
-3
No files matched your search
@@ -1340,6 +1340,7 @@
|
|||||||
"src/app/layers/alprCameras.js",
|
"src/app/layers/alprCameras.js",
|
||||||
"src/app/layers/bikeshare.js",
|
"src/app/layers/bikeshare.js",
|
||||||
"src/app/layers/cctv.js",
|
"src/app/layers/cctv.js",
|
||||||
|
"src/app/layers/cyclones.js",
|
||||||
"src/app/layers/earthquakes.js",
|
"src/app/layers/earthquakes.js",
|
||||||
"src/app/layers/firms.js",
|
"src/app/layers/firms.js",
|
||||||
"src/app/layers/flights.js",
|
"src/app/layers/flights.js",
|
||||||
@@ -2075,6 +2076,7 @@
|
|||||||
"src/app/layers/alprCameras.js",
|
"src/app/layers/alprCameras.js",
|
||||||
"src/app/layers/bikeshare.js",
|
"src/app/layers/bikeshare.js",
|
||||||
"src/app/layers/cctv.js",
|
"src/app/layers/cctv.js",
|
||||||
|
"src/app/layers/cyclones.js",
|
||||||
"src/app/layers/earthquakes.js",
|
"src/app/layers/earthquakes.js",
|
||||||
"src/app/layers/firms.js",
|
"src/app/layers/firms.js",
|
||||||
"src/app/layers/flights.js",
|
"src/app/layers/flights.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createWeatherClock } from '../layers/weather/clock.js';
|
import { createWeatherClock } from '../layers/weather/clock.js';
|
||||||
import { createWeatherLayer } from '../layers/weather/index.js';
|
import { createWeatherLayer } from '../layers/weather/index.js';
|
||||||
import { createCyclonesLayer } from '../layers/cyclones/index.js';
|
import { createApplicationCyclones } from './layers/cyclones.js';
|
||||||
import { createWindLayer } from '../layers/wind/index.js';
|
import { createWindLayer } from '../layers/wind/index.js';
|
||||||
import { createLayerCatalog } from './catalog.js';
|
import { createLayerCatalog } from './catalog.js';
|
||||||
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
||||||
@@ -181,7 +181,7 @@ export function createApplicationCatalog({
|
|||||||
id: 'weather-lightning',
|
id: 'weather-lightning',
|
||||||
clock: weatherClock,
|
clock: weatherClock,
|
||||||
}),
|
}),
|
||||||
createCyclonesLayer({ feed: sources.cyclones }),
|
createApplicationCyclones({ feed: sources.cyclones }),
|
||||||
...createInfrastructureLayers(localGeoJsonServices),
|
...createInfrastructureLayers(localGeoJsonServices),
|
||||||
createApplicationCables({ source: sources.cables }),
|
createApplicationCables({ source: sources.cables }),
|
||||||
createApplicationFirms({
|
createApplicationFirms({
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createCyclonesLayer } from '../../layers/cyclones/index.js';
|
||||||
|
import * as context from '../../data/contextStore.js';
|
||||||
|
|
||||||
|
/** Wire cyclone selection into the application shared-context owner. */
|
||||||
|
export function createApplicationCyclones(options) {
|
||||||
|
const browserContext = typeof window !== 'undefined' ? { context } : {};
|
||||||
|
return createCyclonesLayer({
|
||||||
|
...browserContext,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ export function createCyclonesLayer({
|
|||||||
feed,
|
feed,
|
||||||
hitTestOverlay = hitTestWorldOverlay,
|
hitTestOverlay = hitTestWorldOverlay,
|
||||||
overlayHost,
|
overlayHost,
|
||||||
|
context = null,
|
||||||
cesium = Cesium,
|
cesium = Cesium,
|
||||||
createRendering = createCycloneRendering,
|
createRendering = createCycloneRendering,
|
||||||
matchMedia = globalThis.matchMedia?.bind(globalThis),
|
matchMedia = globalThis.matchMedia?.bind(globalThis),
|
||||||
@@ -52,6 +53,8 @@ export function createCyclonesLayer({
|
|||||||
selectedId = null,
|
selectedId = null,
|
||||||
selectionIntent = 'auto',
|
selectionIntent = 'auto',
|
||||||
navigationGeneration = 0,
|
navigationGeneration = 0,
|
||||||
|
selectedContextStormId = null,
|
||||||
|
selectedContextEntity = null,
|
||||||
clickHandler = null,
|
clickHandler = null,
|
||||||
removeClickCapture = null;
|
removeClickCapture = null;
|
||||||
let enabled = false,
|
let enabled = false,
|
||||||
@@ -62,6 +65,46 @@ export function createCyclonesLayer({
|
|||||||
const notify = () => listener?.();
|
const notify = () => listener?.();
|
||||||
const selected = () =>
|
const selected = () =>
|
||||||
snapshot?.storms.find((storm) => storm.id === selectedId) || null;
|
snapshot?.storms.find((storm) => storm.id === selectedId) || null;
|
||||||
|
|
||||||
|
function clearSelectedContext() {
|
||||||
|
context?.removeEntityContextsForLayer?.('weather-cyclones');
|
||||||
|
selectedContextStormId = null;
|
||||||
|
selectedContextEntity = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishSelectedContext({ announce = false } = {}) {
|
||||||
|
const storm = selected();
|
||||||
|
if (!storm || !context) {
|
||||||
|
if (!storm) clearSelectedContext();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
selectedContextEntity ||= { show: true };
|
||||||
|
const record = context.registerEntityContext?.(selectedContextEntity, {
|
||||||
|
id: `cyclone:${storm.id}`,
|
||||||
|
layerId: 'weather-cyclones',
|
||||||
|
layerName: 'Cyclone advisories',
|
||||||
|
source: 'NOAA NHC / CPHC',
|
||||||
|
label: `Cyclone · ${storm.name}`,
|
||||||
|
latitude: storm.position.latitude,
|
||||||
|
longitude: storm.position.longitude,
|
||||||
|
properties: {
|
||||||
|
classification: storm.classification,
|
||||||
|
basin: storm.basin,
|
||||||
|
advisoryNumber: storm.advisoryNumber,
|
||||||
|
issuedAt: storm.issuedAt,
|
||||||
|
positionAt: storm.positionAt,
|
||||||
|
windKt: storm.windKt,
|
||||||
|
pressureHpa: storm.pressureHpa,
|
||||||
|
movement: storm.movement,
|
||||||
|
advisoryUrl: storm.advisoryUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!record) return null;
|
||||||
|
selectedContextStormId = storm.id;
|
||||||
|
if (announce) context.selectEntityContext?.(selectedContextEntity);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
function select(id) {
|
function select(id) {
|
||||||
if (selectedId !== id) ++navigationGeneration;
|
if (selectedId !== id) ++navigationGeneration;
|
||||||
selectedId = id;
|
selectedId = id;
|
||||||
@@ -206,6 +249,7 @@ export function createCyclonesLayer({
|
|||||||
selectedId = null;
|
selectedId = null;
|
||||||
selectionIntent = 'auto';
|
selectionIntent = 'auto';
|
||||||
++navigationGeneration;
|
++navigationGeneration;
|
||||||
|
clearSelectedContext();
|
||||||
rendering?.clear();
|
rendering?.clear();
|
||||||
},
|
},
|
||||||
async update(_viewer, { signal } = {}) {
|
async update(_viewer, { signal } = {}) {
|
||||||
@@ -229,6 +273,7 @@ export function createCyclonesLayer({
|
|||||||
snapshot = next;
|
snapshot = next;
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
++navigationGeneration;
|
++navigationGeneration;
|
||||||
|
clearSelectedContext();
|
||||||
error = next.reason || 'Cyclone advisories unavailable';
|
error = next.reason || 'Cyclone advisories unavailable';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -251,6 +296,10 @@ export function createCyclonesLayer({
|
|||||||
: snapshot.storms[0]?.id || null,
|
: snapshot.storms[0]?.id || null,
|
||||||
);
|
);
|
||||||
else rendering.setSelection(selectedId);
|
else rendering.setSelection(selectedId);
|
||||||
|
if (selectedContextStormId) {
|
||||||
|
if (selectedId === selectedContextStormId) publishSelectedContext();
|
||||||
|
else clearSelectedContext();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (controller.signal.aborted || request !== controller) return false;
|
if (controller.signal.aborted || request !== controller) return false;
|
||||||
@@ -260,6 +309,7 @@ export function createCyclonesLayer({
|
|||||||
snapshot = null;
|
snapshot = null;
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
++navigationGeneration;
|
++navigationGeneration;
|
||||||
|
clearSelectedContext();
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
signal?.removeEventListener('abort', abort);
|
signal?.removeEventListener('abort', abort);
|
||||||
@@ -275,6 +325,7 @@ export function createCyclonesLayer({
|
|||||||
if (params.clear === true || params.stormId === null) {
|
if (params.clear === true || params.stormId === null) {
|
||||||
selectionIntent = 'cleared';
|
selectionIntent = 'cleared';
|
||||||
select(null);
|
select(null);
|
||||||
|
clearSelectedContext();
|
||||||
notify();
|
notify();
|
||||||
} else if (
|
} else if (
|
||||||
typeof params.stormId === 'string' &&
|
typeof params.stormId === 'string' &&
|
||||||
@@ -282,6 +333,7 @@ export function createCyclonesLayer({
|
|||||||
) {
|
) {
|
||||||
selectionIntent = 'user';
|
selectionIntent = 'user';
|
||||||
select(params.stormId);
|
select(params.stormId);
|
||||||
|
publishSelectedContext({ announce: true });
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
const storm = selected();
|
const storm = selected();
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ test('a click on a storm card or lead-hour label selects that storm without pick
|
|||||||
});
|
});
|
||||||
function harness(
|
function harness(
|
||||||
feed = { getSnapshot: async () => snapshot() },
|
feed = { getSnapshot: async () => snapshot() },
|
||||||
{ reducedMotion = false } = {},
|
{ reducedMotion = false, context = null } = {},
|
||||||
) {
|
) {
|
||||||
const applied = [],
|
const applied = [],
|
||||||
navigation = [],
|
navigation = [],
|
||||||
@@ -368,6 +368,7 @@ function harness(
|
|||||||
};
|
};
|
||||||
const layer = createCyclonesLayer({
|
const layer = createCyclonesLayer({
|
||||||
feed,
|
feed,
|
||||||
|
context,
|
||||||
createRendering: () => rendering,
|
createRendering: () => rendering,
|
||||||
matchMedia: () => ({ matches: reducedMotion }),
|
matchMedia: () => ({ matches: reducedMotion }),
|
||||||
openLink: (url) => opened.push(url),
|
openLink: (url) => opened.push(url),
|
||||||
@@ -401,6 +402,75 @@ function harness(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
test('explicit cyclone selection publishes shared context without auto-claiming it', async () => {
|
||||||
|
let next = snapshot([storm(), storm('ep162026')]);
|
||||||
|
const records = new Map();
|
||||||
|
let selectedContext = null;
|
||||||
|
let selectedEvents = 0;
|
||||||
|
const context = {
|
||||||
|
registerEntityContext(entity, metadata) {
|
||||||
|
entity.__gevContextId = metadata.id;
|
||||||
|
const record = { ...metadata, entity };
|
||||||
|
records.set(metadata.id, record);
|
||||||
|
return record;
|
||||||
|
},
|
||||||
|
selectEntityContext(entity) {
|
||||||
|
selectedContext = records.get(entity.__gevContextId) || null;
|
||||||
|
selectedEvents += 1;
|
||||||
|
return selectedContext;
|
||||||
|
},
|
||||||
|
removeEntityContextsForLayer(layerId) {
|
||||||
|
for (const [id, record] of records) {
|
||||||
|
if (record.layerId === layerId) records.delete(id);
|
||||||
|
}
|
||||||
|
if (selectedContext?.layerId === layerId) selectedContext = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const h = harness({ getSnapshot: async () => next }, { context });
|
||||||
|
h.layer.enable();
|
||||||
|
await h.layer.update();
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
selectedEvents,
|
||||||
|
0,
|
||||||
|
'the automatic first-storm row selection must not claim shared context',
|
||||||
|
);
|
||||||
|
assert.equal(records.size, 0);
|
||||||
|
|
||||||
|
h.layer.setParams({ stormId: 'ep162026' });
|
||||||
|
assert.equal(selectedEvents, 1);
|
||||||
|
assert.equal(selectedContext?.layerId, 'weather-cyclones');
|
||||||
|
assert.equal(selectedContext?.source, 'NOAA NHC / CPHC');
|
||||||
|
assert.equal(selectedContext?.label, 'Cyclone · Another system');
|
||||||
|
assert.equal(selectedContext?.latitude, 15.5);
|
||||||
|
assert.equal(selectedContext?.longitude, -125.8);
|
||||||
|
assert.equal(selectedContext?.properties.advisoryNumber, '10');
|
||||||
|
|
||||||
|
await h.layer.update();
|
||||||
|
assert.equal(
|
||||||
|
selectedEvents,
|
||||||
|
1,
|
||||||
|
'refresh updates the selected record without re-announcing the click',
|
||||||
|
);
|
||||||
|
|
||||||
|
next = snapshot([storm()]);
|
||||||
|
await h.layer.update();
|
||||||
|
assert.equal(
|
||||||
|
selectedContext,
|
||||||
|
null,
|
||||||
|
'a vanished user-selected storm must clear instead of transferring context to the fallback row',
|
||||||
|
);
|
||||||
|
assert.equal(records.size, 0);
|
||||||
|
|
||||||
|
h.layer.setParams({ stormId: 'ep152026' });
|
||||||
|
assert.equal(selectedEvents, 2);
|
||||||
|
h.layer.setParams({ clear: true });
|
||||||
|
assert.equal(selectedContext, null);
|
||||||
|
assert.equal(records.size, 0);
|
||||||
|
|
||||||
|
h.layer.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
test('refresh preserves selection intent, including explicit clears and missing storms', async () => {
|
test('refresh preserves selection intent, including explicit clears and missing storms', async () => {
|
||||||
let next = snapshot([storm(), storm('ep162026')]);
|
let next = snapshot([storm(), storm('ep162026')]);
|
||||||
const h = harness({ getSnapshot: async () => next });
|
const h = harness({ getSnapshot: async () => next });
|
||||||
|
|||||||
Reference in new issue
Block a user