mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
A final review of both PDFs found text the earlier checks missed:
- Map cards: fire confidence (high/nominal/low) and age (24h), vessel
types (TOWING, TANKER) and installation classes (BASE, AIRFIELD) showed
in English. They now go through the catalog; the data stays English.
- The wind readout kept "10-14 14:00 UTC" and "29.93°N 97.74°W"; it now
uses the locale's date, decimal separator and hemisphere letters.
- Catalog: one term per concept (label/подпись, basemap/подложка,
PROVIDER CONTROLS, HEARD, zoom in, REGIONAL NEWS), calques and broken
grammar in the scene editor, provider settings and Bhote Koshi notes,
and Spanish wording that read wrong ("restringidas por el proveedor",
"Incluye … no se incluyen", "justo ahora", "Sal").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
964 lines
29 KiB
JavaScript
964 lines
29 KiB
JavaScript
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 { setLocale } from '../i18n/index.js';
|
|
import { SceneDirector } from '../scenes/director.js';
|
|
import { createPlaybackClock } from '../director/clock.js';
|
|
import { createDefaultScenePacks } from '../scenes/packs/defaults.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() {}, removeStaticCredit() {} },
|
|
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 overview pins translate their descriptive titles and keep place names', async (t) => {
|
|
t.after(() => setLocale('en'));
|
|
setLocale('ru');
|
|
const viewer = viewerFixture();
|
|
const overlayHost = overlayHostFixture();
|
|
const layer = createBhoteKoshiLocatorLayer({
|
|
boundaryResolver: async () => null,
|
|
requestRender() {},
|
|
overlayHost,
|
|
now: () => 0,
|
|
...nonRunningAnimation(),
|
|
});
|
|
|
|
layer.setParams({ presentation: BHOTE_KOSHI_FLOOD_PATH_PRESENTATION });
|
|
await layer.enable(viewer);
|
|
const titles = new Map(
|
|
overlayHost._test.publications
|
|
.at(-1)
|
|
.entries.map(({ id, title }) => [id, title]),
|
|
);
|
|
assert.equal(
|
|
titles.get('bhote-koshi-place-immediate-collapse-viewpoint'),
|
|
'01 · ВИД НА ОБРУШЕНИЕ',
|
|
);
|
|
assert.equal(
|
|
titles.get('bhote-koshi-place-gyirong-border-gate'),
|
|
'04 · ПОГРАНПЕРЕХОД',
|
|
);
|
|
assert.equal(
|
|
titles.get('bhote-koshi-place-dandagaun-viewpoint'),
|
|
'11 · СМОТРОВАЯ ПЛОЩАДКА DANDAGAUN',
|
|
);
|
|
assert.equal(
|
|
titles.get('bhote-koshi-place-bidur-trishuli-bridge'),
|
|
'14 · МОСТ BIDUR',
|
|
);
|
|
assert.equal(titles.get('bhote-koshi-place-timure-cluster'), '05 · TIMURE');
|
|
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 = {
|
|
_clock: createPlaybackClock({
|
|
isRunning: () => false,
|
|
timingForShot: () => null,
|
|
onProgress() {},
|
|
}),
|
|
_scenePacks: createDefaultScenePacks(),
|
|
dataManager: { layers: new Map([[layer.id, { module: layer }]]) },
|
|
viewer,
|
|
_sceneSeekGeneration: 0,
|
|
_loadGeneration: 0,
|
|
_cancelActiveSceneTravel:
|
|
SceneDirector.prototype._cancelActiveSceneTravel,
|
|
_setSceneMediaPlayback: SceneDirector.prototype._setSceneMediaPlayback,
|
|
};
|
|
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);
|
|
});
|