mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
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]>
177 lines
5.5 KiB
JavaScript
177 lines
5.5 KiB
JavaScript
const HANDLE_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'Home', 'End']);
|
|
|
|
function clampUnit(value) {
|
|
return Math.max(0, Math.min(1, Number(value) || 0));
|
|
}
|
|
|
|
function defaultViewportWidth() {
|
|
return (
|
|
Number(document.documentElement?.clientWidth) ||
|
|
Number(globalThis.window?.innerWidth) ||
|
|
0
|
|
);
|
|
}
|
|
|
|
function defaultValueText(beforePercent, afterPercent) {
|
|
return `A ${beforePercent} percent, B ${afterPercent} percent`;
|
|
}
|
|
|
|
/**
|
|
* Split divider for before/after imagery comparison.
|
|
*
|
|
* Owns the DOM line + handle, pointer capture, keyboard, ARIA, a CSS custom
|
|
* property and scene.splitPosition. `setValue` is the owner's own write and
|
|
* never echoes through `onChange`; `onChange` reports pointer and keyboard
|
|
* changes so the owner can mirror them, and `requestRender` fires whenever
|
|
* the clamped value actually changes. `destroy` is idempotent: it removes the
|
|
* listeners and line, clears the CSS property and restores the scene split
|
|
* captured at creation once.
|
|
*/
|
|
export function createImagerySplit({
|
|
scene,
|
|
parent = document.body,
|
|
initialValue = 0.5,
|
|
id,
|
|
handleClass,
|
|
cssTarget = document.documentElement,
|
|
cssProperty,
|
|
beforeLabel = 'A',
|
|
afterLabel = 'B',
|
|
beforeTitle,
|
|
afterTitle,
|
|
ariaLabel,
|
|
formatValueText = defaultValueText,
|
|
getViewportWidth = defaultViewportWidth,
|
|
onChange,
|
|
requestRender,
|
|
} = {}) {
|
|
const restoreValue = scene ? (scene.splitPosition ?? null) : null;
|
|
const line = document.createElement('div');
|
|
if (id) line.id = id;
|
|
const before = document.createElement('span');
|
|
const after = document.createElement('span');
|
|
const handle = document.createElement('button');
|
|
before.className = 'before';
|
|
after.className = 'after';
|
|
before.textContent = beforeLabel;
|
|
if (beforeTitle != null) before.title = beforeTitle;
|
|
after.textContent = afterLabel;
|
|
if (afterTitle != null) after.title = afterTitle;
|
|
handle.type = 'button';
|
|
if (handleClass) handle.className = handleClass;
|
|
handle.setAttribute('role', 'slider');
|
|
if (ariaLabel != null) handle.setAttribute('aria-label', ariaLabel);
|
|
handle.setAttribute('aria-valuemin', '0');
|
|
handle.setAttribute('aria-valuemax', '100');
|
|
handle.setAttribute('aria-orientation', 'horizontal');
|
|
handle.textContent = '↔';
|
|
|
|
let value = null;
|
|
let cssValue = null;
|
|
let dragging = false;
|
|
let pointerId = null;
|
|
let destroyed = false;
|
|
|
|
function apply(next, { notify = false, render = true } = {}) {
|
|
const clamped = clampUnit(next);
|
|
const changed = clamped !== value;
|
|
value = clamped;
|
|
if (scene && scene.splitPosition !== value) scene.splitPosition = value;
|
|
const css = `${value * 100}%`;
|
|
if (cssProperty && cssValue !== css) {
|
|
cssTarget.style.setProperty(cssProperty, css);
|
|
cssValue = css;
|
|
}
|
|
const beforePercent = Math.round(value * 100);
|
|
handle.setAttribute('aria-valuenow', String(beforePercent));
|
|
handle.setAttribute(
|
|
'aria-valuetext',
|
|
formatValueText(beforePercent, 100 - beforePercent),
|
|
);
|
|
if (changed && render) requestRender?.();
|
|
if (changed && notify) onChange?.(value);
|
|
return changed;
|
|
}
|
|
|
|
function setFromClientX(clientX) {
|
|
const viewportWidth = Number(getViewportWidth()) || 0;
|
|
if (viewportWidth <= 0 || !Number.isFinite(Number(clientX))) return;
|
|
apply(Number(clientX) / viewportWidth, { notify: true });
|
|
}
|
|
|
|
function handlePointerDown(event) {
|
|
if (event.button != null && event.button !== 0) return;
|
|
event.preventDefault?.();
|
|
dragging = true;
|
|
pointerId = event.pointerId ?? null;
|
|
handle.setPointerCapture?.(event.pointerId);
|
|
setFromClientX(event.clientX);
|
|
}
|
|
|
|
function handlePointerMove(event) {
|
|
if (!dragging) return;
|
|
if (pointerId != null && event.pointerId !== pointerId) return;
|
|
event.preventDefault?.();
|
|
setFromClientX(event.clientX);
|
|
}
|
|
|
|
function finishPointerDrag(event) {
|
|
if (!dragging) return;
|
|
if (pointerId != null && event.pointerId !== pointerId) return;
|
|
handle.releasePointerCapture?.(event.pointerId);
|
|
dragging = false;
|
|
pointerId = null;
|
|
}
|
|
|
|
function handleKeyDown(event) {
|
|
if (!HANDLE_KEYS.has(event.key)) return;
|
|
event.preventDefault?.();
|
|
const step = event.shiftKey ? 0.05 : 0.01;
|
|
if (event.key === 'Home') apply(0, { notify: true });
|
|
else if (event.key === 'End') apply(1, { notify: true });
|
|
else
|
|
apply(value + (event.key === 'ArrowRight' ? step : -step), {
|
|
notify: true,
|
|
});
|
|
}
|
|
|
|
const listeners = [
|
|
['pointerdown', handlePointerDown],
|
|
['pointermove', handlePointerMove],
|
|
['pointerup', finishPointerDrag],
|
|
['pointercancel', finishPointerDrag],
|
|
['keydown', handleKeyDown],
|
|
];
|
|
for (const [type, listener] of listeners)
|
|
handle.addEventListener(type, listener);
|
|
line.append(before, handle, after);
|
|
parent.appendChild(line);
|
|
apply(initialValue, { render: false });
|
|
|
|
return {
|
|
getValue() {
|
|
return value;
|
|
},
|
|
setValue(next) {
|
|
if (destroyed) return false;
|
|
return apply(next);
|
|
},
|
|
setVisible(visible) {
|
|
line.hidden = !visible;
|
|
},
|
|
destroy() {
|
|
if (destroyed) return;
|
|
destroyed = true;
|
|
if (dragging && pointerId != null)
|
|
handle.releasePointerCapture?.(pointerId);
|
|
dragging = false;
|
|
pointerId = null;
|
|
for (const [type, listener] of listeners)
|
|
handle.removeEventListener?.(type, listener);
|
|
line.remove();
|
|
if (cssProperty) cssTarget.style.removeProperty(cssProperty);
|
|
if (scene && restoreValue != null) scene.splitPosition = restoreValue;
|
|
},
|
|
};
|
|
}
|