mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Knip, configured with the application, package exports, scripts, QA harnesses, workers, tools and tests as entry points, reports exports that no module imports. Each was checked by hand against string-based and namespace imports. - Symbols that are still used inside their own module lose only the `export` keyword (176 constants and helpers). - Unused re-exports are removed from barrel modules (flight, military, vessel, CCTV, directions, transit history, local ADS-B and street-level policy modules). - Declarations that nothing uses at all are deleted, among them AIS_WATCHDOG_STATUSES, applyFilter, isRadioCountryCode, compactText, DENSITY_STOPS, the unused CCTV endpoint constants, and the SDR band constants. - Default exports that nothing imports are removed. The ones in src/data/earthquakes.js, src/data/telegeographySubmarineCables.js, src/data/wind.js and src/data/bhoteKoshiLocator.js each constructed a layer instance at import time that nothing used; those instances are no longer created. Every export of a package.json entry stays, as does TOMTOM_CREDIT, which the traffic layer reads from the credits namespace it is given.
1250 lines
41 KiB
JavaScript
1250 lines
41 KiB
JavaScript
import * as Cesium from 'cesium';
|
|
import {
|
|
clearOverlaySource,
|
|
setOverlayEntries,
|
|
} from '../overlays/worldOverlay.js';
|
|
import { governorRequestRender } from '../renderGovernor.js';
|
|
import {
|
|
BHOTE_KOSHI_LOCATOR_CREDIT,
|
|
registerDynamicCredit,
|
|
showOsmCredit,
|
|
hideOsmCredit,
|
|
} from './dataCredits.js';
|
|
import { BHOTE_KOSHI_FLOOD_PATH } from './bhoteKoshiFloodPath.js';
|
|
import { INCIDENT_OVERVIEW_PLACES } from './bhoteKoshiIncidentPlaces.js';
|
|
import { t, msg, getLocale } from '../i18n/index.js';
|
|
|
|
const BHOTE_KOSHI_LOCATOR_LAYER_ID = 'bhote-koshi-locator';
|
|
export const BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID =
|
|
'bhote-koshi-locator-callout';
|
|
export const NEPAL_CONTEXT_PRESENTATION = 'nepal-context';
|
|
export const BHOTE_KOSHI_REGIONAL_PRESENTATION = 'bhote-koshi-regional';
|
|
export const BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION = 'bhote-koshi-city-context';
|
|
export const BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION =
|
|
'bhote-koshi-incident-places';
|
|
export const BHOTE_KOSHI_FLOOD_PATH_PRESENTATION = 'bhote-koshi-flood-path';
|
|
export const BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION =
|
|
'bhote-koshi-path-overview';
|
|
export const BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION =
|
|
'bhote-koshi-trigger-record';
|
|
|
|
const LEGACY_PATH_BORDER_PRESENTATION = 'bhote-koshi-path-border';
|
|
|
|
const BORDER_DRAW_DURATION_MS = 2400;
|
|
const BORDER_START_DELAY_MS = 700;
|
|
const INCIDENT_CALLOUT_GAP_MS = 200;
|
|
const CALLOUT_REVEAL_DURATION_MS = 1200;
|
|
const CALLOUT_PULSE_DURATION_MS = 1800;
|
|
const CALLOUT_RING_MIN_SIZE_PX = 16;
|
|
const CALLOUT_RING_SIZE_RANGE_PX = 14;
|
|
const CITY_CALLOUT_START_DELAY_MS = 350;
|
|
const CITY_CALLOUT_STAGGER_MS = 220;
|
|
const CITY_CALLOUT_REVEAL_MS = 500;
|
|
const PLACE_CALLOUT_START_DELAY_MS = 350;
|
|
const PLACE_CALLOUT_STAGGER_MS = 650;
|
|
const PLACE_CALLOUT_REVEAL_MS = 520;
|
|
const FLOOD_PATH_START_DELAY_MS = 250;
|
|
const FLOOD_PATH_DRAW_DURATION_MS = 4000;
|
|
const TRIGGER_APPROACH_ARM_MS = 500;
|
|
const TRIGGER_APPROACH_DURATION_SEC = 4.6;
|
|
const TRIGGER_ORBIT_DURATION_SEC = 2.8;
|
|
const TRIGGER_SOURCE_URL =
|
|
'https://earthquake.usgs.gov/earthquakes/eventpage/us7000tbwb/executive';
|
|
|
|
// Descriptive pin titles translate at display (same entries as the event
|
|
// panel); place names stay Latin, anything else shows as authored.
|
|
const PLACE_TITLE_TEXT = new Map(
|
|
[
|
|
msg('Collapse viewpoint'),
|
|
msg('Dammed lake'),
|
|
msg('Second landslide'),
|
|
msg('Border gate'),
|
|
msg('Dandagaun viewpoint'),
|
|
msg('Bidur bridge'),
|
|
msg('Devighat bridge'),
|
|
].map((text) => [text.toUpperCase(), text]),
|
|
);
|
|
|
|
function placeTitle(title) {
|
|
const text = PLACE_TITLE_TEXT.get(title);
|
|
return text ? t(text).toLocaleUpperCase(getLocale()) : title;
|
|
}
|
|
|
|
const INCIDENT = Object.freeze({
|
|
lat: 28.3632333,
|
|
lon: 85.4423639,
|
|
heightM: 5200,
|
|
title: msg('BHOTE KOSHI FLOOD INCIDENT'),
|
|
subtitle: msg('NEPAL · 26 AUG 2026'),
|
|
});
|
|
const TRIGGER_RECORD = Object.freeze({
|
|
lat: 28.271,
|
|
lon: 85.515,
|
|
heightM: 4200,
|
|
title: msg('USGS INITIAL REPORT'),
|
|
details: Object.freeze([
|
|
msg('26 AUG 2026 · 08:37 NPT'),
|
|
msg('28.2710° N · 85.5150° E'),
|
|
msg('INITIAL M4.4 · RECLASSIFIED M5.2 LANDSLIDE'),
|
|
msg('USGS EVENT us7000tbwb · OPEN SOURCE'),
|
|
]),
|
|
});
|
|
const TRIGGER_APPROACH_CAMERA = Object.freeze({
|
|
lat: 28.368,
|
|
lon: 85.473,
|
|
alt: 10526,
|
|
heading: 156,
|
|
pitch: -44,
|
|
roll: 0,
|
|
});
|
|
const TRIGGER_ORBIT_CAMERA = Object.freeze({
|
|
lat: 28.3769,
|
|
lon: 85.4923,
|
|
alt: 10526,
|
|
heading: 168,
|
|
pitch: -44,
|
|
roll: 0,
|
|
});
|
|
const NEPAL = Object.freeze({
|
|
lat: 28.3949,
|
|
lon: 84.124,
|
|
heightM: 6800,
|
|
// i18n-ignore: country name, shown as is
|
|
title: 'NEPAL',
|
|
});
|
|
const NEARBY_CITIES = Object.freeze([
|
|
Object.freeze({
|
|
id: 'kathmandu',
|
|
// i18n-ignore: city name
|
|
title: 'KATHMANDU',
|
|
lat: 27.7017,
|
|
lon: 85.3206,
|
|
heightM: 3600,
|
|
placement: 'left',
|
|
}),
|
|
Object.freeze({
|
|
id: 'bhaktapur',
|
|
// i18n-ignore: city name
|
|
title: 'BHAKTAPUR',
|
|
lat: 27.673,
|
|
lon: 85.43,
|
|
heightM: 3400,
|
|
placement: 'right',
|
|
}),
|
|
Object.freeze({
|
|
id: 'dhulikhel',
|
|
// i18n-ignore: city name
|
|
title: 'DHULIKHEL',
|
|
lat: 27.6221,
|
|
lon: 85.5428,
|
|
heightM: 3600,
|
|
placement: 'right-lower',
|
|
}),
|
|
]);
|
|
const BORDER_COLOR = Cesium.Color.fromCssColorString('#20e7f2');
|
|
const BORDER_ACCENT = '#20e7f2';
|
|
const INCIDENT_ACCENT = '#ffad55';
|
|
const INCIDENT_COLOR = Cesium.Color.fromCssColorString(INCIDENT_ACCENT);
|
|
|
|
const DEFAULT_OVERLAY_HOST = Object.freeze({
|
|
setEntries: setOverlayEntries,
|
|
clearSource: clearOverlaySource,
|
|
});
|
|
|
|
// Applications supply their boundary resolver; the layer has no server-route dependency.
|
|
const DEFAULT_BOUNDARY_RESOLVER = async () => null;
|
|
|
|
function abortError() {
|
|
return new DOMException(
|
|
'Bhote Koshi locator activation cancelled',
|
|
'AbortError',
|
|
);
|
|
}
|
|
|
|
function normalizePresentation(value) {
|
|
if (value === NEPAL_CONTEXT_PRESENTATION) return NEPAL_CONTEXT_PRESENTATION;
|
|
if (value === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION) {
|
|
return BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION;
|
|
}
|
|
if (value === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION) {
|
|
return BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION;
|
|
}
|
|
if (value === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION) {
|
|
return BHOTE_KOSHI_FLOOD_PATH_PRESENTATION;
|
|
}
|
|
if (
|
|
value === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION ||
|
|
value === LEGACY_PATH_BORDER_PRESENTATION
|
|
) {
|
|
return BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION;
|
|
}
|
|
if (value === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) {
|
|
return BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION;
|
|
}
|
|
return BHOTE_KOSHI_REGIONAL_PRESENTATION;
|
|
}
|
|
|
|
function presentationUsesBoundary(value) {
|
|
return (
|
|
value === BHOTE_KOSHI_REGIONAL_PRESENTATION ||
|
|
value === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION
|
|
);
|
|
}
|
|
|
|
function presentationAnimatesBoundary(value) {
|
|
return value === BHOTE_KOSHI_REGIONAL_PRESENTATION;
|
|
}
|
|
|
|
function smoothstep(from, to, value) {
|
|
if (to <= from) return value >= to ? 1 : 0;
|
|
const amount = Math.max(0, Math.min(1, (value - from) / (to - from)));
|
|
return amount * amount * (3 - 2 * amount);
|
|
}
|
|
|
|
/** Resolve the persistent text callout's dot, leader, and card entrance. */
|
|
export function anchoredCalloutPresentation(elapsedMs) {
|
|
const progress = Math.max(
|
|
0,
|
|
Math.min(1, Number(elapsedMs) / CALLOUT_REVEAL_DURATION_MS || 0),
|
|
);
|
|
const contentAlpha = smoothstep(0.34, 1, progress);
|
|
return {
|
|
alpha: smoothstep(0, 0.18, progress),
|
|
leaderProgress: smoothstep(0.1, 0.52, progress),
|
|
contentAlpha,
|
|
scale: 0.96 + contentAlpha * 0.04,
|
|
};
|
|
}
|
|
|
|
/** Return the progressive prefix used by the animated Nepal border. */
|
|
export function revealPolylinePositions(positions, progress) {
|
|
if (!Array.isArray(positions) || positions.length < 2) return [];
|
|
const clamped = Math.max(0, Math.min(1, Number(progress) || 0));
|
|
if (clamped >= 1) return positions;
|
|
const scaled = clamped * (positions.length - 1);
|
|
const completed = Math.floor(scaled);
|
|
const revealed = positions.slice(0, completed + 1);
|
|
const next = Math.min(completed + 1, positions.length - 1);
|
|
if (next > completed) {
|
|
revealed.push(
|
|
Cesium.Cartesian3.lerp(
|
|
positions[completed],
|
|
positions[next],
|
|
scaled - completed,
|
|
new Cesium.Cartesian3(),
|
|
),
|
|
);
|
|
}
|
|
return revealed.length >= 2 ? revealed : [positions[0], positions[0]];
|
|
}
|
|
|
|
/** Close a region ring without duplicating an already-closed final vertex. */
|
|
export function closeRegionRing(ring) {
|
|
if (!Array.isArray(ring) || ring.length < 3) return [];
|
|
const normalized = ring
|
|
.map((point) => [Number(point?.[0]), Number(point?.[1])])
|
|
.filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
|
|
if (normalized.length < 3) return [];
|
|
const [firstLon, firstLat] = normalized[0];
|
|
const [lastLon, lastLat] = normalized[normalized.length - 1];
|
|
if (firstLon !== lastLon || firstLat !== lastLat)
|
|
normalized.push([firstLon, firstLat]);
|
|
return normalized;
|
|
}
|
|
|
|
/** Return the largest exterior ring from a Polygon or MultiPolygon GeoJSON. */
|
|
export function largestGeoJsonRing(geojson) {
|
|
const polygons =
|
|
geojson?.type === 'Polygon'
|
|
? [geojson.coordinates]
|
|
: geojson?.type === 'MultiPolygon'
|
|
? geojson.coordinates
|
|
: [];
|
|
const rings = polygons
|
|
.map((polygon) => polygon?.[0])
|
|
.filter((ring) => Array.isArray(ring) && ring.length >= 3);
|
|
return rings.sort((a, b) => b.length - a.length)[0] || [];
|
|
}
|
|
|
|
/**
|
|
* Lightweight scene overlay for the multi-shot Nepal incident setup. It uses
|
|
* the local OpenStreetMap boundary proxy and deliberately avoids the full
|
|
* reconstruction layer's imagery, panel, playback, and camera ownership.
|
|
*/
|
|
export function createBhoteKoshiLocatorLayer({
|
|
boundaryResolver = DEFAULT_BOUNDARY_RESOLVER,
|
|
overlayHost = DEFAULT_OVERLAY_HOST,
|
|
requestRender = governorRequestRender,
|
|
now = () => performance.now(),
|
|
scheduleFrame = (callback) => requestAnimationFrame(callback),
|
|
cancelFrame = (handle) => cancelAnimationFrame(handle),
|
|
scheduleDelay = (callback, delayMs) => setTimeout(callback, delayMs),
|
|
cancelDelay = (handle) => clearTimeout(handle),
|
|
} = {}) {
|
|
let _viewer = null;
|
|
let _dataSource = null;
|
|
let _enabled = false;
|
|
let _boundaryReady = false;
|
|
let _boundaryResolved = false;
|
|
let _boundaryRing = null;
|
|
let _generation = 0;
|
|
let _presentation = BHOTE_KOSHI_REGIONAL_PRESENTATION;
|
|
let _borderAnimationFrame = null;
|
|
let _calloutPulseFrame = null;
|
|
let _triggerApproachTimer = null;
|
|
let _removeTriggerMoveStart = null;
|
|
let _removeTriggerMoveEnd = null;
|
|
let _triggerApproachInFlight = false;
|
|
let _triggerApproachAttempt = null;
|
|
let _triggerCalloutStartedAt = Number.POSITIVE_INFINITY;
|
|
let _approachEnabled = false;
|
|
let _overlayEntries = [];
|
|
let _placeRevealMoving = false;
|
|
let _placeRevealSettledAt = Number.POSITIVE_INFINITY;
|
|
|
|
function queueOverlayEntry(entry) {
|
|
_overlayEntries.push(entry);
|
|
}
|
|
|
|
function publishOverlayEntries({ moving = false } = {}) {
|
|
overlayHost.setEntries(
|
|
BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID,
|
|
_overlayEntries,
|
|
{
|
|
cohortLimit: Math.max(1, _overlayEntries.length),
|
|
collisionCapacity: Math.max(1, _overlayEntries.length),
|
|
moving,
|
|
},
|
|
);
|
|
}
|
|
|
|
function clearTriggerApproach({ preserveCallout = false } = {}) {
|
|
// Revoke ownership before cancelFlight can synchronously deliver callbacks.
|
|
_triggerApproachAttempt = null;
|
|
if (_triggerApproachTimer !== null) cancelDelay(_triggerApproachTimer);
|
|
_triggerApproachTimer = null;
|
|
_removeTriggerMoveStart?.();
|
|
_removeTriggerMoveEnd?.();
|
|
_removeTriggerMoveStart = null;
|
|
_removeTriggerMoveEnd = null;
|
|
const wasInFlight = _triggerApproachInFlight;
|
|
_triggerApproachInFlight = false;
|
|
if (wasInFlight) _viewer?.camera?.cancelFlight?.();
|
|
if (!preserveCallout) _triggerCalloutStartedAt = Number.POSITIVE_INFINITY;
|
|
}
|
|
|
|
function stopAnimations() {
|
|
if (_borderAnimationFrame !== null) cancelFrame(_borderAnimationFrame);
|
|
if (_calloutPulseFrame !== null) cancelFrame(_calloutPulseFrame);
|
|
_borderAnimationFrame = null;
|
|
_calloutPulseFrame = null;
|
|
_placeRevealMoving = false;
|
|
_placeRevealSettledAt = Number.POSITIVE_INFINITY;
|
|
clearTriggerApproach();
|
|
}
|
|
|
|
function removeDataSource() {
|
|
stopAnimations();
|
|
overlayHost.clearSource(BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID);
|
|
if (_dataSource && _viewer?.dataSources)
|
|
_viewer.dataSources.remove(_dataSource, true);
|
|
_dataSource = null;
|
|
}
|
|
|
|
function addCallout(
|
|
dataSource,
|
|
subject,
|
|
color,
|
|
{
|
|
labelPlacement = 'right',
|
|
sequenceStartedAt = now(),
|
|
visibleAfterMs = 0,
|
|
showAnchor = true,
|
|
} = {},
|
|
) {
|
|
const position = Cesium.Cartesian3.fromDegrees(
|
|
subject.lon,
|
|
subject.lat,
|
|
subject.heightM,
|
|
);
|
|
const visibleAt = sequenceStartedAt + visibleAfterMs;
|
|
const isVisible = () => now() >= visibleAt;
|
|
const pulseProgress = () => {
|
|
const elapsed = Math.max(0, now() - visibleAt);
|
|
return (elapsed % CALLOUT_PULSE_DURATION_MS) / CALLOUT_PULSE_DURATION_MS;
|
|
};
|
|
const ringSize = new Cesium.CallbackProperty(() => {
|
|
return (
|
|
CALLOUT_RING_MIN_SIZE_PX + CALLOUT_RING_SIZE_RANGE_PX * pulseProgress()
|
|
);
|
|
}, false);
|
|
const ringColor = new Cesium.CallbackProperty((_time, result) => {
|
|
const dynamicColor = Cesium.Color.clone(color, result);
|
|
dynamicColor.alpha = isVisible() ? 0.14 * (1 - pulseProgress()) : 0;
|
|
return dynamicColor;
|
|
}, false);
|
|
const ringOutlineColor = new Cesium.CallbackProperty((_time, result) => {
|
|
const dynamicColor = Cesium.Color.clone(color, result);
|
|
dynamicColor.alpha = isVisible()
|
|
? 0.08 + 0.72 * (1 - pulseProgress())
|
|
: 0;
|
|
return dynamicColor;
|
|
}, false);
|
|
if (showAnchor) {
|
|
dataSource.entities.add({
|
|
id: `${subject === INCIDENT ? 'bhote-koshi-incident' : 'nepal-context'}-halo`,
|
|
position,
|
|
show: new Cesium.CallbackProperty(isVisible, false),
|
|
point: {
|
|
pixelSize: ringSize,
|
|
color: ringColor,
|
|
outlineColor: ringOutlineColor,
|
|
outlineWidth: 2,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
scaleByDistance: new Cesium.NearFarScalar(
|
|
250000,
|
|
0.8,
|
|
25000000,
|
|
1.35,
|
|
),
|
|
},
|
|
});
|
|
}
|
|
const presentation = () => anchoredCalloutPresentation(now() - visibleAt);
|
|
if (subject === INCIDENT) {
|
|
queueOverlayEntry({
|
|
id: 'bhote-koshi-incident-callout',
|
|
position,
|
|
variant: 'card',
|
|
paintLane: 'thumbnail',
|
|
title: t(subject.title),
|
|
details: [t(subject.subtitle)],
|
|
accent: INCIDENT_ACCENT,
|
|
sourceAlpha: () => presentation().alpha,
|
|
presentationScale: () => presentation().scale,
|
|
leaderProgress: () => presentation().leaderProgress,
|
|
contentAlpha: () => presentation().contentAlpha,
|
|
anchorDot: true,
|
|
leaderStyle: 'elbow',
|
|
priority: 1_000_000,
|
|
protected: true,
|
|
active: true,
|
|
collisionGroup: 'ambient-card',
|
|
zIndex: 60,
|
|
minDistance: 0,
|
|
maxDistance: 5_000_000,
|
|
distanceFadeStartRatio: 0.9,
|
|
distanceScale: {
|
|
near: 1200,
|
|
nearValue: 1,
|
|
far: 4_000_000,
|
|
farValue: 0.78,
|
|
},
|
|
edgeFade: 'none',
|
|
horizonCull: true,
|
|
terrainOcclusion: false,
|
|
gapPx: 36,
|
|
leaderOffsetPx: 4,
|
|
placement: 'right',
|
|
});
|
|
} else {
|
|
const labelOnLeft = labelPlacement === 'left';
|
|
queueOverlayEntry({
|
|
id: 'nepal-context-callout',
|
|
position,
|
|
variant: 'label',
|
|
paintLane: 'ambient-label',
|
|
title: subject.title,
|
|
details: subject.subtitle ? [subject.subtitle] : [],
|
|
accent: BORDER_ACCENT,
|
|
sourceAlpha: () => presentation().alpha,
|
|
presentationScale: () => presentation().scale,
|
|
leaderProgress: () => presentation().leaderProgress,
|
|
contentAlpha: () => presentation().contentAlpha,
|
|
anchorDot: showAnchor,
|
|
leaderStyle: 'elbow',
|
|
priority: 900_000,
|
|
protected: true,
|
|
active: true,
|
|
collisionGroup: 'nepal-locator-label',
|
|
zIndex: 48,
|
|
minDistance: 0,
|
|
maxDistance: 25_000_000,
|
|
distanceFadeStartRatio: 0.9,
|
|
distanceScale: {
|
|
near: 250000,
|
|
nearValue: 1.05,
|
|
far: 25000000,
|
|
farValue: 0.72,
|
|
},
|
|
edgeFade: 'none',
|
|
horizonCull: true,
|
|
terrainOcclusion: false,
|
|
gapPx: 18,
|
|
placement: labelOnLeft ? 'left' : 'right',
|
|
});
|
|
}
|
|
}
|
|
|
|
function startCalloutAnimation() {
|
|
const generation = _generation;
|
|
const pulseFrame = () => {
|
|
if (!_enabled || generation !== _generation) return;
|
|
if (_placeRevealMoving && now() >= _placeRevealSettledAt) {
|
|
_placeRevealMoving = false;
|
|
_placeRevealSettledAt = Number.POSITIVE_INFINITY;
|
|
publishOverlayEntries();
|
|
}
|
|
requestRender('bhote-koshi-locator-callout-pulse');
|
|
_calloutPulseFrame = scheduleFrame(pulseFrame);
|
|
};
|
|
_calloutPulseFrame = scheduleFrame(pulseFrame);
|
|
}
|
|
|
|
function addIncidentCallout(
|
|
dataSource,
|
|
sequenceStartedAt,
|
|
visibleAfterMs = 0,
|
|
) {
|
|
addCallout(dataSource, INCIDENT, INCIDENT_COLOR, {
|
|
sequenceStartedAt,
|
|
visibleAfterMs,
|
|
});
|
|
}
|
|
|
|
function addTriggerRecordCallout(dataSource) {
|
|
const position = Cesium.Cartesian3.fromDegrees(
|
|
TRIGGER_RECORD.lon,
|
|
TRIGGER_RECORD.lat,
|
|
TRIGGER_RECORD.heightM,
|
|
);
|
|
const isVisible = () => now() >= _triggerCalloutStartedAt;
|
|
const presentation = () =>
|
|
anchoredCalloutPresentation(now() - _triggerCalloutStartedAt);
|
|
const pulseProgress = () => {
|
|
if (!isVisible()) return 0;
|
|
return (
|
|
((now() - _triggerCalloutStartedAt) % CALLOUT_PULSE_DURATION_MS) /
|
|
CALLOUT_PULSE_DURATION_MS
|
|
);
|
|
};
|
|
dataSource.entities.add({
|
|
id: 'bhote-koshi-trigger-record-halo',
|
|
position,
|
|
show: new Cesium.CallbackProperty(isVisible, false),
|
|
point: {
|
|
pixelSize: new Cesium.CallbackProperty(
|
|
() =>
|
|
CALLOUT_RING_MIN_SIZE_PX +
|
|
CALLOUT_RING_SIZE_RANGE_PX * pulseProgress(),
|
|
false,
|
|
),
|
|
color: new Cesium.CallbackProperty((_time, result) => {
|
|
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
|
color.alpha = 0.16 * (1 - pulseProgress());
|
|
return color;
|
|
}, false),
|
|
outlineColor: new Cesium.CallbackProperty((_time, result) => {
|
|
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
|
color.alpha = isVisible() ? 0.08 + 0.78 * (1 - pulseProgress()) : 0;
|
|
return color;
|
|
}, false),
|
|
outlineWidth: 2,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
scaleByDistance: new Cesium.NearFarScalar(3000, 1, 250000, 0.82),
|
|
},
|
|
});
|
|
queueOverlayEntry({
|
|
id: 'bhote-koshi-trigger-record-callout',
|
|
position,
|
|
variant: 'card',
|
|
paintLane: 'thumbnail',
|
|
title: t(TRIGGER_RECORD.title),
|
|
details: TRIGGER_RECORD.details.map((line) => t(line)),
|
|
accent: INCIDENT_ACCENT,
|
|
sourceAlpha: () => presentation().alpha,
|
|
presentationScale: () => presentation().scale,
|
|
leaderProgress: () => presentation().leaderProgress,
|
|
contentAlpha: () => presentation().contentAlpha,
|
|
anchorDot: true,
|
|
leaderStyle: 'elbow',
|
|
priority: 1_000_000,
|
|
protected: true,
|
|
active: true,
|
|
interactive: true,
|
|
accessibilityLabel: t(
|
|
'Open the USGS initial report for the 26 August 2026 Nepal landslide',
|
|
),
|
|
activate: () => {
|
|
const opened = window.open(
|
|
TRIGGER_SOURCE_URL,
|
|
'_blank',
|
|
'noopener,noreferrer',
|
|
);
|
|
if (opened) opened.opener = null;
|
|
},
|
|
collisionGroup: 'ambient-card',
|
|
zIndex: 60,
|
|
minDistance: 0,
|
|
maxDistance: 1_000_000,
|
|
distanceFadeStartRatio: 0.92,
|
|
distanceScale: { near: 1200, nearValue: 1, far: 800000, farValue: 0.82 },
|
|
edgeFade: 'none',
|
|
horizonCull: true,
|
|
terrainOcclusion: false,
|
|
gapPx: 34,
|
|
leaderOffsetPx: 4,
|
|
placement: 'left',
|
|
});
|
|
}
|
|
|
|
function startTriggerApproach(attempt) {
|
|
if (
|
|
!_enabled ||
|
|
_presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION ||
|
|
_triggerApproachAttempt !== attempt ||
|
|
attempt.phase !== 'armed'
|
|
)
|
|
return;
|
|
attempt.phase = 'flying';
|
|
if (_triggerApproachTimer !== null) cancelDelay(_triggerApproachTimer);
|
|
_triggerApproachTimer = null;
|
|
_removeTriggerMoveStart?.();
|
|
_removeTriggerMoveEnd?.();
|
|
_removeTriggerMoveStart = null;
|
|
_removeTriggerMoveEnd = null;
|
|
_triggerCalloutStartedAt = now();
|
|
requestRender('bhote-koshi-trigger-record-reveal');
|
|
const camera = _viewer?.camera;
|
|
if (!camera?.flyTo) return;
|
|
const generation = _generation;
|
|
_triggerApproachInFlight = true;
|
|
const finish = () => {
|
|
if (_triggerApproachAttempt !== attempt) return;
|
|
attempt.phase = 'finished';
|
|
_triggerApproachInFlight = false;
|
|
};
|
|
camera.flyTo({
|
|
destination: Cesium.Cartesian3.fromDegrees(
|
|
TRIGGER_APPROACH_CAMERA.lon,
|
|
TRIGGER_APPROACH_CAMERA.lat,
|
|
TRIGGER_APPROACH_CAMERA.alt,
|
|
),
|
|
orientation: {
|
|
heading: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.heading),
|
|
pitch: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.pitch),
|
|
roll: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.roll),
|
|
},
|
|
duration: TRIGGER_APPROACH_DURATION_SEC,
|
|
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
|
complete: () => {
|
|
if (_triggerApproachAttempt !== attempt || attempt.phase !== 'flying')
|
|
return;
|
|
if (
|
|
!_enabled ||
|
|
generation !== _generation ||
|
|
_presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
|
|
) {
|
|
finish();
|
|
return;
|
|
}
|
|
attempt.phase = 'orbiting';
|
|
camera.flyTo({
|
|
destination: Cesium.Cartesian3.fromDegrees(
|
|
TRIGGER_ORBIT_CAMERA.lon,
|
|
TRIGGER_ORBIT_CAMERA.lat,
|
|
TRIGGER_ORBIT_CAMERA.alt,
|
|
),
|
|
orientation: {
|
|
heading: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.heading),
|
|
pitch: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.pitch),
|
|
roll: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.roll),
|
|
},
|
|
duration: TRIGGER_ORBIT_DURATION_SEC,
|
|
easingFunction: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
|
|
complete: finish,
|
|
cancel: finish,
|
|
});
|
|
},
|
|
cancel: finish,
|
|
});
|
|
}
|
|
|
|
function armTriggerApproach() {
|
|
if (!_approachEnabled) {
|
|
_triggerCalloutStartedAt = now();
|
|
return;
|
|
}
|
|
const attempt = { phase: 'armed' };
|
|
_triggerApproachAttempt = attempt;
|
|
const start = () => startTriggerApproach(attempt);
|
|
const camera = _viewer?.camera;
|
|
if (
|
|
!camera?.moveStart?.addEventListener ||
|
|
!camera?.moveEnd?.addEventListener
|
|
) {
|
|
start();
|
|
return;
|
|
}
|
|
_removeTriggerMoveStart = camera.moveStart.addEventListener(() => {
|
|
if (_triggerApproachAttempt !== attempt || attempt.phase !== 'armed')
|
|
return;
|
|
if (_triggerApproachTimer !== null) cancelDelay(_triggerApproachTimer);
|
|
_triggerApproachTimer = null;
|
|
_removeTriggerMoveStart?.();
|
|
_removeTriggerMoveStart = null;
|
|
_removeTriggerMoveEnd = camera.moveEnd.addEventListener(start);
|
|
});
|
|
_triggerApproachTimer = scheduleDelay(start, TRIGGER_APPROACH_ARM_MS);
|
|
}
|
|
|
|
function addNepalCallout(
|
|
dataSource,
|
|
sequenceStartedAt = now(),
|
|
labelPlacement = 'right',
|
|
{ showAnchor = true } = {},
|
|
) {
|
|
addCallout(dataSource, NEPAL, BORDER_COLOR, {
|
|
labelPlacement,
|
|
sequenceStartedAt,
|
|
showAnchor,
|
|
});
|
|
}
|
|
|
|
function addNearbyCityCallouts(dataSource, sequenceStartedAt) {
|
|
NEARBY_CITIES.forEach((city, index) => {
|
|
const visibleAt =
|
|
sequenceStartedAt +
|
|
CITY_CALLOUT_START_DELAY_MS +
|
|
index * CITY_CALLOUT_STAGGER_MS;
|
|
const progress = () =>
|
|
smoothstep(0, 1, (now() - visibleAt) / CITY_CALLOUT_REVEAL_MS);
|
|
const labelOnLeft = city.placement === 'left';
|
|
const labelLower = city.placement === 'right-lower';
|
|
const position = Cesium.Cartesian3.fromDegrees(
|
|
city.lon,
|
|
city.lat,
|
|
city.heightM,
|
|
);
|
|
queueOverlayEntry({
|
|
id: `bhote-koshi-city-${city.id}`,
|
|
position,
|
|
variant: 'label',
|
|
paintLane: 'ambient-label',
|
|
title: city.title,
|
|
details: [],
|
|
accent: BORDER_ACCENT,
|
|
sourceAlpha: progress,
|
|
presentationScale: () => 0.92 + progress() * 0.08,
|
|
leaderProgress: progress,
|
|
contentAlpha: progress,
|
|
anchorDot: true,
|
|
leaderStyle: 'elbow',
|
|
priority: 700_000 - index,
|
|
collisionGroup: 'nepal-nearby-city',
|
|
zIndex: 44,
|
|
minDistance: 0,
|
|
maxDistance: 3_000_000,
|
|
distanceFadeStartRatio: 0.82,
|
|
distanceScale: {
|
|
near: 100000,
|
|
nearValue: 1.08,
|
|
far: 3000000,
|
|
farValue: 0.78,
|
|
},
|
|
edgeFade: 'keyhole',
|
|
horizonCull: true,
|
|
terrainOcclusion: false,
|
|
gapPx: 14,
|
|
placement: labelLower ? 'below' : labelOnLeft ? 'left' : 'right',
|
|
});
|
|
});
|
|
}
|
|
|
|
function addIncidentPlaceCallouts(
|
|
dataSource,
|
|
sequenceStartedAt,
|
|
{ pulseHalos = false, places = INCIDENT_OVERVIEW_PLACES } = {},
|
|
) {
|
|
places.forEach((place, index) => {
|
|
const visibleAt =
|
|
sequenceStartedAt +
|
|
PLACE_CALLOUT_START_DELAY_MS +
|
|
index * PLACE_CALLOUT_STAGGER_MS;
|
|
const progress = () =>
|
|
smoothstep(0, 1, (now() - visibleAt) / PLACE_CALLOUT_REVEAL_MS);
|
|
const labelOnLeft = place.placement === 'left';
|
|
const labelLower = place.placement === 'right-lower';
|
|
const position = Cesium.Cartesian3.fromDegrees(
|
|
place.lon,
|
|
place.lat,
|
|
place.heightM,
|
|
);
|
|
if (pulseHalos) {
|
|
const pulseSequenceDurationMs =
|
|
PLACE_CALLOUT_STAGGER_MS * places.length;
|
|
const pulseWindowElapsed = () => {
|
|
const sequenceElapsed = Math.max(
|
|
0,
|
|
now() - sequenceStartedAt - PLACE_CALLOUT_START_DELAY_MS,
|
|
);
|
|
return (
|
|
(sequenceElapsed % pulseSequenceDurationMs) -
|
|
index * PLACE_CALLOUT_STAGGER_MS
|
|
);
|
|
};
|
|
const pulseIsActive = () => {
|
|
const elapsed = pulseWindowElapsed();
|
|
return (
|
|
now() >= visibleAt &&
|
|
elapsed >= 0 &&
|
|
elapsed < PLACE_CALLOUT_STAGGER_MS
|
|
);
|
|
};
|
|
const pulseProgress = () => {
|
|
return Math.max(
|
|
0,
|
|
Math.min(1, pulseWindowElapsed() / PLACE_CALLOUT_STAGGER_MS),
|
|
);
|
|
};
|
|
dataSource.entities.add({
|
|
id: `bhote-koshi-place-${place.id}-halo`,
|
|
position,
|
|
show: new Cesium.CallbackProperty(pulseIsActive, false),
|
|
point: {
|
|
pixelSize: new Cesium.CallbackProperty(
|
|
() =>
|
|
CALLOUT_RING_MIN_SIZE_PX +
|
|
CALLOUT_RING_SIZE_RANGE_PX * pulseProgress(),
|
|
false,
|
|
),
|
|
color: new Cesium.CallbackProperty((_time, result) => {
|
|
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
|
color.alpha = 0.16 * (1 - pulseProgress());
|
|
return color;
|
|
}, false),
|
|
outlineColor: new Cesium.CallbackProperty((_time, result) => {
|
|
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
|
color.alpha = 0.08 + 0.78 * (1 - pulseProgress());
|
|
return color;
|
|
}, false),
|
|
outlineWidth: 2,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
scaleByDistance: new Cesium.NearFarScalar(5000, 1, 500000, 0.82),
|
|
},
|
|
});
|
|
}
|
|
queueOverlayEntry({
|
|
id: `bhote-koshi-place-${place.id}`,
|
|
position,
|
|
variant: 'label',
|
|
paintLane: 'ambient-label',
|
|
title: `${String(index + 1).padStart(2, '0')} · ${placeTitle(place.title)}`,
|
|
details: [],
|
|
accent: INCIDENT_ACCENT,
|
|
sourceAlpha: progress,
|
|
presentationScale: () => 0.92 + progress() * 0.08,
|
|
leaderProgress: progress,
|
|
contentAlpha: progress,
|
|
anchorDot: true,
|
|
leaderStyle: 'elbow',
|
|
priority: 800_000 - index,
|
|
collisionGroup: 'bhote-koshi-place',
|
|
zIndex: 46,
|
|
minDistance: 0,
|
|
maxDistance: 500_000,
|
|
distanceFadeStartRatio: 0.82,
|
|
distanceScale: {
|
|
near: 5000,
|
|
nearValue: 1.08,
|
|
far: 500000,
|
|
farValue: 0.82,
|
|
},
|
|
edgeFade: 'keyhole',
|
|
horizonCull: true,
|
|
terrainOcclusion: false,
|
|
gapPx: 14,
|
|
placement: labelLower ? 'below' : labelOnLeft ? 'left' : 'right',
|
|
});
|
|
});
|
|
}
|
|
|
|
function addIncidentFloodPath(dataSource, sequenceStartedAt) {
|
|
const positions = BHOTE_KOSHI_FLOOD_PATH.map(([lon, lat]) =>
|
|
Cesium.Cartesian3.fromDegrees(lon, lat),
|
|
);
|
|
const drawStartedAt = sequenceStartedAt + FLOOD_PATH_START_DELAY_MS;
|
|
const animatedPositions = new Cesium.CallbackProperty(
|
|
() =>
|
|
revealPolylinePositions(
|
|
positions,
|
|
(now() - drawStartedAt) / FLOOD_PATH_DRAW_DURATION_MS,
|
|
),
|
|
false,
|
|
);
|
|
dataSource.entities.add({
|
|
id: 'bhote-koshi-flood-path',
|
|
show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false),
|
|
polyline: {
|
|
positions: animatedPositions,
|
|
width: 3.5,
|
|
clampToGround: true,
|
|
material: BORDER_COLOR.withAlpha(0.96),
|
|
classificationType: Cesium.ClassificationType.BOTH,
|
|
},
|
|
});
|
|
}
|
|
|
|
function addNepalBoundary(dataSource, ring, sequenceStartedAt) {
|
|
const closed = closeRegionRing(ring);
|
|
if (closed.length < 4) return false;
|
|
const positions = Cesium.Cartesian3.fromDegreesArray(closed.flat());
|
|
const hierarchy = new Cesium.PolygonHierarchy(positions.slice(0, -1));
|
|
const drawStartedAt = sequenceStartedAt + BORDER_START_DELAY_MS;
|
|
const animatedPositions = new Cesium.CallbackProperty(
|
|
() =>
|
|
revealPolylinePositions(
|
|
positions,
|
|
(now() - drawStartedAt) / BORDER_DRAW_DURATION_MS,
|
|
),
|
|
false,
|
|
);
|
|
dataSource.entities.add({
|
|
id: 'nepal-border-fill',
|
|
show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false),
|
|
polygon: {
|
|
hierarchy,
|
|
material: BORDER_COLOR.withAlpha(0.055),
|
|
classificationType: Cesium.ClassificationType.BOTH,
|
|
},
|
|
});
|
|
dataSource.entities.add({
|
|
id: 'nepal-border-halo',
|
|
polyline: {
|
|
positions: animatedPositions,
|
|
width: 5,
|
|
clampToGround: true,
|
|
material: BORDER_COLOR.withAlpha(0.12),
|
|
classificationType: Cesium.ClassificationType.BOTH,
|
|
},
|
|
});
|
|
dataSource.entities.add({
|
|
id: 'nepal-border-highlight',
|
|
polyline: {
|
|
positions: animatedPositions,
|
|
width: 2.5,
|
|
clampToGround: true,
|
|
material: BORDER_COLOR.withAlpha(0.96),
|
|
classificationType: Cesium.ClassificationType.BOTH,
|
|
},
|
|
});
|
|
const generation = _generation;
|
|
const drawFrame = () => {
|
|
if (
|
|
!_enabled ||
|
|
generation !== _generation ||
|
|
!presentationAnimatesBoundary(_presentation)
|
|
)
|
|
return;
|
|
requestRender('bhote-koshi-locator-border-draw');
|
|
if (now() - drawStartedAt < BORDER_DRAW_DURATION_MS) {
|
|
_borderAnimationFrame = scheduleFrame(drawFrame);
|
|
} else {
|
|
_borderAnimationFrame = null;
|
|
}
|
|
};
|
|
_borderAnimationFrame = scheduleFrame(drawFrame);
|
|
return true;
|
|
}
|
|
|
|
function renderPresentation() {
|
|
if (!_dataSource) return;
|
|
stopAnimations();
|
|
overlayHost.clearSource(BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID);
|
|
_overlayEntries = [];
|
|
_dataSource.entities.removeAll();
|
|
if (_presentation === NEPAL_CONTEXT_PRESENTATION) {
|
|
addNepalCallout(_dataSource);
|
|
} else if (_presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION) {
|
|
const sequenceStartedAt = now();
|
|
addIncidentPlaceCallouts(_dataSource, sequenceStartedAt, {
|
|
pulseHalos: true,
|
|
places: INCIDENT_OVERVIEW_PLACES,
|
|
});
|
|
_placeRevealMoving = true;
|
|
_placeRevealSettledAt =
|
|
sequenceStartedAt +
|
|
PLACE_CALLOUT_START_DELAY_MS +
|
|
(INCIDENT_OVERVIEW_PLACES.length - 1) * PLACE_CALLOUT_STAGGER_MS +
|
|
PLACE_CALLOUT_REVEAL_MS;
|
|
} else if (_presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION) {
|
|
const sequenceStartedAt = now();
|
|
const settledPlacesAt =
|
|
sequenceStartedAt -
|
|
PLACE_CALLOUT_START_DELAY_MS -
|
|
(INCIDENT_OVERVIEW_PLACES.length - 1) * PLACE_CALLOUT_STAGGER_MS -
|
|
PLACE_CALLOUT_REVEAL_MS;
|
|
addIncidentFloodPath(_dataSource, sequenceStartedAt);
|
|
addIncidentPlaceCallouts(_dataSource, settledPlacesAt);
|
|
} else if (_presentation === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION) {
|
|
const sequenceStartedAt = now();
|
|
const settledPathAt =
|
|
sequenceStartedAt -
|
|
FLOOD_PATH_START_DELAY_MS -
|
|
FLOOD_PATH_DRAW_DURATION_MS;
|
|
const settledPlacesAt =
|
|
sequenceStartedAt -
|
|
PLACE_CALLOUT_START_DELAY_MS -
|
|
(INCIDENT_OVERVIEW_PLACES.length - 1) * PLACE_CALLOUT_STAGGER_MS -
|
|
PLACE_CALLOUT_REVEAL_MS;
|
|
addIncidentFloodPath(_dataSource, settledPathAt);
|
|
addIncidentPlaceCallouts(_dataSource, settledPlacesAt);
|
|
} else if (_presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) {
|
|
const sequenceStartedAt = now();
|
|
const settledPathAt =
|
|
sequenceStartedAt -
|
|
FLOOD_PATH_START_DELAY_MS -
|
|
FLOOD_PATH_DRAW_DURATION_MS;
|
|
addIncidentFloodPath(_dataSource, settledPathAt);
|
|
addTriggerRecordCallout(_dataSource);
|
|
armTriggerApproach();
|
|
} else {
|
|
const sequenceStartedAt = now();
|
|
addNepalCallout(_dataSource, sequenceStartedAt, 'left');
|
|
if (!_boundaryResolved) {
|
|
publishOverlayEntries();
|
|
startCalloutAnimation();
|
|
requestRender('bhote-koshi-locator-presentation');
|
|
return;
|
|
}
|
|
if (_presentation === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION) {
|
|
const settledAt =
|
|
sequenceStartedAt - BORDER_START_DELAY_MS - BORDER_DRAW_DURATION_MS;
|
|
if (_boundaryRing)
|
|
addNepalBoundary(_dataSource, _boundaryRing, settledAt);
|
|
addIncidentCallout(
|
|
_dataSource,
|
|
sequenceStartedAt - CALLOUT_REVEAL_DURATION_MS,
|
|
);
|
|
addNearbyCityCallouts(_dataSource, sequenceStartedAt);
|
|
} else {
|
|
if (_boundaryRing)
|
|
addNepalBoundary(_dataSource, _boundaryRing, sequenceStartedAt);
|
|
const incidentDelayMs = _boundaryRing
|
|
? BORDER_START_DELAY_MS +
|
|
BORDER_DRAW_DURATION_MS +
|
|
INCIDENT_CALLOUT_GAP_MS
|
|
: BORDER_START_DELAY_MS;
|
|
addIncidentCallout(_dataSource, sequenceStartedAt, incidentDelayMs);
|
|
}
|
|
}
|
|
publishOverlayEntries({ moving: _placeRevealMoving });
|
|
startCalloutAnimation();
|
|
requestRender('bhote-koshi-locator-presentation');
|
|
}
|
|
|
|
async function init(viewer) {
|
|
_viewer = viewer;
|
|
return true;
|
|
}
|
|
|
|
async function enable(viewer, { signal } = {}) {
|
|
_viewer = viewer;
|
|
_enabled = true;
|
|
_boundaryReady = false;
|
|
_boundaryResolved = false;
|
|
_boundaryRing = null;
|
|
const generation = ++_generation;
|
|
removeDataSource();
|
|
const dataSource = new Cesium.CustomDataSource('bhote-koshi-locator');
|
|
_dataSource = dataSource;
|
|
renderPresentation();
|
|
viewer.dataSources.add(dataSource);
|
|
registerDynamicCredit(viewer, BHOTE_KOSHI_LOCATOR_CREDIT);
|
|
showOsmCredit(viewer, BHOTE_KOSHI_LOCATOR_LAYER_ID);
|
|
requestRender('bhote-koshi-locator-callout');
|
|
|
|
try {
|
|
const boundary = await boundaryResolver(signal);
|
|
if (signal?.aborted || !_enabled || generation !== _generation)
|
|
throw abortError();
|
|
_boundaryRing = closeRegionRing(boundary?.ring);
|
|
_boundaryReady = _boundaryRing.length >= 4;
|
|
_boundaryResolved = true;
|
|
if (!_boundaryReady && presentationUsesBoundary(_presentation)) {
|
|
console.warn('[Data:BhoteKoshiLocator] Nepal boundary unavailable');
|
|
}
|
|
if (presentationUsesBoundary(_presentation)) {
|
|
renderPresentation();
|
|
}
|
|
requestRender('bhote-koshi-locator-boundary');
|
|
return true;
|
|
} catch (error) {
|
|
if (error?.name === 'AbortError') {
|
|
if (generation === _generation) removeDataSource();
|
|
throw error;
|
|
}
|
|
console.warn(
|
|
'[Data:BhoteKoshiLocator] Nepal boundary lookup failed:',
|
|
error,
|
|
);
|
|
_boundaryResolved = true;
|
|
_boundaryRing = null;
|
|
if (presentationUsesBoundary(_presentation)) renderPresentation();
|
|
requestRender('bhote-koshi-locator-degraded');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
async function disable() {
|
|
hideOsmCredit(_viewer, BHOTE_KOSHI_LOCATOR_LAYER_ID);
|
|
_enabled = false;
|
|
_boundaryReady = false;
|
|
_boundaryResolved = false;
|
|
_boundaryRing = null;
|
|
_generation += 1;
|
|
removeDataSource();
|
|
requestRender('bhote-koshi-locator-disable');
|
|
return true;
|
|
}
|
|
|
|
async function destroy() {
|
|
await disable();
|
|
_viewer = null;
|
|
}
|
|
|
|
return {
|
|
id: BHOTE_KOSHI_LOCATOR_LAYER_ID,
|
|
name: 'Bhote Koshi Locator',
|
|
// Scene-owned component; retain registration without a standalone menu row.
|
|
showInTogglePanel: false,
|
|
icon: '◎',
|
|
source: 'OpenStreetMap + GeoPera',
|
|
updateInterval: 0,
|
|
init,
|
|
enable,
|
|
disable,
|
|
update: async () => true,
|
|
destroy,
|
|
cancelSceneMotion() {
|
|
clearTriggerApproach({ preserveCallout: true });
|
|
},
|
|
setParams(params = {}) {
|
|
_presentation = normalizePresentation(params.presentation);
|
|
_approachEnabled =
|
|
_presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION &&
|
|
params.preview !== true;
|
|
if (_enabled) renderPresentation();
|
|
return true;
|
|
},
|
|
getParams() {
|
|
return { presentation: _presentation };
|
|
},
|
|
getRowControls() {
|
|
return {
|
|
chips: [
|
|
{
|
|
id: 'nepal-context',
|
|
// i18n-ignore: country name
|
|
label: 'NEPAL',
|
|
active: _presentation === NEPAL_CONTEXT_PRESENTATION,
|
|
title: t(
|
|
'Show the Nepal context callout without a border or country fill',
|
|
),
|
|
params: { presentation: NEPAL_CONTEXT_PRESENTATION },
|
|
},
|
|
{
|
|
id: 'bhote-koshi-regional',
|
|
// i18n-ignore: river name (Bhote Koshi)
|
|
label: 'BHOTE',
|
|
active: _presentation === BHOTE_KOSHI_REGIONAL_PRESENTATION,
|
|
title: t(
|
|
'Introduce Nepal, draw its border, then reveal the Bhote Koshi incident',
|
|
),
|
|
params: { presentation: BHOTE_KOSHI_REGIONAL_PRESENTATION },
|
|
},
|
|
{
|
|
id: 'bhote-koshi-city-context',
|
|
label: t('CITIES'),
|
|
active: _presentation === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION,
|
|
title: t(
|
|
'Keep the regional incident context and introduce nearby city labels',
|
|
),
|
|
params: { presentation: BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION },
|
|
},
|
|
{
|
|
id: 'bhote-koshi-incident-places',
|
|
label: t('PLACES'),
|
|
active: _presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION,
|
|
title: t(
|
|
'Introduce the reconstruction scene key places from trigger to downstream',
|
|
),
|
|
params: { presentation: BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION },
|
|
},
|
|
{
|
|
id: 'bhote-koshi-flood-path',
|
|
label: t('PATH'),
|
|
active: _presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION,
|
|
title: t(
|
|
'Animate the sourced cyan river-centerline route to Trishuli Bazaar',
|
|
),
|
|
params: { presentation: BHOTE_KOSHI_FLOOD_PATH_PRESENTATION },
|
|
},
|
|
{
|
|
id: 'bhote-koshi-path-overview',
|
|
label: t('OVERVIEW'),
|
|
active: _presentation === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION,
|
|
title: t(
|
|
'Keep the completed flood path and location labels visible',
|
|
),
|
|
params: { presentation: BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION },
|
|
},
|
|
{
|
|
id: 'bhote-koshi-trigger-record',
|
|
label: t('RECORD'),
|
|
active: _presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION,
|
|
title: t(
|
|
'Reveal the trigger record while making a slow camera approach',
|
|
),
|
|
params: {
|
|
presentation: BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION,
|
|
preview: true,
|
|
},
|
|
},
|
|
],
|
|
legend: [],
|
|
};
|
|
},
|
|
getStats() {
|
|
return {
|
|
count: _enabled ? 1 : 0,
|
|
status: !_enabled
|
|
? 'idle'
|
|
: _presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION ||
|
|
_presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION ||
|
|
_presentation === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION ||
|
|
_presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION ||
|
|
_boundaryReady
|
|
? 'nominal'
|
|
: 'degraded',
|
|
source: 'OpenStreetMap + GeoPera',
|
|
coverage: !_enabled
|
|
? 'Incident locator'
|
|
: _presentation === NEPAL_CONTEXT_PRESENTATION
|
|
? 'Nepal context'
|
|
: _presentation === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION
|
|
? 'Bhote Koshi · Nearby cities'
|
|
: _presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION
|
|
? 'Bhote Koshi · Incident places'
|
|
: _presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION
|
|
? 'Bhote Koshi · Flood path'
|
|
: _presentation === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION
|
|
? 'Bhote Koshi · Path overview'
|
|
: _presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
|
|
? 'Bhote Koshi · Trigger record'
|
|
: 'Nepal · Bhote Koshi',
|
|
};
|
|
},
|
|
};
|
|
}
|