mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(layers): Recent Imagery — open satellite imagery for a selected box (#716)
Select a box on the world and browse the last 30 days of open satellite imagery over it, keyless: NASA GIBS Harmonized Landsat Sentinel-2 (30 m) and the VIIRS daily overview (250 m), with a day strip, IMAGE / VS BASEMAP / A / B modes with a swipe divider, PNG export and share links. The before/after swipe from the Nepal scene moves into shared modules so both use it. Co-authored-by: manjunath22466 <[email protected]>
This commit is contained in:
1 parent
25491f7e33
commit
082074a006
50 files changed
+13169
-440
No files matched your search
+32
-136
@@ -22,6 +22,8 @@ import {
|
||||
createBhoteKoshiEmbeddedMedia,
|
||||
resolveEmbeddedMediaSource,
|
||||
} from './bhoteKoshiEmbeddedMedia.js';
|
||||
import { acquireImageryComparison } from '../maps/imageryComparison.js';
|
||||
import { createImagerySplit } from '../ui/imagerySplit.js';
|
||||
|
||||
export const BHOTE_KOSHI_LAYER_ID = 'bhote-koshi-2026';
|
||||
export const BHOTE_KOSHI_OVERLAY_SOURCE_ID = 'bhote-koshi-witnesses';
|
||||
@@ -964,10 +966,7 @@ export function createBhoteKoshiEventLayer({
|
||||
let _enabled = false;
|
||||
let _panel = null;
|
||||
let _panelRefs = null;
|
||||
let _splitLine = null;
|
||||
let _splitHandle = null;
|
||||
let _splitDragging = false;
|
||||
let _splitPointerId = null;
|
||||
let _splitControl = null;
|
||||
let _beforeLayer = null;
|
||||
let _afterLayer = null;
|
||||
let _imageryLayerCollection = null;
|
||||
@@ -1022,14 +1021,11 @@ export function createBhoteKoshiEventLayer({
|
||||
let _playStartedAt = 0;
|
||||
let _playStartProgress = 0;
|
||||
let _animationFrame = null;
|
||||
let _previousMapStack = null;
|
||||
let _eventMapGeneration = null;
|
||||
let _previousSplitPosition = null;
|
||||
let _comparisonLease = null;
|
||||
let _introTimer = null;
|
||||
let _introGeneration = 0;
|
||||
let _lastSurgePosition = null;
|
||||
let _lastSurgeVisible = null;
|
||||
let _lastSplitCssValue = null;
|
||||
let _presentation = 'standalone';
|
||||
let _sceneBeatId = null;
|
||||
let _sceneBeatReveal = 0.36;
|
||||
@@ -2556,79 +2552,20 @@ export function createBhoteKoshiEventLayer({
|
||||
createFloodDataSource();
|
||||
}
|
||||
|
||||
function ensureSplitLine(event) {
|
||||
_splitLine = document.createElement('div');
|
||||
_splitLine.id = 'bhote-koshi-split-line';
|
||||
const beforeLabel = document.createElement('span');
|
||||
const afterLabel = document.createElement('span');
|
||||
const splitHandle = document.createElement('button');
|
||||
beforeLabel.className = 'before';
|
||||
afterLabel.className = 'after';
|
||||
beforeLabel.textContent = 'A';
|
||||
beforeLabel.title = event.imagery.before.label;
|
||||
afterLabel.textContent = 'B';
|
||||
afterLabel.title = event.imagery.after.label;
|
||||
splitHandle.type = 'button';
|
||||
splitHandle.className = 'bhote-koshi-split-handle';
|
||||
splitHandle.setAttribute('role', 'slider');
|
||||
splitHandle.setAttribute(
|
||||
'aria-label',
|
||||
'Historical reference and post-event image divider',
|
||||
);
|
||||
splitHandle.setAttribute('aria-valuemin', '0');
|
||||
splitHandle.setAttribute('aria-valuemax', '100');
|
||||
splitHandle.setAttribute('aria-orientation', 'horizontal');
|
||||
splitHandle.textContent = '↔';
|
||||
splitHandle.addEventListener('pointerdown', handleSplitPointerDown);
|
||||
splitHandle.addEventListener('pointermove', handleSplitPointerMove);
|
||||
splitHandle.addEventListener('pointerup', finishSplitPointerDrag);
|
||||
splitHandle.addEventListener('pointercancel', finishSplitPointerDrag);
|
||||
splitHandle.addEventListener('keydown', handleSplitKeyDown);
|
||||
_splitHandle = splitHandle;
|
||||
_splitLine.append(beforeLabel, splitHandle, afterLabel);
|
||||
document.body.appendChild(_splitLine);
|
||||
}
|
||||
|
||||
function setSplitFromClientX(clientX) {
|
||||
const viewportWidth =
|
||||
Number(document.documentElement?.clientWidth) ||
|
||||
Number(window.innerWidth) ||
|
||||
0;
|
||||
if (viewportWidth <= 0 || !Number.isFinite(Number(clientX))) return;
|
||||
setSplit(Number(clientX) / viewportWidth);
|
||||
}
|
||||
|
||||
function handleSplitPointerDown(event) {
|
||||
if (event.button != null && event.button !== 0) return;
|
||||
event.preventDefault?.();
|
||||
_splitDragging = true;
|
||||
_splitPointerId = event.pointerId ?? null;
|
||||
_splitHandle?.setPointerCapture?.(event.pointerId);
|
||||
setSplitFromClientX(event.clientX);
|
||||
}
|
||||
|
||||
function handleSplitPointerMove(event) {
|
||||
if (!_splitDragging) return;
|
||||
if (_splitPointerId != null && event.pointerId !== _splitPointerId) return;
|
||||
event.preventDefault?.();
|
||||
setSplitFromClientX(event.clientX);
|
||||
}
|
||||
|
||||
function finishSplitPointerDrag(event) {
|
||||
if (!_splitDragging) return;
|
||||
if (_splitPointerId != null && event.pointerId !== _splitPointerId) return;
|
||||
_splitHandle?.releasePointerCapture?.(event.pointerId);
|
||||
_splitDragging = false;
|
||||
_splitPointerId = null;
|
||||
}
|
||||
|
||||
function handleSplitKeyDown(event) {
|
||||
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
||||
event.preventDefault?.();
|
||||
const step = event.shiftKey ? 0.05 : 0.01;
|
||||
if (event.key === 'Home') setSplit(0);
|
||||
else if (event.key === 'End') setSplit(1);
|
||||
else setSplit(_split + (event.key === 'ArrowRight' ? step : -step));
|
||||
function createSplitControl(event) {
|
||||
_splitControl = createImagerySplit({
|
||||
scene: _viewer.scene,
|
||||
initialValue: _split,
|
||||
id: 'bhote-koshi-split-line',
|
||||
handleClass: 'bhote-koshi-split-handle',
|
||||
cssProperty: '--bhote-koshi-split',
|
||||
beforeTitle: event.imagery.before.label,
|
||||
afterTitle: event.imagery.after.label,
|
||||
ariaLabel: 'Historical reference and post-event image divider',
|
||||
formatValueText: (beforePercent, afterPercent) =>
|
||||
`A historical reference ${beforePercent} percent, B post-event ${afterPercent} percent`,
|
||||
onChange: setSplit,
|
||||
});
|
||||
}
|
||||
|
||||
function applyCinematicCamera() {
|
||||
@@ -3211,7 +3148,7 @@ export function createBhoteKoshiEventLayer({
|
||||
_panelRefs.observedStatus.hidden = !imageryAvailable;
|
||||
if (_panelRefs?.progressInput)
|
||||
_panelRefs.progressInput.hidden = !scrubberAvailable;
|
||||
if (_splitLine) _splitLine.hidden = !imageryAvailable;
|
||||
_splitControl?.setVisible(imageryAvailable);
|
||||
if (_beforeLayer) _beforeLayer.show = imageryAvailable;
|
||||
if (_afterLayer) _afterLayer.show = imageryAvailable;
|
||||
if (_panelRefs?.caveat) {
|
||||
@@ -3328,24 +3265,7 @@ export function createBhoteKoshiEventLayer({
|
||||
|
||||
function setSplit(split) {
|
||||
_split = clampUnit(split);
|
||||
if (_viewer && _viewer.scene.splitPosition !== _split)
|
||||
_viewer.scene.splitPosition = _split;
|
||||
const cssValue = `${_split * 100}%`;
|
||||
if (_lastSplitCssValue !== cssValue) {
|
||||
document.documentElement.style.setProperty(
|
||||
'--bhote-koshi-split',
|
||||
cssValue,
|
||||
);
|
||||
_lastSplitCssValue = cssValue;
|
||||
}
|
||||
if (_splitHandle) {
|
||||
const beforePercent = Math.round(_split * 100);
|
||||
_splitHandle.setAttribute('aria-valuenow', String(beforePercent));
|
||||
_splitHandle.setAttribute(
|
||||
'aria-valuetext',
|
||||
`A historical reference ${beforePercent} percent, B post-event ${100 - beforePercent} percent`,
|
||||
);
|
||||
}
|
||||
_splitControl?.setValue(_split);
|
||||
syncPanel();
|
||||
renderHost.request('bhote-koshi-split');
|
||||
}
|
||||
@@ -3428,7 +3348,6 @@ export function createBhoteKoshiEventLayer({
|
||||
async function enable(viewer, { origin = 'local-restore', signal } = {}) {
|
||||
_viewer = viewer;
|
||||
_enabled = true;
|
||||
_previousSplitPosition = viewer.scene.splitPosition;
|
||||
// Public lifecycle passes signal, not intent origin, to enable. Its atomic
|
||||
// restore has already applied the scene parameters; those own presentation.
|
||||
// A missing origin is passive, never authority to focus or start playback.
|
||||
@@ -3437,24 +3356,20 @@ export function createBhoteKoshiEventLayer({
|
||||
const event = await loadEvent();
|
||||
throwIfEnableCancelled(signal, _enabled);
|
||||
registerDynamicCredit(viewer, BHOTE_KOSHI_CREDIT);
|
||||
_previousMapStack = _mapStackController?.getActiveId?.() || null;
|
||||
if (
|
||||
!sceneDirected &&
|
||||
_mapStackController &&
|
||||
_previousMapStack !== 'esri-imagery'
|
||||
) {
|
||||
await _mapStackController.setStack('esri-imagery');
|
||||
if (_mapStackController) {
|
||||
_comparisonLease = acquireImageryComparison(_mapStackController, {
|
||||
owner: BHOTE_KOSHI_LAYER_ID,
|
||||
switchPolicy: sceneDirected ? 'preserve' : 'esri',
|
||||
});
|
||||
await _comparisonLease.ready;
|
||||
}
|
||||
_eventMapGeneration = sceneDirected
|
||||
? null
|
||||
: (_mapStackController?.getSwitchGeneration?.() ?? null);
|
||||
throwIfEnableCancelled(signal, _enabled);
|
||||
await createImagery(event);
|
||||
throwIfEnableCancelled(signal, _enabled);
|
||||
await syncTerrainComparison();
|
||||
throwIfEnableCancelled(signal, _enabled);
|
||||
createFloodDataSource();
|
||||
ensureSplitLine(event);
|
||||
createSplitControl(event);
|
||||
const panelView = createPanel(event, {
|
||||
close: () =>
|
||||
_dataManager?.setEnabled(BHOTE_KOSHI_LAYER_ID, false, {
|
||||
@@ -3573,17 +3488,8 @@ export function createBhoteKoshiEventLayer({
|
||||
?.classList.remove('bhote-event-active');
|
||||
_panel = null;
|
||||
_panelRefs = null;
|
||||
_splitDragging = false;
|
||||
_splitPointerId = null;
|
||||
_splitLine?.remove();
|
||||
_splitLine = null;
|
||||
_splitHandle = null;
|
||||
document.documentElement.style.removeProperty('--bhote-koshi-split');
|
||||
_lastSplitCssValue = null;
|
||||
if (_viewer?.scene && _previousSplitPosition != null) {
|
||||
_viewer.scene.splitPosition = _previousSplitPosition;
|
||||
}
|
||||
_previousSplitPosition = null;
|
||||
_splitControl?.destroy();
|
||||
_splitControl = null;
|
||||
_progress = 0;
|
||||
_split = 0.5;
|
||||
_presentation = 'standalone';
|
||||
@@ -3601,20 +3507,10 @@ export function createBhoteKoshiEventLayer({
|
||||
cancel?.(_sceneSeekFrame);
|
||||
_sceneSeekFrame = null;
|
||||
}
|
||||
const previousMapStack = _previousMapStack;
|
||||
const eventMapGeneration = _eventMapGeneration;
|
||||
_previousMapStack = null;
|
||||
_eventMapGeneration = null;
|
||||
const comparisonLease = _comparisonLease;
|
||||
_comparisonLease = null;
|
||||
try {
|
||||
if (
|
||||
_mapStackController &&
|
||||
previousMapStack &&
|
||||
previousMapStack !== 'esri-imagery' &&
|
||||
_mapStackController.getActiveId?.() === 'esri-imagery' &&
|
||||
_mapStackController.getSwitchGeneration?.() === eventMapGeneration
|
||||
) {
|
||||
await _mapStackController.setStack(previousMapStack);
|
||||
}
|
||||
await comparisonLease?.release();
|
||||
} finally {
|
||||
renderHost.request('bhote-koshi-disable');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user