mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 20:04:00 +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/bikeshare.js",
|
||||
"src/app/layers/cctv.js",
|
||||
"src/app/layers/cyclones.js",
|
||||
"src/app/layers/earthquakes.js",
|
||||
"src/app/layers/firms.js",
|
||||
"src/app/layers/flights.js",
|
||||
@@ -2075,6 +2076,7 @@
|
||||
"src/app/layers/alprCameras.js",
|
||||
"src/app/layers/bikeshare.js",
|
||||
"src/app/layers/cctv.js",
|
||||
"src/app/layers/cyclones.js",
|
||||
"src/app/layers/earthquakes.js",
|
||||
"src/app/layers/firms.js",
|
||||
"src/app/layers/flights.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createWeatherClock } from '../layers/weather/clock.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 { createLayerCatalog } from './catalog.js';
|
||||
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
||||
@@ -181,7 +181,7 @@ export function createApplicationCatalog({
|
||||
id: 'weather-lightning',
|
||||
clock: weatherClock,
|
||||
}),
|
||||
createCyclonesLayer({ feed: sources.cyclones }),
|
||||
createApplicationCyclones({ feed: sources.cyclones }),
|
||||
...createInfrastructureLayers(localGeoJsonServices),
|
||||
createApplicationCables({ source: sources.cables }),
|
||||
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,
|
||||
hitTestOverlay = hitTestWorldOverlay,
|
||||
overlayHost,
|
||||
context = null,
|
||||
cesium = Cesium,
|
||||
createRendering = createCycloneRendering,
|
||||
matchMedia = globalThis.matchMedia?.bind(globalThis),
|
||||
@@ -52,6 +53,8 @@ export function createCyclonesLayer({
|
||||
selectedId = null,
|
||||
selectionIntent = 'auto',
|
||||
navigationGeneration = 0,
|
||||
selectedContextStormId = null,
|
||||
selectedContextEntity = null,
|
||||
clickHandler = null,
|
||||
removeClickCapture = null;
|
||||
let enabled = false,
|
||||
@@ -62,6 +65,46 @@ export function createCyclonesLayer({
|
||||
const notify = () => listener?.();
|
||||
const selected = () =>
|
||||
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) {
|
||||
if (selectedId !== id) ++navigationGeneration;
|
||||
selectedId = id;
|
||||
@@ -206,6 +249,7 @@ export function createCyclonesLayer({
|
||||
selectedId = null;
|
||||
selectionIntent = 'auto';
|
||||
++navigationGeneration;
|
||||
clearSelectedContext();
|
||||
rendering?.clear();
|
||||
},
|
||||
async update(_viewer, { signal } = {}) {
|
||||
@@ -229,6 +273,7 @@ export function createCyclonesLayer({
|
||||
snapshot = next;
|
||||
selectedId = null;
|
||||
++navigationGeneration;
|
||||
clearSelectedContext();
|
||||
error = next.reason || 'Cyclone advisories unavailable';
|
||||
return true;
|
||||
}
|
||||
@@ -251,6 +296,10 @@ export function createCyclonesLayer({
|
||||
: snapshot.storms[0]?.id || null,
|
||||
);
|
||||
else rendering.setSelection(selectedId);
|
||||
if (selectedContextStormId) {
|
||||
if (selectedId === selectedContextStormId) publishSelectedContext();
|
||||
else clearSelectedContext();
|
||||
}
|
||||
return true;
|
||||
} catch (cause) {
|
||||
if (controller.signal.aborted || request !== controller) return false;
|
||||
@@ -260,6 +309,7 @@ export function createCyclonesLayer({
|
||||
snapshot = null;
|
||||
selectedId = null;
|
||||
++navigationGeneration;
|
||||
clearSelectedContext();
|
||||
return true;
|
||||
} finally {
|
||||
signal?.removeEventListener('abort', abort);
|
||||
@@ -275,6 +325,7 @@ export function createCyclonesLayer({
|
||||
if (params.clear === true || params.stormId === null) {
|
||||
selectionIntent = 'cleared';
|
||||
select(null);
|
||||
clearSelectedContext();
|
||||
notify();
|
||||
} else if (
|
||||
typeof params.stormId === 'string' &&
|
||||
@@ -282,6 +333,7 @@ export function createCyclonesLayer({
|
||||
) {
|
||||
selectionIntent = 'user';
|
||||
select(params.stormId);
|
||||
publishSelectedContext({ announce: true });
|
||||
notify();
|
||||
}
|
||||
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(
|
||||
feed = { getSnapshot: async () => snapshot() },
|
||||
{ reducedMotion = false } = {},
|
||||
{ reducedMotion = false, context = null } = {},
|
||||
) {
|
||||
const applied = [],
|
||||
navigation = [],
|
||||
@@ -368,6 +368,7 @@ function harness(
|
||||
};
|
||||
const layer = createCyclonesLayer({
|
||||
feed,
|
||||
context,
|
||||
createRendering: () => rendering,
|
||||
matchMedia: () => ({ matches: reducedMotion }),
|
||||
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 () => {
|
||||
let next = snapshot([storm(), storm('ep162026')]);
|
||||
const h = harness({ getSnapshot: async () => next });
|
||||
|
||||
Reference in new issue
Block a user