mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat: add Nepal flood incident scene
This commit is contained in:
1 parent
76c20be277
commit
60768ccc0b
48 files changed
+15431
-161
No files matched your search
@@ -0,0 +1,671 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { INCIDENT_OVERVIEW_HOLD_SEC, INCIDENT_OVERVIEW_PLACES } from './bhoteKoshiIncidentPlaces.js';
|
||||
import { BHOTE_KOSHI_FLOOD_PATH } from './bhoteKoshiFloodPath.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { SceneDirector } from '../scenes/director.js';
|
||||
import {
|
||||
anchoredCalloutPresentation,
|
||||
BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION,
|
||||
BHOTE_KOSHI_FLOOD_PATH_PRESENTATION,
|
||||
BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION,
|
||||
BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID,
|
||||
BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION,
|
||||
BHOTE_KOSHI_REGIONAL_PRESENTATION,
|
||||
BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION,
|
||||
closeRegionRing,
|
||||
createBhoteKoshiLocatorLayer,
|
||||
largestGeoJsonRing,
|
||||
NEPAL_CONTEXT_PRESENTATION,
|
||||
revealPolylinePositions,
|
||||
} from './bhoteKoshiLocator.js';
|
||||
|
||||
function viewerFixture() {
|
||||
const added = [];
|
||||
const removed = [];
|
||||
return {
|
||||
creditDisplay: { addStaticCredit() {} },
|
||||
dataSources: {
|
||||
add(source) { added.push(source); },
|
||||
remove(source) { removed.push(source); },
|
||||
},
|
||||
_test: { added, removed },
|
||||
};
|
||||
}
|
||||
|
||||
function nonRunningAnimation() {
|
||||
return {
|
||||
scheduleFrame() { return 1; },
|
||||
cancelFrame() {},
|
||||
};
|
||||
}
|
||||
|
||||
function overlayHostFixture() {
|
||||
const publications = [];
|
||||
const clears = [];
|
||||
return {
|
||||
setEntries(sourceId, entries, options) {
|
||||
publications.push({ sourceId, entries, options });
|
||||
},
|
||||
clearSource(sourceId) {
|
||||
clears.push(sourceId);
|
||||
},
|
||||
_test: { publications, clears },
|
||||
};
|
||||
}
|
||||
|
||||
test('Bhote Koshi locator closes valid region rings once', () => {
|
||||
assert.deepEqual(closeRegionRing([[80, 26], [88, 26], [88, 30]]), [
|
||||
[80, 26], [88, 26], [88, 30], [80, 26],
|
||||
]);
|
||||
assert.deepEqual(closeRegionRing([[80, 26], [88, 26], [80, 26]]), [
|
||||
[80, 26], [88, 26], [80, 26],
|
||||
]);
|
||||
assert.deepEqual(closeRegionRing([[80, 26], ['bad', 27]]), []);
|
||||
});
|
||||
|
||||
test('Bhote Koshi locator selects the largest GeoJSON country exterior', () => {
|
||||
const main = [[80, 26], [88, 26], [88, 30], [80, 30], [80, 26]];
|
||||
const island = [[82, 25], [83, 25], [82, 25]];
|
||||
assert.equal(largestGeoJsonRing({ type: 'Polygon', coordinates: [main] }), main);
|
||||
assert.equal(largestGeoJsonRing({
|
||||
type: 'MultiPolygon',
|
||||
coordinates: [[island], [main]],
|
||||
}), main);
|
||||
assert.deepEqual(largestGeoJsonRing({ type: 'Point', coordinates: [84, 28] }), []);
|
||||
});
|
||||
|
||||
test('Bhote Koshi locator reveals a border progressively', () => {
|
||||
const positions = [
|
||||
new Cesium.Cartesian3(0, 0, 0),
|
||||
new Cesium.Cartesian3(10, 0, 0),
|
||||
new Cesium.Cartesian3(20, 0, 0),
|
||||
];
|
||||
assert.deepEqual(revealPolylinePositions(positions, 0), [positions[0], positions[0]]);
|
||||
assert.deepEqual(revealPolylinePositions(positions, 0.25), [
|
||||
positions[0],
|
||||
new Cesium.Cartesian3(5, 0, 0),
|
||||
]);
|
||||
assert.equal(revealPolylinePositions(positions, 1), positions);
|
||||
});
|
||||
|
||||
test('Bhote Koshi locator stages its anchored callout from dot to leader to text', () => {
|
||||
assert.deepEqual(anchoredCalloutPresentation(0), {
|
||||
alpha: 0,
|
||||
leaderProgress: 0,
|
||||
contentAlpha: 0,
|
||||
scale: 0.96,
|
||||
});
|
||||
assert.deepEqual(anchoredCalloutPresentation(1200), {
|
||||
alpha: 1,
|
||||
leaderProgress: 1,
|
||||
contentAlpha: 1,
|
||||
scale: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('Bhote Koshi regional locator sequences Nepal, border, then the incident', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
const renders = [];
|
||||
let nowMs = 0;
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => ({
|
||||
name: 'Nepal',
|
||||
ring: [[80, 26], [88, 26], [88, 30], [80, 30]],
|
||||
}),
|
||||
requestRender: (reason) => renders.push(reason),
|
||||
overlayHost,
|
||||
now: () => nowMs,
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
|
||||
assert.equal(await layer.init(viewer), true);
|
||||
assert.equal(await layer.enable(viewer, { origin: 'scene' }), true);
|
||||
assert.equal(viewer._test.added.length, 1);
|
||||
const source = viewer._test.added[0];
|
||||
const ids = source.entities.values.map((entity) => entity.id);
|
||||
for (const id of [
|
||||
'nepal-context-halo',
|
||||
'nepal-border-fill',
|
||||
'nepal-border-highlight',
|
||||
'bhote-koshi-incident-halo',
|
||||
]) {
|
||||
assert.ok(ids.includes(id), `${id} belongs to the regional sequence`);
|
||||
}
|
||||
const incidentHalo = source.entities.getById('bhote-koshi-incident-halo');
|
||||
const border = source.entities.getById('nepal-border-highlight');
|
||||
const publication = overlayHost._test.publications.at(-1);
|
||||
assert.equal(publication.sourceId, BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID);
|
||||
assert.equal(publication.entries.length, 2);
|
||||
const nepal = publication.entries.find(({ id }) => id === 'nepal-context-callout');
|
||||
const incident = publication.entries.find(({ id }) => id === 'bhote-koshi-incident-callout');
|
||||
assert.equal(nepal.variant, 'label');
|
||||
assert.equal(nepal.title, 'NEPAL');
|
||||
assert.equal(nepal.anchorDot, true);
|
||||
assert.equal(incident.variant, 'card');
|
||||
assert.equal(incident.paintLane, 'thumbnail');
|
||||
assert.equal(incident.title, 'BHOTE KOSHI FLOOD INCIDENT');
|
||||
assert.deepEqual(incident.details, ['NEPAL · 26 AUG 2026']);
|
||||
assert.equal(incident.anchorDot, true);
|
||||
assert.equal(incident.leaderStyle, 'elbow');
|
||||
assert.equal('image' in incident, false);
|
||||
assert.equal('requireImage' in incident, false);
|
||||
assert.equal(incident.sourceAlpha(), 0);
|
||||
assert.equal(incident.leaderProgress(), 0);
|
||||
assert.equal(incident.contentAlpha(), 0);
|
||||
assert.equal(incident.presentationScale(), 0.96);
|
||||
assert.equal(nepal.sourceAlpha(), 0);
|
||||
assert.ok(border.polyline.positions.getValue().length < 5);
|
||||
assert.equal(incidentHalo.show.getValue(), false);
|
||||
assert.equal(incidentHalo.point.pixelSize.getValue(), 16);
|
||||
nowMs = 3100;
|
||||
assert.equal(nepal.sourceAlpha(), 1);
|
||||
assert.equal(border.polyline.positions.getValue().length, 5);
|
||||
assert.equal(incidentHalo.show.getValue(), false);
|
||||
nowMs = 3300;
|
||||
assert.equal(incidentHalo.show.getValue(), true);
|
||||
assert.equal(incident.sourceAlpha(), 0);
|
||||
nowMs = 4500;
|
||||
assert.equal(incident.sourceAlpha(), 1);
|
||||
assert.equal(incident.leaderProgress(), 1);
|
||||
assert.equal(incident.contentAlpha(), 1);
|
||||
assert.equal(incident.presentationScale(), 1);
|
||||
assert.equal(layer.getStats().status, 'nominal');
|
||||
assert.deepEqual(layer.getParams(), { presentation: BHOTE_KOSHI_REGIONAL_PRESENTATION });
|
||||
assert.ok(renders.includes('bhote-koshi-locator-boundary'));
|
||||
|
||||
assert.equal(await layer.disable(), true);
|
||||
assert.equal(viewer._test.removed.length, 1);
|
||||
assert.equal(overlayHost._test.clears.at(-1), BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID);
|
||||
assert.equal(layer.getStats().status, 'idle');
|
||||
});
|
||||
|
||||
test('Bhote Koshi locator switches between Nepal context and the regional incident treatment', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => ({
|
||||
name: 'Nepal',
|
||||
ring: [[80, 26], [88, 26], [88, 30], [80, 30]],
|
||||
}),
|
||||
requestRender() {},
|
||||
overlayHost,
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
|
||||
layer.setParams({ presentation: NEPAL_CONTEXT_PRESENTATION });
|
||||
await layer.enable(viewer);
|
||||
assert.deepEqual(viewer._test.added[0].entities.values.map((entity) => entity.id), [
|
||||
'nepal-context-halo',
|
||||
]);
|
||||
const nepalLabel = overlayHost._test.publications.at(-1).entries[0];
|
||||
assert.equal(nepalLabel.variant, 'label');
|
||||
assert.equal(nepalLabel.placement, 'right');
|
||||
assert.equal(nepalLabel.anchorDot, true);
|
||||
assert.equal(layer.getStats().coverage, 'Nepal context');
|
||||
assert.deepEqual(layer.getRowControls().chips.map(({ id, active }) => ({ id, active })), [
|
||||
{ id: 'nepal-context', active: true },
|
||||
{ id: 'bhote-koshi-regional', active: false },
|
||||
{ id: 'bhote-koshi-city-context', active: false },
|
||||
{ id: 'bhote-koshi-incident-places', active: false },
|
||||
{ id: 'bhote-koshi-flood-path', active: false },
|
||||
{ id: 'bhote-koshi-path-overview', active: false },
|
||||
{ id: 'bhote-koshi-trigger-record', active: false },
|
||||
]);
|
||||
|
||||
layer.setParams({ presentation: BHOTE_KOSHI_REGIONAL_PRESENTATION });
|
||||
assert.ok(viewer._test.added[0].entities.getById('nepal-border-highlight'));
|
||||
assert.equal(viewer._test.added[0].entities.getById('bhote-koshi-city-kathmandu'), undefined);
|
||||
assert.equal(
|
||||
overlayHost._test.publications.at(-1).entries
|
||||
.find(({ id }) => id === 'bhote-koshi-incident-callout').variant,
|
||||
'card',
|
||||
);
|
||||
|
||||
layer.setParams({ presentation: NEPAL_CONTEXT_PRESENTATION });
|
||||
assert.deepEqual(viewer._test.added[0].entities.values.map((entity) => entity.id), [
|
||||
'nepal-context-halo',
|
||||
]);
|
||||
await layer.destroy();
|
||||
});
|
||||
|
||||
test('Incident Corridor reveals all fifteen overview pins and keeps layout active through the sequence', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
let nowMs = 0;
|
||||
let pulseFrame = null;
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => null, requestRender() {},
|
||||
overlayHost,
|
||||
now: () => nowMs,
|
||||
scheduleFrame(callback) { pulseFrame = callback; return 1; },
|
||||
cancelFrame() {},
|
||||
});
|
||||
layer.setParams({ presentation: BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION });
|
||||
await layer.enable(viewer);
|
||||
const source = viewer._test.added[0];
|
||||
const event = JSON.parse(await readFile(new URL('../../public/events/bhote-koshi-2026/event.json', import.meta.url), 'utf8'));
|
||||
assert.equal(event.evidenceSpine.length, 16);
|
||||
assert.equal(event.evidenceSpine.at(-1).id, 'charaudi');
|
||||
assert.equal(source.entities.values.length, 15);
|
||||
const labels = overlayHost._test.publications.at(-1).entries;
|
||||
assert.equal(labels.length, 15);
|
||||
assert.equal(overlayHost._test.publications.at(-1).options.moving, true);
|
||||
for (const [index, point] of INCIDENT_OVERVIEW_PLACES.entries()) {
|
||||
const id = `bhote-koshi-place-${point.id}`;
|
||||
const pin = labels.find((entry) => entry.id === id);
|
||||
assert.ok(pin, point.id);
|
||||
assert.equal(pin.title, `${String(index + 1).padStart(2, '0')} · ${point.title}`);
|
||||
assert.equal(pin.variant, 'label');
|
||||
const location = Cesium.Cartographic.fromCartesian(pin.position);
|
||||
assert.ok(Math.abs(Cesium.Math.toDegrees(location.latitude) - point.lat) < 1e-6);
|
||||
assert.ok(Math.abs(Cesium.Math.toDegrees(location.longitude) - point.lon) < 1e-6);
|
||||
nowMs = 349 + index * 650;
|
||||
assert.equal(pin.sourceAlpha(), 0);
|
||||
nowMs += 1;
|
||||
assert.equal(pin.sourceAlpha(), 0);
|
||||
assert.equal(source.entities.values.filter(entity => entity.id.endsWith('-halo') && entity.show.getValue()).length, 1);
|
||||
nowMs += 520;
|
||||
assert.equal(pin.sourceAlpha(), 1);
|
||||
}
|
||||
assert.ok(nowMs < INCIDENT_OVERVIEW_HOLD_SEC * 1000);
|
||||
// A repeat halo cycle must not erase already revealed labels.
|
||||
nowMs = 350 + 15 * 650;
|
||||
assert.equal(source.entities.getById('bhote-koshi-place-immediate-collapse-viewpoint-halo').show.getValue(), true);
|
||||
assert.equal(labels.find(({ id }) => id === 'bhote-koshi-place-devighat-taadi-khola-bridge').sourceAlpha(), 1);
|
||||
assert.equal(labels.some(({ id }) => id === 'bhote-koshi-place-charaudi'), false);
|
||||
assert.equal(typeof pulseFrame, 'function');
|
||||
pulseFrame();
|
||||
assert.equal(overlayHost._test.publications.at(-1).options.moving, false);
|
||||
layer.setParams({ presentation: BHOTE_KOSHI_REGIONAL_PRESENTATION });
|
||||
assert.equal(source.entities.values.some(entity => entity.id.startsWith('bhote-koshi-place-')), false);
|
||||
await layer.destroy();
|
||||
});
|
||||
|
||||
test('Bhote Koshi flood path draws a solid cyan sourced route after the place shot', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
let nowMs = 0;
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => null,
|
||||
requestRender() {},
|
||||
overlayHost,
|
||||
now: () => nowMs,
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
|
||||
layer.setParams({ presentation: BHOTE_KOSHI_FLOOD_PATH_PRESENTATION });
|
||||
await layer.enable(viewer);
|
||||
const source = viewer._test.added[0];
|
||||
const path = source.entities.getById('bhote-koshi-flood-path');
|
||||
const labels = overlayHost._test.publications.at(-1).entries;
|
||||
const trigger = labels.find(({ id }) => id === 'bhote-koshi-place-immediate-collapse-viewpoint');
|
||||
const bidur = labels.find(({ id }) => id === 'bhote-koshi-place-bidur-trishuli-bridge');
|
||||
|
||||
assert.equal(source.entities.values.length, 1);
|
||||
assert.equal(labels.length, 15);
|
||||
assert.equal(bidur.title, '14 · BIDUR BRIDGE');
|
||||
for (const place of INCIDENT_OVERVIEW_PLACES) {
|
||||
const pin = labels.find(({ id }) => id === `bhote-koshi-place-${place.id}`);
|
||||
assert.equal(pin.sourceAlpha(), 1, place.id);
|
||||
}
|
||||
assert.equal(path.polyline.clampToGround.getValue(), true);
|
||||
assert.ok(path.polyline.material instanceof Cesium.ColorMaterialProperty);
|
||||
assert.ok(path.polyline.material.color.getValue().equalsEpsilon(
|
||||
Cesium.Color.fromCssColorString('#20e7f2').withAlpha(0.96),
|
||||
Cesium.Math.EPSILON7,
|
||||
));
|
||||
assert.equal(path.show.getValue(), false);
|
||||
assert.equal(trigger.sourceAlpha(), 1);
|
||||
assert.equal(bidur.sourceAlpha(), 1);
|
||||
|
||||
nowMs = 250;
|
||||
assert.equal(path.show.getValue(), true);
|
||||
assert.equal(path.polyline.positions.getValue().length, 2);
|
||||
nowMs = 2250;
|
||||
const halfwayCount = path.polyline.positions.getValue().length;
|
||||
assert.ok(Math.abs(halfwayCount - BHOTE_KOSHI_FLOOD_PATH.length / 2) <= 2);
|
||||
nowMs = 4250;
|
||||
assert.equal(path.polyline.positions.getValue().length, BHOTE_KOSHI_FLOOD_PATH.length);
|
||||
assert.deepEqual(BHOTE_KOSHI_FLOOD_PATH[0], [85.48476, 28.33255]);
|
||||
// More observation pins must not extend or straighten the sourced route.
|
||||
assert.deepEqual(path.polyline.positions.getValue(), BHOTE_KOSHI_FLOOD_PATH.map(([lon, lat]) => (
|
||||
Cesium.Cartesian3.fromDegrees(lon, lat)
|
||||
)));
|
||||
assert.deepEqual(BHOTE_KOSHI_FLOOD_PATH.at(-1), [85.14943, 27.92177]);
|
||||
assert.equal(layer.getStats().coverage, 'Bhote Koshi · Flood path');
|
||||
assert.deepEqual(layer.getParams(), { presentation: BHOTE_KOSHI_FLOOD_PATH_PRESENTATION });
|
||||
await layer.destroy();
|
||||
});
|
||||
|
||||
test('Bhote Koshi path-overview presentation keeps the route settled without Nepal border', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
let nowMs = 0;
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => ({
|
||||
name: 'Nepal',
|
||||
ring: [[80, 26], [88, 26], [88, 30], [80, 30]],
|
||||
}),
|
||||
requestRender() {},
|
||||
overlayHost,
|
||||
now: () => nowMs,
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
|
||||
layer.setParams({ presentation: BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION });
|
||||
await layer.enable(viewer);
|
||||
const source = viewer._test.added[0];
|
||||
const path = source.entities.getById('bhote-koshi-flood-path');
|
||||
const labels = overlayHost._test.publications.at(-1).entries;
|
||||
|
||||
assert.equal(source.entities.values.length, 1);
|
||||
assert.equal(labels.length, 15);
|
||||
for (const [index, place] of INCIDENT_OVERVIEW_PLACES.entries()) {
|
||||
const pin = labels.find(({ id }) => id === `bhote-koshi-place-${place.id}`);
|
||||
assert.equal(pin.title, `${String(index + 1).padStart(2, '0')} · ${place.title}`);
|
||||
assert.equal(pin.sourceAlpha(), 1);
|
||||
}
|
||||
assert.equal(path.show.getValue(), true);
|
||||
assert.equal(path.polyline.positions.getValue().length, BHOTE_KOSHI_FLOOD_PATH.length);
|
||||
assert.ok(Cesium.Cartesian3.equals(path.polyline.positions.getValue()[0],
|
||||
Cesium.Cartesian3.fromDegrees(85.48476, 28.33255)));
|
||||
assert.equal(source.entities.getById('nepal-border-fill'), undefined);
|
||||
assert.equal(source.entities.getById('nepal-border-highlight'), undefined);
|
||||
assert.equal(
|
||||
source.entities.getById('bhote-koshi-place-immediate-collapse-viewpoint-halo'),
|
||||
undefined,
|
||||
);
|
||||
assert.equal(layer.getStats().coverage, 'Bhote Koshi · Path overview');
|
||||
assert.deepEqual(layer.getParams(), { presentation: BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION });
|
||||
await layer.destroy();
|
||||
});
|
||||
|
||||
test('Bhote Koshi trigger-record presentation reveals sourced incident details', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
let nowMs = 0;
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => null,
|
||||
requestRender() {},
|
||||
overlayHost,
|
||||
now: () => nowMs,
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
|
||||
layer.setParams({
|
||||
presentation: BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION,
|
||||
preview: true,
|
||||
});
|
||||
await layer.enable(viewer);
|
||||
const source = viewer._test.added[0];
|
||||
const publication = overlayHost._test.publications.at(-1);
|
||||
const record = publication.entries[0];
|
||||
|
||||
assert.deepEqual(source.entities.values.map(({ id }) => id), [
|
||||
'bhote-koshi-flood-path',
|
||||
'bhote-koshi-trigger-record-halo',
|
||||
]);
|
||||
assert.equal(record.title, 'USGS INITIAL REPORT');
|
||||
assert.deepEqual(record.details, [
|
||||
'26 AUG 2026 · 08:37 NPT',
|
||||
'28.2710° N · 85.5150° E',
|
||||
'INITIAL M4.4 · RECLASSIFIED M5.2 LANDSLIDE',
|
||||
'USGS EVENT us7000tbwb · OPEN SOURCE',
|
||||
]);
|
||||
assert.equal(record.interactive, true);
|
||||
assert.equal(typeof record.activate, 'function');
|
||||
assert.equal(record.sourceAlpha(), 0);
|
||||
nowMs = 1200;
|
||||
assert.equal(record.sourceAlpha(), 1);
|
||||
assert.equal(source.entities.getById('nepal-border-highlight'), undefined);
|
||||
assert.equal(layer.getStats().coverage, 'Bhote Koshi · Trigger record');
|
||||
assert.deepEqual(layer.getParams(), {
|
||||
presentation: BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION,
|
||||
});
|
||||
await layer.destroy();
|
||||
});
|
||||
|
||||
test('Bhote Koshi trigger-record waits for the scene camera, zooms, then orbits', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
const moveStart = new Cesium.Event();
|
||||
const moveEnd = new Cesium.Event();
|
||||
const flights = [];
|
||||
let armedCallback = null;
|
||||
viewer.camera = {
|
||||
moveStart,
|
||||
moveEnd,
|
||||
flyTo(options) { flights.push(options); },
|
||||
cancelFlight() {},
|
||||
};
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => null,
|
||||
requestRender() {},
|
||||
overlayHost,
|
||||
scheduleDelay(callback) {
|
||||
armedCallback = callback;
|
||||
return 1;
|
||||
},
|
||||
cancelDelay() {},
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
|
||||
layer.setParams({ presentation: BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION });
|
||||
await layer.enable(viewer);
|
||||
assert.equal(flights.length, 0);
|
||||
assert.equal(typeof armedCallback, 'function');
|
||||
moveStart.raiseEvent();
|
||||
assert.equal(flights.length, 0);
|
||||
moveEnd.raiseEvent();
|
||||
assert.equal(flights.length, 1);
|
||||
assert.equal(flights[0].duration, 4.6);
|
||||
const destination = Cesium.Cartographic.fromCartesian(flights[0].destination);
|
||||
assert.ok(Math.abs(Cesium.Math.toDegrees(destination.latitude) - 28.368) < 1e-6);
|
||||
assert.ok(Math.abs(Cesium.Math.toDegrees(destination.longitude) - 85.473) < 1e-6);
|
||||
assert.ok(Math.abs(destination.height - 10526) < 1e-4);
|
||||
flights[0].complete();
|
||||
assert.equal(flights.length, 2);
|
||||
assert.equal(flights[1].duration, 2.8);
|
||||
const orbit = Cesium.Cartographic.fromCartesian(flights[1].destination);
|
||||
assert.ok(Math.abs(Cesium.Math.toDegrees(orbit.latitude) - 28.3769) < 1e-6);
|
||||
assert.ok(Math.abs(Cesium.Math.toDegrees(orbit.longitude) - 85.4923) < 1e-6);
|
||||
assert.ok(Math.abs(orbit.height - 10526) < 1e-4);
|
||||
await layer.destroy();
|
||||
});
|
||||
|
||||
for (const phase of ['timer', 'move-end', 'approach', 'orbit']) {
|
||||
test(`Scene Stop revokes locator ${phase} and stale callbacks cannot own a successor`, async () => {
|
||||
const viewer = viewerFixture();
|
||||
const timers = new Map();
|
||||
const callbacks = [];
|
||||
const flights = [];
|
||||
let nextTimer = 0;
|
||||
const moveStart = new Cesium.Event();
|
||||
const moveEnd = new Cesium.Event();
|
||||
viewer.camera = {
|
||||
moveStart, moveEnd,
|
||||
flyTo(options) { flights.push(options); },
|
||||
cancelFlight() {},
|
||||
};
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => null,
|
||||
overlayHost: overlayHostFixture(),
|
||||
requestRender() {},
|
||||
scheduleDelay(callback) {
|
||||
callbacks.push(callback);
|
||||
timers.set(++nextTimer, callback);
|
||||
return nextTimer;
|
||||
},
|
||||
cancelDelay(id) { timers.delete(id); },
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
const director = {
|
||||
dataManager: { layers: new Map([[layer.id, { module: layer }]]) },
|
||||
viewer,
|
||||
_sceneSeekGeneration: 0,
|
||||
_loadGeneration: 0,
|
||||
_cancelActiveSceneTravel: SceneDirector.prototype._cancelActiveSceneTravel,
|
||||
};
|
||||
try {
|
||||
layer.setParams({ presentation: BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION });
|
||||
await layer.enable(viewer);
|
||||
if (phase === 'move-end') moveStart.raiseEvent();
|
||||
if (phase === 'approach' || phase === 'orbit') callbacks[0]();
|
||||
if (phase === 'orbit') flights[0].complete();
|
||||
const oldFlights = [...flights];
|
||||
SceneDirector.prototype.stopScene.call(director);
|
||||
assert.equal(timers.size, 0, 'Stop cancels the actual delayed work');
|
||||
assert.equal(moveStart.numberOfListeners, 0);
|
||||
assert.equal(moveEnd.numberOfListeners, 0);
|
||||
callbacks[0]();
|
||||
moveEnd.raiseEvent();
|
||||
for (const flight of oldFlights) flight.complete();
|
||||
assert.equal(flights.length, oldFlights.length, 'no post-Stop approach or orbit');
|
||||
assert.equal(layer.getStats().count, 1, 'Stop preserves the visible locator');
|
||||
|
||||
layer.setParams({ presentation: BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION });
|
||||
callbacks.at(-1)();
|
||||
const successor = flights.at(-1);
|
||||
const count = flights.length;
|
||||
callbacks[0]();
|
||||
for (const flight of oldFlights) { flight.cancel(); flight.complete(); }
|
||||
assert.equal(flights.length, count, 'old callbacks cannot add a flight');
|
||||
successor.complete();
|
||||
assert.equal(flights.length, count + 1, 'successor retains its own orbit');
|
||||
} finally { await layer.destroy(); }
|
||||
});
|
||||
}
|
||||
|
||||
test('Bhote Koshi locator keeps the regional context settled while nearby cities stagger in', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
let nowMs = 0;
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => ({
|
||||
name: 'Nepal',
|
||||
ring: [[80, 26], [88, 26], [88, 30], [80, 30]],
|
||||
}),
|
||||
requestRender() {},
|
||||
overlayHost,
|
||||
now: () => nowMs,
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
|
||||
layer.setParams({ presentation: BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION });
|
||||
await layer.enable(viewer);
|
||||
const source = viewer._test.added[0];
|
||||
const borderFill = source.entities.getById('nepal-border-fill');
|
||||
const entries = overlayHost._test.publications.at(-1).entries;
|
||||
const incident = entries.find(({ id }) => id === 'bhote-koshi-incident-callout');
|
||||
const kathmandu = entries.find(({ id }) => id === 'bhote-koshi-city-kathmandu');
|
||||
const bhaktapur = entries.find(({ id }) => id === 'bhote-koshi-city-bhaktapur');
|
||||
const dhulikhel = entries.find(({ id }) => id === 'bhote-koshi-city-dhulikhel');
|
||||
|
||||
assert.equal(borderFill.show.getValue(), true);
|
||||
assert.equal(incident.contentAlpha(), 1);
|
||||
assert.equal(kathmandu.title, 'KATHMANDU');
|
||||
assert.equal(bhaktapur.title, 'BHAKTAPUR');
|
||||
assert.equal(dhulikhel.title, 'DHULIKHEL');
|
||||
assert.ok([kathmandu, bhaktapur, dhulikhel].every(({ variant, anchorDot }) => (
|
||||
variant === 'label' && anchorDot === true
|
||||
)));
|
||||
assert.equal(kathmandu.sourceAlpha(), 0);
|
||||
assert.equal(bhaktapur.sourceAlpha(), 0);
|
||||
assert.equal(dhulikhel.sourceAlpha(), 0);
|
||||
|
||||
nowMs = 350;
|
||||
assert.equal(kathmandu.sourceAlpha(), 0);
|
||||
assert.equal(bhaktapur.sourceAlpha(), 0);
|
||||
nowMs = 850;
|
||||
assert.equal(kathmandu.sourceAlpha(), 1);
|
||||
assert.ok(bhaktapur.sourceAlpha() > 0);
|
||||
assert.ok(dhulikhel.sourceAlpha() > 0);
|
||||
nowMs = 1300;
|
||||
assert.equal(dhulikhel.sourceAlpha(), 1);
|
||||
assert.equal(layer.getStats().coverage, 'Bhote Koshi · Nearby cities');
|
||||
assert.deepEqual(layer.getParams(), { presentation: BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION });
|
||||
await layer.destroy();
|
||||
});
|
||||
|
||||
test('Bhote Koshi locator degrades to labels when the regional boundary is unavailable', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
const originalWarn = console.warn;
|
||||
console.warn = () => {};
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => null,
|
||||
requestRender() {},
|
||||
overlayHost,
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
try {
|
||||
await layer.enable(viewer);
|
||||
const ids = viewer._test.added[0].entities.values.map((entity) => entity.id);
|
||||
assert.ok(ids.includes('nepal-context-halo'));
|
||||
assert.ok(ids.includes('bhote-koshi-incident-halo'));
|
||||
assert.equal(ids.includes('bhote-koshi-city-kathmandu'), false);
|
||||
assert.equal(ids.includes('nepal-border-highlight'), false);
|
||||
assert.deepEqual(overlayHost._test.publications.at(-1).entries.map(({ id }) => id), [
|
||||
'nepal-context-callout',
|
||||
'bhote-koshi-incident-callout',
|
||||
]);
|
||||
assert.equal(layer.getStats().status, 'degraded');
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
await layer.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('Bhote Koshi regional locator waits for its boundary before starting the full sequence', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const overlayHost = overlayHostFixture();
|
||||
let releaseBoundary;
|
||||
const boundary = new Promise((resolve) => { releaseBoundary = resolve; });
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => boundary,
|
||||
requestRender() {},
|
||||
overlayHost,
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
|
||||
const enabling = layer.enable(viewer);
|
||||
assert.ok(viewer._test.added[0].entities.getById('nepal-context-halo'));
|
||||
assert.equal(viewer._test.added[0].entities.getById('nepal-border-highlight'), undefined);
|
||||
assert.equal(viewer._test.added[0].entities.getById('bhote-koshi-incident-halo'), undefined);
|
||||
assert.equal(overlayHost._test.publications.length, 1);
|
||||
assert.deepEqual(overlayHost._test.publications[0].entries.map(({ id }) => id), [
|
||||
'nepal-context-callout',
|
||||
]);
|
||||
|
||||
releaseBoundary({ ring: [[80, 26], [88, 26], [88, 30], [80, 30]] });
|
||||
await enabling;
|
||||
assert.ok(viewer._test.added[0].entities.getById('bhote-koshi-incident-halo'));
|
||||
assert.ok(viewer._test.added[0].entities.getById('nepal-border-highlight'));
|
||||
assert.equal(viewer._test.added[0].entities.getById('bhote-koshi-city-kathmandu'), undefined);
|
||||
assert.equal(overlayHost._test.publications.length, 2);
|
||||
assert.deepEqual(overlayHost._test.publications.at(-1).entries.map(({ id }) => id), [
|
||||
'nepal-context-callout',
|
||||
'bhote-koshi-incident-callout',
|
||||
]);
|
||||
await layer.destroy();
|
||||
});
|
||||
|
||||
test('Bhote Koshi locator cancels a stale boundary resolution and removes its surface', async () => {
|
||||
const viewer = viewerFixture();
|
||||
const controller = new AbortController();
|
||||
let releaseBoundary;
|
||||
const boundary = new Promise((resolve) => { releaseBoundary = resolve; });
|
||||
const layer = createBhoteKoshiLocatorLayer({
|
||||
boundaryResolver: async () => boundary,
|
||||
requestRender() {},
|
||||
...nonRunningAnimation(),
|
||||
});
|
||||
const enabling = layer.enable(viewer, { signal: controller.signal });
|
||||
controller.abort();
|
||||
releaseBoundary({ ring: [[80, 26], [88, 26], [88, 30]] });
|
||||
await assert.rejects(enabling, (error) => error?.name === 'AbortError');
|
||||
assert.equal(viewer._test.removed.length, 1);
|
||||
});
|
||||
Reference in new issue
Block a user