mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix: cancel Nepal media preloads and respect drawing ownership
Retain active-flight preloading while canceling hidden frames and pending Facebook sessions on stop, disable, and replacement. Add lifecycle and pointer-ownership regression coverage. Apply the runtime formatting adopted on main and keep the existing source-shape check insensitive to that formatting.
This commit is contained in:
1 parent
1a936ebac1
commit
a9482f0a5b
15 files changed
+3584
-1803
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
- Cancel hidden Nepal provider preloads on Stop, event disable and replacement, and respect drawing-tool pointer ownership for fallback evidence cards. Preserve @manjunath22466’s Nepal scene contribution and source attribution.
|
||||||
|
|
||||||
- Cancel the Nepal Upper Valley locator's pending approach and orbit on scene Stop, replacement, seek, and teardown; late camera callbacks cannot take over a newer shot.
|
- Cancel the Nepal Upper Valley locator's pending approach and orbit on scene Stop, replacement, seek, and teardown; late camera callbacks cannot take over a newer shot.
|
||||||
|
|
||||||
- Keep completed flood history visible when later Nepal media-only shots arrive and reveal their source cards.
|
- Keep completed flood history visible when later Nepal media-only shots arrive and reveal their source cards.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# God's Eye View Current State
|
# God's Eye View Current State
|
||||||
|
|
||||||
|
Nepal media preloads survive repeated camera-flight updates, but Stop, event disable and replacement remove abandoned frames and revoke pending Facebook sessions. Fallback evidence-card clicks respect the shared drawing-tool pointer lease.
|
||||||
|
|
||||||
Nepal flood lines classify the active terrain or photoreal surface, and the surge marker follows that surface. Geographic route coordinates are unchanged; dynamic lines do not rely on unsupported depth-failure materials. Later media-only shots keep their completed source-path history throughout arrival and card reveal.
|
Nepal flood lines classify the active terrain or photoreal surface, and the surge marker follows that surface. Geographic route coordinates are unchanged; dynamic lines do not rely on unsupported depth-failure materials. Later media-only shots keep their completed source-path history throughout arrival and card reveal.
|
||||||
|
|
||||||
Scene Stop, replacement, seek, and teardown revoke the Upper Valley locator's delayed camera approach and orbit while preserving its visible callout. A revoked timer or camera callback cannot start another flight or interfere with a newer shot.
|
Scene Stop, replacement, seek, and teardown revoke the Upper Valley locator's delayed camera approach and orbit while preserving its visible callout. A revoked timer or camera callback cannot start another flight or interfere with a newer shot.
|
||||||
|
|||||||
+352
-175
@@ -26,7 +26,8 @@ const X_HOSTS = new Set([
|
|||||||
]);
|
]);
|
||||||
const X_WIDGET_URL = 'https://platform.twitter.com/widgets.js';
|
const X_WIDGET_URL = 'https://platform.twitter.com/widgets.js';
|
||||||
const FACEBOOK_SDK_ID = 'facebook-jssdk';
|
const FACEBOOK_SDK_ID = 'facebook-jssdk';
|
||||||
const FACEBOOK_SDK_URL = 'https://connect.facebook.net/en_US/sdk.js#xfbml=0&version=v24.0';
|
const FACEBOOK_SDK_URL =
|
||||||
|
'https://connect.facebook.net/en_US/sdk.js#xfbml=0&version=v24.0';
|
||||||
const EMBED_TIMEOUT_MS = 15000;
|
const EMBED_TIMEOUT_MS = 15000;
|
||||||
const CARD_EXIT_DURATION_MS = 620;
|
const CARD_EXIT_DURATION_MS = 620;
|
||||||
const MEDIA_PRECONNECT_ORIGINS = Object.freeze({
|
const MEDIA_PRECONNECT_ORIGINS = Object.freeze({
|
||||||
@@ -44,7 +45,8 @@ let _facebookEmbedSequence = 0;
|
|||||||
const youtubeApis = new WeakMap();
|
const youtubeApis = new WeakMap();
|
||||||
|
|
||||||
function loadYouTubeApi(documentRef, globalRef) {
|
function loadYouTubeApi(documentRef, globalRef) {
|
||||||
if (typeof globalRef.YT?.Player === 'function') return Promise.resolve(globalRef.YT);
|
if (typeof globalRef.YT?.Player === 'function')
|
||||||
|
return Promise.resolve(globalRef.YT);
|
||||||
if (youtubeApis.has(documentRef)) return youtubeApis.get(documentRef);
|
if (youtubeApis.has(documentRef)) return youtubeApis.get(documentRef);
|
||||||
const pending = new Promise((resolve, reject) => {
|
const pending = new Promise((resolve, reject) => {
|
||||||
const previous = globalRef.onYouTubeIframeAPIReady;
|
const previous = globalRef.onYouTubeIframeAPIReady;
|
||||||
@@ -55,30 +57,54 @@ function loadYouTubeApi(documentRef, globalRef) {
|
|||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
globalRef.clearTimeout(timer);
|
globalRef.clearTimeout(timer);
|
||||||
if (globalRef.onYouTubeIframeAPIReady === ready) globalRef.onYouTubeIframeAPIReady = previous;
|
if (globalRef.onYouTubeIframeAPIReady === ready)
|
||||||
if (error) { script.remove(); reject(error); }
|
globalRef.onYouTubeIframeAPIReady = previous;
|
||||||
else resolve(globalRef.YT);
|
if (error) {
|
||||||
|
script.remove();
|
||||||
|
reject(error);
|
||||||
|
} else resolve(globalRef.YT);
|
||||||
};
|
};
|
||||||
const ready = () => {
|
const ready = () => {
|
||||||
try { previous?.(); } finally {
|
try {
|
||||||
finish(typeof globalRef.YT?.Player === 'function' ? null : new Error('YouTube API unavailable'));
|
previous?.();
|
||||||
|
} finally {
|
||||||
|
finish(
|
||||||
|
typeof globalRef.YT?.Player === 'function'
|
||||||
|
? null
|
||||||
|
: new Error('YouTube API unavailable'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
globalRef.onYouTubeIframeAPIReady = ready;
|
globalRef.onYouTubeIframeAPIReady = ready;
|
||||||
script.src = 'https://www.youtube.com/iframe_api';
|
script.src = 'https://www.youtube.com/iframe_api';
|
||||||
script.async = true;
|
script.async = true;
|
||||||
script.addEventListener('error', () => finish(new Error('YouTube API unavailable')), { once: true });
|
script.addEventListener(
|
||||||
timer = globalRef.setTimeout(() => finish(new Error('YouTube API timed out')), 5000);
|
'error',
|
||||||
|
() => finish(new Error('YouTube API unavailable')),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
timer = globalRef.setTimeout(
|
||||||
|
() => finish(new Error('YouTube API timed out')),
|
||||||
|
5000,
|
||||||
|
);
|
||||||
documentRef.head.append(script);
|
documentRef.head.append(script);
|
||||||
}).catch((error) => { youtubeApis.delete(documentRef); throw error; });
|
}).catch((error) => {
|
||||||
|
youtubeApis.delete(documentRef);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
youtubeApis.set(documentRef, pending);
|
youtubeApis.set(documentRef, pending);
|
||||||
return pending;
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A trimmed clip follows provider time, never time spent loading its iframe. */
|
/** A trimmed clip follows provider time, never time spent loading its iframe. */
|
||||||
export function createTrimmedYouTubePlayback({
|
export function createTrimmedYouTubePlayback({
|
||||||
iframe, endAtSec, startAtSec = 0, loadApi, globalRef = globalThis,
|
iframe,
|
||||||
isCurrent = () => true, onStatus = () => {},
|
endAtSec,
|
||||||
|
startAtSec = 0,
|
||||||
|
loadApi,
|
||||||
|
globalRef = globalThis,
|
||||||
|
isCurrent = () => true,
|
||||||
|
onStatus = () => {},
|
||||||
}) {
|
}) {
|
||||||
let player = null;
|
let player = null;
|
||||||
let phase = 'starting';
|
let phase = 'starting';
|
||||||
@@ -87,7 +113,8 @@ export function createTrimmedYouTubePlayback({
|
|||||||
let currentTime = startAtSec;
|
let currentTime = startAtSec;
|
||||||
let poll = null;
|
let poll = null;
|
||||||
let deadline = null;
|
let deadline = null;
|
||||||
const live = () => active && isCurrent() && ['starting', 'playing'].includes(phase);
|
const live = () =>
|
||||||
|
active && isCurrent() && ['starting', 'playing'].includes(phase);
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
globalRef.clearTimeout(deadline);
|
globalRef.clearTimeout(deadline);
|
||||||
globalRef.clearTimeout(poll);
|
globalRef.clearTimeout(poll);
|
||||||
@@ -97,7 +124,11 @@ export function createTrimmedYouTubePlayback({
|
|||||||
if (!live()) return;
|
if (!live()) return;
|
||||||
phase = outcome;
|
phase = outcome;
|
||||||
clear();
|
clear();
|
||||||
try { player?.pauseVideo?.(); } catch { /* Removed provider frame. */ }
|
try {
|
||||||
|
player?.pauseVideo?.();
|
||||||
|
} catch {
|
||||||
|
/* Removed provider frame. */
|
||||||
|
}
|
||||||
onStatus(outcome);
|
onStatus(outcome);
|
||||||
};
|
};
|
||||||
const sample = (stateHint) => {
|
const sample = (stateHint) => {
|
||||||
@@ -109,55 +140,84 @@ export function createTrimmedYouTubePlayback({
|
|||||||
const time = player?.getCurrentTime?.();
|
const time = player?.getCurrentTime?.();
|
||||||
if (Number.isFinite(time)) currentTime = time;
|
if (Number.isFinite(time)) currentTime = time;
|
||||||
const state = stateHint ?? player?.getPlayerState?.();
|
const state = stateHint ?? player?.getPlayerState?.();
|
||||||
if (currentTime >= endAtSec || (state === 0 && currentTime >= endAtSec - 0.1)) {
|
if (
|
||||||
finish('completed'); return;
|
currentTime >= endAtSec ||
|
||||||
|
(state === 0 && currentTime >= endAtSec - 0.1)
|
||||||
|
) {
|
||||||
|
finish('completed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state === 0) {
|
||||||
|
finish('unavailable');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (state === 0) { finish('unavailable'); return; }
|
|
||||||
// Autoplay can begin before the API attaches, without a new PLAYING edge.
|
// Autoplay can begin before the API attaches, without a new PLAYING edge.
|
||||||
if (!started && (state === 1 || currentTime > previousTime)) {
|
if (!started && (state === 1 || currentTime > previousTime)) {
|
||||||
started = true;
|
started = true;
|
||||||
phase = 'playing';
|
phase = 'playing';
|
||||||
globalRef.clearTimeout(deadline);
|
globalRef.clearTimeout(deadline);
|
||||||
deadline = globalRef.setTimeout(() => finish('timeout'), (endAtSec - startAtSec) * 1000 + 5000);
|
deadline = globalRef.setTimeout(
|
||||||
|
() => finish('timeout'),
|
||||||
|
(endAtSec - startAtSec) * 1000 + 5000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch { finish('unavailable'); return; }
|
} catch {
|
||||||
|
finish('unavailable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
poll = globalRef.setTimeout(() => sample(), 100);
|
poll = globalRef.setTimeout(() => sample(), 100);
|
||||||
};
|
};
|
||||||
deadline = globalRef.setTimeout(() => finish('timeout'), 5000);
|
deadline = globalRef.setTimeout(() => finish('timeout'), 5000);
|
||||||
Promise.resolve().then(loadApi).then((api) => {
|
Promise.resolve()
|
||||||
if (!live()) return;
|
.then(loadApi)
|
||||||
player = new api.Player(iframe, { events: {
|
.then((api) => {
|
||||||
onReady(event) {
|
if (!live()) return;
|
||||||
if (!live()) return;
|
player = new api.Player(iframe, {
|
||||||
player = event.target;
|
events: {
|
||||||
sample();
|
onReady(event) {
|
||||||
if (!live()) return;
|
if (!live()) return;
|
||||||
try { event.target.mute(); event.target.playVideo(); } catch { finish('unavailable'); }
|
player = event.target;
|
||||||
},
|
sample();
|
||||||
onStateChange(event) {
|
if (!live()) return;
|
||||||
if (!live()) return;
|
try {
|
||||||
if (event.target) player = event.target;
|
event.target.mute();
|
||||||
sample(event.data);
|
event.target.playVideo();
|
||||||
},
|
} catch {
|
||||||
onError: () => finish('unavailable'),
|
finish('unavailable');
|
||||||
onAutoplayBlocked: () => finish('blocked'),
|
}
|
||||||
} });
|
},
|
||||||
}).catch(() => finish('unavailable'));
|
onStateChange(event) {
|
||||||
|
if (!live()) return;
|
||||||
|
if (event.target) player = event.target;
|
||||||
|
sample(event.data);
|
||||||
|
},
|
||||||
|
onError: () => finish('unavailable'),
|
||||||
|
onAutoplayBlocked: () => finish('blocked'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => finish('unavailable'));
|
||||||
return {
|
return {
|
||||||
getState: () => ({ phase, currentTime }),
|
getState: () => ({ phase, currentTime }),
|
||||||
destroy() {
|
destroy() {
|
||||||
active = false;
|
active = false;
|
||||||
phase = 'cancelled';
|
phase = 'cancelled';
|
||||||
clear();
|
clear();
|
||||||
try { player?.destroy?.(); } catch { /* Detached frame already released. */ }
|
try {
|
||||||
|
player?.destroy?.();
|
||||||
|
} catch {
|
||||||
|
/* Detached frame already released. */
|
||||||
|
}
|
||||||
player = null;
|
player = null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function facebookSdkReady(api) {
|
function facebookSdkReady(api) {
|
||||||
return typeof api?.XFBML?.parse === 'function'
|
return (
|
||||||
&& typeof api?.Event?.subscribe === 'function';
|
typeof api?.XFBML?.parse === 'function' &&
|
||||||
|
typeof api?.Event?.subscribe === 'function'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFacebookSdk(documentRef, globalRef = globalThis) {
|
function loadFacebookSdk(documentRef, globalRef = globalThis) {
|
||||||
@@ -199,10 +259,11 @@ function loadFacebookSdk(documentRef, globalRef = globalThis) {
|
|||||||
() => finish(new Error('Facebook player API could not load.')),
|
() => finish(new Error('Facebook player API could not load.')),
|
||||||
{ once: true },
|
{ once: true },
|
||||||
);
|
);
|
||||||
timer = globalRef.setTimeout?.(
|
timer =
|
||||||
() => finish(new Error('Facebook player API timed out.')),
|
globalRef.setTimeout?.(
|
||||||
EMBED_TIMEOUT_MS,
|
() => finish(new Error('Facebook player API timed out.')),
|
||||||
) ?? null;
|
EMBED_TIMEOUT_MS,
|
||||||
|
) ?? null;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
_facebookSdkPromise = null;
|
_facebookSdkPromise = null;
|
||||||
throw error;
|
throw error;
|
||||||
@@ -221,22 +282,24 @@ export function publicEmbeddedMediaUrl(value) {
|
|||||||
}
|
}
|
||||||
const host = url.hostname.toLowerCase();
|
const host = url.hostname.toLowerCase();
|
||||||
if (
|
if (
|
||||||
url.protocol !== 'https:'
|
url.protocol !== 'https:' ||
|
||||||
|| url.username
|
url.username ||
|
||||||
|| url.password
|
url.password ||
|
||||||
|| (url.port && url.port !== '443')
|
(url.port && url.port !== '443') ||
|
||||||
|| !host.includes('.')
|
!host.includes('.') ||
|
||||||
|| host.includes(':')
|
host.includes(':') ||
|
||||||
|| /^\d+(\.\d+){3}$/.test(host)
|
/^\d+(\.\d+){3}$/.test(host) ||
|
||||||
|| /(^|\.)(localhost|local|internal|test|invalid)$/.test(host)
|
/(^|\.)(localhost|local|internal|test|invalid)$/.test(host)
|
||||||
) return null;
|
)
|
||||||
|
return null;
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function youtubeVideoId(url) {
|
function youtubeVideoId(url) {
|
||||||
const host = url.hostname.toLowerCase();
|
const host = url.hostname.toLowerCase();
|
||||||
if (!YOUTUBE_HOSTS.has(host)) return null;
|
if (!YOUTUBE_HOSTS.has(host)) return null;
|
||||||
if (host === 'youtu.be') return /^\/([\w-]{11})\/?$/.exec(url.pathname)?.[1] || null;
|
if (host === 'youtu.be')
|
||||||
|
return /^\/([\w-]{11})\/?$/.exec(url.pathname)?.[1] || null;
|
||||||
if (url.pathname === '/watch') return url.searchParams.get('v');
|
if (url.pathname === '/watch') return url.searchParams.get('v');
|
||||||
return /^\/(?:embed|shorts)\/([\w-]{11})\/?$/.exec(url.pathname)?.[1] || null;
|
return /^\/(?:embed|shorts)\/([\w-]{11})\/?$/.exec(url.pathname)?.[1] || null;
|
||||||
}
|
}
|
||||||
@@ -244,16 +307,21 @@ function youtubeVideoId(url) {
|
|||||||
function isFacebookVideoUrl(url) {
|
function isFacebookVideoUrl(url) {
|
||||||
const host = url.hostname.toLowerCase();
|
const host = url.hostname.toLowerCase();
|
||||||
if (!FACEBOOK_HOSTS.has(host)) return false;
|
if (!FACEBOOK_HOSTS.has(host)) return false;
|
||||||
if (host === 'fb.watch' || host === 'www.fb.watch') return /^\/[\w.-]+\/?$/.test(url.pathname);
|
if (host === 'fb.watch' || host === 'www.fb.watch')
|
||||||
return /^\/reel\/[\w.-]+\/?$/.test(url.pathname)
|
return /^\/[\w.-]+\/?$/.test(url.pathname);
|
||||||
|| (url.pathname === '/watch/' && Boolean(url.searchParams.get('v')))
|
return (
|
||||||
|| /\/videos\/[\w.-]+\/?$/.test(url.pathname)
|
/^\/reel\/[\w.-]+\/?$/.test(url.pathname) ||
|
||||||
|| /^\/share\/(?:v|r)\/[\w.-]+\/?$/.test(url.pathname);
|
(url.pathname === '/watch/' && Boolean(url.searchParams.get('v'))) ||
|
||||||
|
/\/videos\/[\w.-]+\/?$/.test(url.pathname) ||
|
||||||
|
/^\/share\/(?:v|r)\/[\w.-]+\/?$/.test(url.pathname)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function xPostId(url) {
|
function xPostId(url) {
|
||||||
if (!X_HOSTS.has(url.hostname.toLowerCase())) return null;
|
if (!X_HOSTS.has(url.hostname.toLowerCase())) return null;
|
||||||
return /^\/[A-Za-z0-9_]{1,15}\/status\/(\d+)\/?$/.exec(url.pathname)?.[1] || null;
|
return (
|
||||||
|
/^\/[A-Za-z0-9_]{1,15}\/status\/(\d+)\/?$/.exec(url.pathname)?.[1] || null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve only providers that can render inside a Bhote Koshi callout. */
|
/** Resolve only providers that can render inside a Bhote Koshi callout. */
|
||||||
@@ -279,11 +347,10 @@ export function resolveEmbeddedMediaSource(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Provider-owned iframe URL for YouTube or Facebook callouts. */
|
/** Provider-owned iframe URL for YouTube or Facebook callouts. */
|
||||||
export function embeddedMediaFrameUrl(source, {
|
export function embeddedMediaFrameUrl(
|
||||||
autoplay = false,
|
source,
|
||||||
startAtSec = 0,
|
{ autoplay = false, startAtSec = 0, endAtSec = null } = {},
|
||||||
endAtSec = null,
|
) {
|
||||||
} = {}) {
|
|
||||||
if (source?.provider === 'youtube') {
|
if (source?.provider === 'youtube') {
|
||||||
const url = new URL(`https://www.youtube-nocookie.com/embed/${source.id}`);
|
const url = new URL(`https://www.youtube-nocookie.com/embed/${source.id}`);
|
||||||
url.searchParams.set('playsinline', '1');
|
url.searchParams.set('playsinline', '1');
|
||||||
@@ -342,24 +409,40 @@ function loadXWidgets(documentRef, globalRef = globalThis) {
|
|||||||
script.dataset.gevXWidgets = 'true';
|
script.dataset.gevXWidgets = 'true';
|
||||||
documentRef.head?.append(script);
|
documentRef.head?.append(script);
|
||||||
}
|
}
|
||||||
script.addEventListener?.('error', () => finish(new Error('X embed could not load.')), {
|
script.addEventListener?.(
|
||||||
once: true,
|
'error',
|
||||||
});
|
() => finish(new Error('X embed could not load.')),
|
||||||
|
{
|
||||||
|
once: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
const waitForApi = () => {
|
const waitForApi = () => {
|
||||||
if (xWidgetsReady(globalRef.twttr)) {
|
if (xWidgetsReady(globalRef.twttr)) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof globalRef.twttr?.ready === 'function') globalRef.twttr.ready(ready);
|
if (typeof globalRef.twttr?.ready === 'function')
|
||||||
|
globalRef.twttr.ready(ready);
|
||||||
else ready();
|
else ready();
|
||||||
};
|
};
|
||||||
script.addEventListener?.('load', waitForApi, { once: true });
|
script.addEventListener?.('load', waitForApi, { once: true });
|
||||||
if (typeof globalRef.twttr?.ready === 'function') globalRef.twttr.ready(ready);
|
if (typeof globalRef.twttr?.ready === 'function')
|
||||||
|
globalRef.twttr.ready(ready);
|
||||||
if (script.dataset?.gevXLoaded === 'true') waitForApi();
|
if (script.dataset?.gevXLoaded === 'true') waitForApi();
|
||||||
else script.addEventListener?.('load', () => { script.dataset.gevXLoaded = 'true'; }, {
|
else
|
||||||
once: true,
|
script.addEventListener?.(
|
||||||
});
|
'load',
|
||||||
timer = setTimeout(() => finish(new Error('X embed timed out.')), EMBED_TIMEOUT_MS);
|
() => {
|
||||||
|
script.dataset.gevXLoaded = 'true';
|
||||||
|
},
|
||||||
|
{
|
||||||
|
once: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
timer = setTimeout(
|
||||||
|
() => finish(new Error('X embed timed out.')),
|
||||||
|
EMBED_TIMEOUT_MS,
|
||||||
|
);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
_xWidgetsPromise = null;
|
_xWidgetsPromise = null;
|
||||||
throw error;
|
throw error;
|
||||||
@@ -368,19 +451,26 @@ function loadXWidgets(documentRef, globalRef = globalThis) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function providerLabel(provider) {
|
function providerLabel(provider) {
|
||||||
return ({ youtube: 'YOUTUBE', facebook: 'FACEBOOK', x: 'X' })[provider] || 'SOURCE';
|
return (
|
||||||
|
{ youtube: 'YOUTUBE', facebook: 'FACEBOOK', x: 'X' }[provider] || 'SOURCE'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPortraitMedia(media = {}) {
|
function isPortraitMedia(media = {}) {
|
||||||
const width = Number(media.width);
|
const width = Number(media.width);
|
||||||
const height = Number(media.height);
|
const height = Number(media.height);
|
||||||
return String(media.orientation || '').toLowerCase() === 'portrait'
|
return (
|
||||||
|| (Number.isFinite(width) && Number.isFinite(height) && height > width);
|
String(media.orientation || '').toLowerCase() === 'portrait' ||
|
||||||
|
(Number.isFinite(width) && Number.isFinite(height) && height > width)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureProviderConnections(documentRef, provider) {
|
function ensureProviderConnections(documentRef, provider) {
|
||||||
for (const origin of MEDIA_PRECONNECT_ORIGINS[provider] || []) {
|
for (const origin of MEDIA_PRECONNECT_ORIGINS[provider] || []) {
|
||||||
if (documentRef.querySelector?.(`link[data-gev-media-preconnect="${origin}"]`)) continue;
|
if (
|
||||||
|
documentRef.querySelector?.(`link[data-gev-media-preconnect="${origin}"]`)
|
||||||
|
)
|
||||||
|
continue;
|
||||||
const link = documentRef.createElement('link');
|
const link = documentRef.createElement('link');
|
||||||
link.rel = 'preconnect';
|
link.rel = 'preconnect';
|
||||||
link.href = origin;
|
link.href = origin;
|
||||||
@@ -394,17 +484,30 @@ function anchorOnScreen(viewer, anchor, root) {
|
|||||||
if (!viewer?.scene || !anchor || !root) return null;
|
if (!viewer?.scene || !anchor || !root) return null;
|
||||||
const camera = viewer.camera;
|
const camera = viewer.camera;
|
||||||
if (camera?.positionWC && camera?.directionWC) {
|
if (camera?.positionWC && camera?.directionWC) {
|
||||||
const delta = Cesium.Cartesian3.subtract(anchor, camera.positionWC, new Cesium.Cartesian3());
|
const delta = Cesium.Cartesian3.subtract(
|
||||||
|
anchor,
|
||||||
|
camera.positionWC,
|
||||||
|
new Cesium.Cartesian3(),
|
||||||
|
);
|
||||||
if (Cesium.Cartesian3.dot(delta, camera.directionWC) <= 0) return null;
|
if (Cesium.Cartesian3.dot(delta, camera.directionWC) <= 0) return null;
|
||||||
}
|
}
|
||||||
const pixel = Cesium.SceneTransforms.worldToWindowCoordinates(viewer.scene, anchor);
|
const pixel = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||||
|
viewer.scene,
|
||||||
|
anchor,
|
||||||
|
);
|
||||||
if (!pixel) return null;
|
if (!pixel) return null;
|
||||||
const canvasBounds = viewer.canvas?.getBoundingClientRect?.();
|
const canvasBounds = viewer.canvas?.getBoundingClientRect?.();
|
||||||
const rootBounds = root.getBoundingClientRect?.();
|
const rootBounds = root.getBoundingClientRect?.();
|
||||||
if (!canvasBounds || !rootBounds) return null;
|
if (!canvasBounds || !rootBounds) return null;
|
||||||
const x = pixel.x + canvasBounds.left - rootBounds.left;
|
const x = pixel.x + canvasBounds.left - rootBounds.left;
|
||||||
const y = pixel.y + canvasBounds.top - rootBounds.top;
|
const y = pixel.y + canvasBounds.top - rootBounds.top;
|
||||||
if (x < -40 || y < -40 || x > rootBounds.width + 40 || y > rootBounds.height + 40) return null;
|
if (
|
||||||
|
x < -40 ||
|
||||||
|
y < -40 ||
|
||||||
|
x > rootBounds.width + 40 ||
|
||||||
|
y > rootBounds.height + 40
|
||||||
|
)
|
||||||
|
return null;
|
||||||
return { x, y, width: rootBounds.width, height: rootBounds.height };
|
return { x, y, width: rootBounds.width, height: rootBounds.height };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,15 +522,27 @@ function placeCard(card, leader, anchor) {
|
|||||||
let laneRight = anchor.width - margin;
|
let laneRight = anchor.width - margin;
|
||||||
let laneBottom = anchor.height - margin;
|
let laneBottom = anchor.height - margin;
|
||||||
if (rootBounds && documentRef?.querySelector) {
|
if (rootBounds && documentRef?.querySelector) {
|
||||||
const leftPanel = documentRef.querySelector('#scene-panel')?.getBoundingClientRect?.();
|
const leftPanel = documentRef
|
||||||
const rightRail = documentRef.querySelector('#right-context-rail')?.getBoundingClientRect?.();
|
.querySelector('#scene-panel')
|
||||||
const commandDock = documentRef.querySelector('#command-dock')?.getBoundingClientRect?.();
|
?.getBoundingClientRect?.();
|
||||||
if (leftPanel?.width > 0) laneLeft = Math.max(laneLeft, leftPanel.right - rootBounds.left + margin);
|
const rightRail = documentRef
|
||||||
if (rightRail?.width > 0) laneRight = Math.min(laneRight, rightRail.left - rootBounds.left - margin);
|
.querySelector('#right-context-rail')
|
||||||
if (commandDock?.height > 0) laneBottom = Math.min(
|
?.getBoundingClientRect?.();
|
||||||
laneBottom,
|
const commandDock = documentRef
|
||||||
commandDock.top - rootBounds.top - margin,
|
.querySelector('#command-dock')
|
||||||
);
|
?.getBoundingClientRect?.();
|
||||||
|
if (leftPanel?.width > 0)
|
||||||
|
laneLeft = Math.max(laneLeft, leftPanel.right - rootBounds.left + margin);
|
||||||
|
if (rightRail?.width > 0)
|
||||||
|
laneRight = Math.min(
|
||||||
|
laneRight,
|
||||||
|
rightRail.left - rootBounds.left - margin,
|
||||||
|
);
|
||||||
|
if (commandDock?.height > 0)
|
||||||
|
laneBottom = Math.min(
|
||||||
|
laneBottom,
|
||||||
|
commandDock.top - rootBounds.top - margin,
|
||||||
|
);
|
||||||
if (laneRight - laneLeft < width) {
|
if (laneRight - laneLeft < width) {
|
||||||
laneLeft = margin;
|
laneLeft = margin;
|
||||||
laneRight = anchor.width - margin;
|
laneRight = anchor.width - margin;
|
||||||
@@ -437,11 +552,13 @@ function placeCard(card, leader, anchor) {
|
|||||||
if (left + width > laneRight) left = anchor.x - width - gap;
|
if (left + width > laneRight) left = anchor.x - width - gap;
|
||||||
left = Math.max(laneLeft, Math.min(laneRight - width, left));
|
left = Math.max(laneLeft, Math.min(laneRight - width, left));
|
||||||
const belowAnchor = anchor.y + gap;
|
const belowAnchor = anchor.y + gap;
|
||||||
const prefersBelow = card.classList.contains('is-youtube')
|
const prefersBelow =
|
||||||
|| card.classList.contains('is-portrait');
|
card.classList.contains('is-youtube') ||
|
||||||
const desiredTop = prefersBelow && belowAnchor + height <= laneBottom
|
card.classList.contains('is-portrait');
|
||||||
? belowAnchor
|
const desiredTop =
|
||||||
: anchor.y - height * 0.42;
|
prefersBelow && belowAnchor + height <= laneBottom
|
||||||
|
? belowAnchor
|
||||||
|
: anchor.y - height * 0.42;
|
||||||
const top = Math.max(margin, Math.min(laneBottom - height, desiredTop));
|
const top = Math.max(margin, Math.min(laneBottom - height, desiredTop));
|
||||||
card.style.left = `${left}px`;
|
card.style.left = `${left}px`;
|
||||||
card.style.top = `${top}px`;
|
card.style.top = `${top}px`;
|
||||||
@@ -449,7 +566,9 @@ function placeCard(card, leader, anchor) {
|
|||||||
const localY = anchor.y - top;
|
const localY = anchor.y - top;
|
||||||
const edgeX = localX < 0 ? 0 : width;
|
const edgeX = localX < 0 ? 0 : width;
|
||||||
const edgeY = Math.max(16, Math.min(height - 16, localY));
|
const edgeY = Math.max(16, Math.min(height - 16, localY));
|
||||||
leader.querySelector('path')?.setAttribute('d', `M ${localX} ${localY} L ${edgeX} ${edgeY}`);
|
leader
|
||||||
|
.querySelector('path')
|
||||||
|
?.setAttribute('d', `M ${localX} ${localY} L ${edgeX} ${edgeY}`);
|
||||||
const dot = leader.querySelector('circle');
|
const dot = leader.querySelector('circle');
|
||||||
dot?.setAttribute('cx', String(localX));
|
dot?.setAttribute('cx', String(localX));
|
||||||
dot?.setAttribute('cy', String(localY));
|
dot?.setAttribute('cy', String(localY));
|
||||||
@@ -492,9 +611,9 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
let generation = 0;
|
let generation = 0;
|
||||||
const leavingRecords = new Set();
|
const leavingRecords = new Set();
|
||||||
|
|
||||||
const prefersReducedMotion = () => (
|
const prefersReducedMotion = () =>
|
||||||
globalRef.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true
|
globalRef.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ===
|
||||||
);
|
true;
|
||||||
const scheduleFrame = (callback) => {
|
const scheduleFrame = (callback) => {
|
||||||
if (typeof globalRef.requestAnimationFrame === 'function') {
|
if (typeof globalRef.requestAnimationFrame === 'function') {
|
||||||
return { kind: 'frame', id: globalRef.requestAnimationFrame(callback) };
|
return { kind: 'frame', id: globalRef.requestAnimationFrame(callback) };
|
||||||
@@ -507,7 +626,8 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
};
|
};
|
||||||
const cancelScheduledFrame = (scheduled) => {
|
const cancelScheduledFrame = (scheduled) => {
|
||||||
if (!scheduled) return;
|
if (!scheduled) return;
|
||||||
if (scheduled.kind === 'frame') globalRef.cancelAnimationFrame?.(scheduled.id);
|
if (scheduled.kind === 'frame')
|
||||||
|
globalRef.cancelAnimationFrame?.(scheduled.id);
|
||||||
else globalRef.clearTimeout?.(scheduled.id);
|
else globalRef.clearTimeout?.(scheduled.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -585,34 +705,41 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
embed.dataset.autoplay = 'false';
|
embed.dataset.autoplay = 'false';
|
||||||
session.embed = embed;
|
session.embed = embed;
|
||||||
host.append(embed);
|
host.append(embed);
|
||||||
void facebookLoader(documentRef, globalRef).then((api) => {
|
void facebookLoader(documentRef, globalRef)
|
||||||
if (!session.active) return;
|
.then((api) => {
|
||||||
session.api = api;
|
if (!session.active) return;
|
||||||
session.readyListener = (message) => {
|
session.api = api;
|
||||||
if (!session.active || message?.type !== 'video'
|
session.readyListener = (message) => {
|
||||||
|| (message.id && message.id !== embed.id)) return;
|
if (
|
||||||
globalRef.clearTimeout?.(session.timer);
|
!session.active ||
|
||||||
session.timer = null;
|
message?.type !== 'video' ||
|
||||||
session.player = message.instance;
|
(message.id && message.id !== embed.id)
|
||||||
session.ready = true;
|
)
|
||||||
session.error = null;
|
return;
|
||||||
session.onReady?.();
|
globalRef.clearTimeout?.(session.timer);
|
||||||
setFacebookSessionPlayback(session, session.shouldPlay);
|
session.timer = null;
|
||||||
};
|
session.player = message.instance;
|
||||||
api.Event.subscribe('xfbml.ready', session.readyListener);
|
session.ready = true;
|
||||||
api.XFBML.parse(session.host);
|
session.error = null;
|
||||||
session.timer = globalRef.setTimeout?.(() => {
|
session.onReady?.();
|
||||||
if (!session.active || session.ready) return;
|
setFacebookSessionPlayback(session, session.shouldPlay);
|
||||||
session.error = new Error(
|
};
|
||||||
'Facebook embed unavailable. The post may be private, removed, region-restricted, or blocked.',
|
api.Event.subscribe('xfbml.ready', session.readyListener);
|
||||||
);
|
api.XFBML.parse(session.host);
|
||||||
session.onError?.(session.error);
|
session.timer =
|
||||||
}, EMBED_TIMEOUT_MS) ?? null;
|
globalRef.setTimeout?.(() => {
|
||||||
}).catch((error) => {
|
if (!session.active || session.ready) return;
|
||||||
if (!session.active) return;
|
session.error = new Error(
|
||||||
session.error = error;
|
'Facebook embed unavailable. The post may be private, removed, region-restricted, or blocked.',
|
||||||
session.onError?.(error);
|
);
|
||||||
});
|
session.onError?.(session.error);
|
||||||
|
}, EMBED_TIMEOUT_MS) ?? null;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (!session.active) return;
|
||||||
|
session.error = error;
|
||||||
|
session.onError?.(error);
|
||||||
|
});
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,9 +771,11 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const removePostRender = viewer.scene?.postRender?.addEventListener?.(position);
|
const removePostRender =
|
||||||
|
viewer.scene?.postRender?.addEventListener?.(position);
|
||||||
|
|
||||||
function hide({ immediate = false } = {}) {
|
function hide({ immediate = false, preserveWarm = false } = {}) {
|
||||||
|
if (!preserveWarm) clearWarm();
|
||||||
generation += 1;
|
generation += 1;
|
||||||
const current = record;
|
const current = record;
|
||||||
record = null;
|
record = null;
|
||||||
@@ -662,15 +791,17 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
current.card.classList.remove('is-entering');
|
current.card.classList.remove('is-entering');
|
||||||
current.card.classList.add('is-leaving');
|
current.card.classList.add('is-leaving');
|
||||||
leavingRecords.add(current);
|
leavingRecords.add(current);
|
||||||
current.exitTimer = globalRef.setTimeout?.(
|
current.exitTimer =
|
||||||
() => removeRecord(current),
|
globalRef.setTimeout?.(
|
||||||
CARD_EXIT_DURATION_MS,
|
() => removeRecord(current),
|
||||||
) ?? null;
|
CARD_EXIT_DURATION_MS,
|
||||||
|
) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearWarm(key = null) {
|
function clearWarm(key = null) {
|
||||||
if (!warmRecord || (key && warmRecord.key !== key)) return;
|
if (!warmRecord || (key && warmRecord.key !== key)) return;
|
||||||
if (warmRecord.facebookSession) destroyFacebookSession(warmRecord.facebookSession);
|
if (warmRecord.facebookSession)
|
||||||
|
destroyFacebookSession(warmRecord.facebookSession);
|
||||||
else warmRecord.iframe?.remove?.();
|
else warmRecord.iframe?.remove?.();
|
||||||
warmRecord = null;
|
warmRecord = null;
|
||||||
}
|
}
|
||||||
@@ -694,9 +825,10 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
clearWarm();
|
clearWarm();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const startAtSec = source.provider === 'youtube'
|
const startAtSec =
|
||||||
? Math.max(0, Number(media.embedStartAtSec ?? media.clipInSec) || 0)
|
source.provider === 'youtube'
|
||||||
: 0;
|
? Math.max(0, Number(media.embedStartAtSec ?? media.clipInSec) || 0)
|
||||||
|
: 0;
|
||||||
ensureProviderConnections(documentRef, source.provider);
|
ensureProviderConnections(documentRef, source.provider);
|
||||||
const endAtSec = media.embedEndAtSec;
|
const endAtSec = media.embedEndAtSec;
|
||||||
const key = `${source.provider}:${source.id || source.url}:${startAtSec}:${endAtSec ?? ''}`;
|
const key = `${source.provider}:${source.id || source.url}:${startAtSec}:${endAtSec ?? ''}`;
|
||||||
@@ -713,7 +845,11 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
}
|
}
|
||||||
const iframe = documentRef.createElement('iframe');
|
const iframe = documentRef.createElement('iframe');
|
||||||
iframe.className = 'bhote-embedded-media-warmup';
|
iframe.className = 'bhote-embedded-media-warmup';
|
||||||
iframe.src = embeddedMediaFrameUrl(source, { autoplay: false, startAtSec, endAtSec });
|
iframe.src = embeddedMediaFrameUrl(source, {
|
||||||
|
autoplay: false,
|
||||||
|
startAtSec,
|
||||||
|
endAtSec,
|
||||||
|
});
|
||||||
iframe.title = `Preloading ${providerLabel(source.provider)} media`;
|
iframe.title = `Preloading ${providerLabel(source.provider)} media`;
|
||||||
iframe.tabIndex = -1;
|
iframe.tabIndex = -1;
|
||||||
iframe.setAttribute('aria-hidden', 'true');
|
iframe.setAttribute('aria-hidden', 'true');
|
||||||
@@ -729,15 +865,20 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
try {
|
try {
|
||||||
const api = await xLoader(documentRef, globalRef);
|
const api = await xLoader(documentRef, globalRef);
|
||||||
if (token !== generation || record !== current) return;
|
if (token !== generation || record !== current) return;
|
||||||
const widget = await api.widgets.createTweet(current.source.id, current.player, {
|
const widget = await api.widgets.createTweet(
|
||||||
align: 'center',
|
current.source.id,
|
||||||
conversation: 'none',
|
current.player,
|
||||||
dnt: true,
|
{
|
||||||
theme: 'dark',
|
align: 'center',
|
||||||
width: 390,
|
conversation: 'none',
|
||||||
});
|
dnt: true,
|
||||||
|
theme: 'dark',
|
||||||
|
width: 390,
|
||||||
|
},
|
||||||
|
);
|
||||||
if (token !== generation || record !== current) return;
|
if (token !== generation || record !== current) return;
|
||||||
if (!widget) throw new Error('The X post is unavailable or cannot be embedded.');
|
if (!widget)
|
||||||
|
throw new Error('The X post is unavailable or cannot be embedded.');
|
||||||
current.status.textContent = 'X POST · PROVIDER CONTROLS';
|
current.status.textContent = 'X POST · PROVIDER CONTROLS';
|
||||||
position();
|
position();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -749,8 +890,9 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindFacebook(current, token, warmKey, autoplay) {
|
function bindFacebook(current, token, warmKey, autoplay) {
|
||||||
const session = takeWarmFacebook(warmKey, current.player)
|
const session =
|
||||||
|| createFacebookSession(current.source, current.player);
|
takeWarmFacebook(warmKey, current.player) ||
|
||||||
|
createFacebookSession(current.source, current.player);
|
||||||
current.facebookSession = session;
|
current.facebookSession = session;
|
||||||
session.shouldPlay = autoplay === true;
|
session.shouldPlay = autoplay === true;
|
||||||
session.onReady = () => {
|
session.onReady = () => {
|
||||||
@@ -775,6 +917,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setPlayback(playing) {
|
function setPlayback(playing) {
|
||||||
|
if (!playing) clearWarm();
|
||||||
// Removing an uncontrolled provider frame also cancels delayed autoplay.
|
// Removing an uncontrolled provider frame also cancels delayed autoplay.
|
||||||
// Provider commands alone cannot stop a frame that has not loaded yet.
|
// Provider commands alone cannot stop a frame that has not loaded yet.
|
||||||
if (!playing && record && !record.facebookSession) {
|
if (!playing && record && !record.facebookSession) {
|
||||||
@@ -792,37 +935,49 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
hide();
|
hide();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const startAtSec = source.provider === 'youtube'
|
const startAtSec =
|
||||||
? Math.max(0, Number(media.embedStartAtSec ?? media.clipInSec) || 0)
|
source.provider === 'youtube'
|
||||||
: 0;
|
? Math.max(0, Number(media.embedStartAtSec ?? media.clipInSec) || 0)
|
||||||
|
: 0;
|
||||||
ensureProviderConnections(documentRef, source.provider);
|
ensureProviderConnections(documentRef, source.provider);
|
||||||
const endAtSec = media.embedEndAtSec;
|
const endAtSec = media.embedEndAtSec;
|
||||||
const warmKey = `${source.provider}:${source.id || source.url}:${startAtSec}:${endAtSec ?? ''}`;
|
const warmKey = `${source.provider}:${source.id || source.url}:${startAtSec}:${endAtSec ?? ''}`;
|
||||||
const key = source.provider === 'facebook'
|
const key =
|
||||||
? warmKey
|
source.provider === 'facebook'
|
||||||
: `${warmKey}:${autoplay ? 1 : 0}`;
|
? warmKey
|
||||||
|
: `${warmKey}:${autoplay ? 1 : 0}`;
|
||||||
if (record?.key === key) {
|
if (record?.key === key) {
|
||||||
record.anchor = anchor;
|
record.anchor = anchor;
|
||||||
if (source.provider === 'facebook') setPlayback(autoplay);
|
if (source.provider === 'facebook') setPlayback(autoplay);
|
||||||
position();
|
position();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
hide();
|
if (warmRecord && warmRecord.key !== warmKey) clearWarm();
|
||||||
|
hide({ preserveWarm: true });
|
||||||
const token = ++generation;
|
const token = ++generation;
|
||||||
const card = documentRef.createElement('section');
|
const card = documentRef.createElement('section');
|
||||||
const portraitClass = isPortraitMedia(media) ? ' is-portrait' : '';
|
const portraitClass = isPortraitMedia(media) ? ' is-portrait' : '';
|
||||||
card.className = `bhote-embedded-callout is-${source.provider}${portraitClass}`;
|
card.className = `bhote-embedded-callout is-${source.provider}${portraitClass}`;
|
||||||
card.dataset.provider = source.provider;
|
card.dataset.provider = source.provider;
|
||||||
card.setAttribute('aria-label', `Embedded ${providerLabel(source.provider)} media for ${observation?.title || 'witness source'}`);
|
card.setAttribute(
|
||||||
const leader = documentRef.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
'aria-label',
|
||||||
|
`Embedded ${providerLabel(source.provider)} media for ${observation?.title || 'witness source'}`,
|
||||||
|
);
|
||||||
|
const leader = documentRef.createElementNS(
|
||||||
|
'http://www.w3.org/2000/svg',
|
||||||
|
'svg',
|
||||||
|
);
|
||||||
leader.classList.add('bhote-embedded-callout-leader');
|
leader.classList.add('bhote-embedded-callout-leader');
|
||||||
leader.setAttribute('aria-hidden', 'true');
|
leader.setAttribute('aria-hidden', 'true');
|
||||||
leader.innerHTML = '<path pathLength="1" fill="none" stroke="rgba(147,213,228,.72)" stroke-width="1.5"/><circle r="4" fill="#6be8ff" stroke="#ffffff" stroke-width="1.5"/>';
|
leader.innerHTML =
|
||||||
|
'<path pathLength="1" fill="none" stroke="rgba(147,213,228,.72)" stroke-width="1.5"/><circle r="4" fill="#6be8ff" stroke="#ffffff" stroke-width="1.5"/>';
|
||||||
const player = documentRef.createElement('div');
|
const player = documentRef.createElement('div');
|
||||||
player.className = 'bhote-embedded-callout-player';
|
player.className = 'bhote-embedded-callout-player';
|
||||||
const footer = documentRef.createElement('footer');
|
const footer = documentRef.createElement('footer');
|
||||||
const title = documentRef.createElement('strong');
|
const title = documentRef.createElement('strong');
|
||||||
title.textContent = String(observation?.shortTitle || observation?.title || 'Witness source').toUpperCase();
|
title.textContent = String(
|
||||||
|
observation?.shortTitle || observation?.title || 'Witness source',
|
||||||
|
).toUpperCase();
|
||||||
const status = documentRef.createElement('small');
|
const status = documentRef.createElement('small');
|
||||||
status.setAttribute('role', 'status');
|
status.setAttribute('role', 'status');
|
||||||
status.textContent = `${providerLabel(source.provider)} · EMBEDDED SOURCE`;
|
status.textContent = `${providerLabel(source.provider)} · EMBEDDED SOURCE`;
|
||||||
@@ -854,36 +1009,58 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
bindFacebook(record, token, warmKey, autoplay);
|
bindFacebook(record, token, warmKey, autoplay);
|
||||||
} else {
|
} else {
|
||||||
const iframe = documentRef.createElement('iframe');
|
const iframe = documentRef.createElement('iframe');
|
||||||
iframe.src = embeddedMediaFrameUrl(source, { autoplay, startAtSec, endAtSec });
|
iframe.src = embeddedMediaFrameUrl(source, {
|
||||||
const controlled = autoplay && Number.isFinite(Number(endAtSec)) && Number(endAtSec) > startAtSec;
|
autoplay,
|
||||||
|
startAtSec,
|
||||||
|
endAtSec,
|
||||||
|
});
|
||||||
|
const controlled =
|
||||||
|
autoplay &&
|
||||||
|
Number.isFinite(Number(endAtSec)) &&
|
||||||
|
Number(endAtSec) > startAtSec;
|
||||||
if (controlled) {
|
if (controlled) {
|
||||||
const url = new URL(iframe.src);
|
const url = new URL(iframe.src);
|
||||||
url.searchParams.set('enablejsapi', '1');
|
url.searchParams.set('enablejsapi', '1');
|
||||||
if (globalRef.location?.origin) url.searchParams.set('origin', globalRef.location.origin);
|
if (globalRef.location?.origin)
|
||||||
|
url.searchParams.set('origin', globalRef.location.origin);
|
||||||
iframe.src = url.href;
|
iframe.src = url.href;
|
||||||
}
|
}
|
||||||
iframe.title = `${providerLabel(source.provider)} media: ${observation?.title || 'witness source'}`;
|
iframe.title = `${providerLabel(source.provider)} media: ${observation?.title || 'witness source'}`;
|
||||||
iframe.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen';
|
iframe.allow =
|
||||||
|
'autoplay; encrypted-media; picture-in-picture; fullscreen';
|
||||||
iframe.allowFullscreen = true;
|
iframe.allowFullscreen = true;
|
||||||
iframe.loading = 'eager';
|
iframe.loading = 'eager';
|
||||||
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
|
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
|
||||||
iframe.addEventListener?.('load', () => {
|
iframe.addEventListener?.(
|
||||||
if (token !== generation || record?.key !== key) return;
|
'load',
|
||||||
clearWarm(warmKey);
|
() => {
|
||||||
if (!record.youtubeSession || ['starting', 'playing'].includes(record.youtubeSession.getState().phase)) {
|
if (token !== generation || record?.key !== key) return;
|
||||||
status.textContent = `${providerLabel(source.provider)} · PROVIDER CONTROLS`;
|
clearWarm(warmKey);
|
||||||
}
|
if (
|
||||||
position();
|
!record.youtubeSession ||
|
||||||
}, { once: true });
|
['starting', 'playing'].includes(
|
||||||
|
record.youtubeSession.getState().phase,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
status.textContent = `${providerLabel(source.provider)} · PROVIDER CONTROLS`;
|
||||||
|
}
|
||||||
|
position();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
player.append(iframe);
|
player.append(iframe);
|
||||||
if (controlled) {
|
if (controlled) {
|
||||||
const current = record;
|
const current = record;
|
||||||
current.youtubeSession = createTrimmedYouTubePlayback({
|
current.youtubeSession = createTrimmedYouTubePlayback({
|
||||||
iframe, endAtSec: Number(endAtSec), startAtSec, globalRef,
|
iframe,
|
||||||
|
endAtSec: Number(endAtSec),
|
||||||
|
startAtSec,
|
||||||
|
globalRef,
|
||||||
loadApi: () => youtubeLoader(documentRef, globalRef),
|
loadApi: () => youtubeLoader(documentRef, globalRef),
|
||||||
isCurrent: () => token === generation && record === current,
|
isCurrent: () => token === generation && record === current,
|
||||||
onStatus: (phase) => {
|
onStatus: (phase) => {
|
||||||
if (phase !== 'completed') status.textContent = `YOUTUBE ${phase.toUpperCase()} · OPEN ORIGINAL`;
|
if (phase !== 'completed')
|
||||||
|
status.textContent = `YOUTUBE ${phase.toUpperCase()} · OPEN ORIGINAL`;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,3 +248,98 @@ test('Mailung playback stops at source timestamp 0:07 without changing other cli
|
|||||||
assert.equal(url.searchParams.has('end'), false);
|
assert.equal(url.searchParams.has('end'), false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function preloadFixture(options = {}) {
|
||||||
|
class Element extends EventTarget {
|
||||||
|
constructor(tag) {
|
||||||
|
super(); this.tag = tag; this.children = []; this.dataset = {};
|
||||||
|
this.classList = { add() {}, remove() {}, contains() { return false; } };
|
||||||
|
}
|
||||||
|
setAttribute() {}
|
||||||
|
removeAttribute() {}
|
||||||
|
append(...children) {
|
||||||
|
for (const child of children) { child.remove(); child.parent = this; this.children.push(child); }
|
||||||
|
}
|
||||||
|
remove() {
|
||||||
|
if (this.parent) this.parent.children = this.parent.children.filter(child => child !== this);
|
||||||
|
this.parent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const documentRef = {
|
||||||
|
body: new Element('body'),
|
||||||
|
createElement: tag => new Element(tag),
|
||||||
|
createElementNS: (_, tag) => new Element(tag),
|
||||||
|
};
|
||||||
|
const media = createBhoteKoshiEmbeddedMedia({ viewer: {}, documentRef, globalRef: {}, ...options });
|
||||||
|
return { media, root: documentRef.body.children[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const operation of ['pause', 'hide', 'destroy']) {
|
||||||
|
test(`${operation} removes a hidden YouTube preload`, () => {
|
||||||
|
const { media, root } = preloadFixture();
|
||||||
|
media.warm({ sourceUrl: 'https://youtu.be/DbqRexFxv3k' });
|
||||||
|
assert.equal(root.children.length, 1);
|
||||||
|
media[operation]();
|
||||||
|
assert.equal(root.children.length, 0);
|
||||||
|
media.destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('camera updates retain a preload until the visible player loads', () => {
|
||||||
|
const { media, root } = preloadFixture();
|
||||||
|
const observation = { media: { sourceUrl: 'https://youtu.be/DbqRexFxv3k' } };
|
||||||
|
media.warm({ observation });
|
||||||
|
const frame = root.children[0];
|
||||||
|
media.hide({ preserveWarm: true });
|
||||||
|
media.warm({ observation });
|
||||||
|
assert.equal(root.children[0], frame);
|
||||||
|
assert.equal(media.show({ observation, anchor: {} }), true);
|
||||||
|
assert.equal(frame.parent, root, 'keep the preload while the visible player starts');
|
||||||
|
const card = root.children.find(node => node.tag === 'section');
|
||||||
|
const body = card.children.find(node => node.className === 'bhote-embedded-callout-body');
|
||||||
|
const player = body.children.find(node => node.className === 'bhote-embedded-callout-player');
|
||||||
|
player.children.find(node => node.tag === 'iframe').dispatchEvent(new Event('load'));
|
||||||
|
assert.equal(frame.parent, null, 'visible player readiness releases the preload');
|
||||||
|
media.destroy();
|
||||||
|
assert.equal(root.children.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hiding revokes a pending Facebook SDK before it can mount or subscribe', async () => {
|
||||||
|
let resolveApi;
|
||||||
|
const calls = [];
|
||||||
|
const { media, root } = preloadFixture({
|
||||||
|
facebookLoader: () => new Promise(resolve => { resolveApi = resolve; }),
|
||||||
|
});
|
||||||
|
media.warm({ sourceUrl: 'https://www.facebook.com/reel/1571491657757829' });
|
||||||
|
assert.equal(root.children.length, 1);
|
||||||
|
media.hide();
|
||||||
|
assert.equal(root.children.length, 0);
|
||||||
|
resolveApi({ Event: { subscribe() { calls.push('subscribe'); } }, XFBML: { parse() { calls.push('parse'); } } });
|
||||||
|
await Promise.resolve(); await Promise.resolve();
|
||||||
|
assert.deepEqual(calls, []);
|
||||||
|
media.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pausing cancels a mounted Facebook preload and rejects its late ready callback', async () => {
|
||||||
|
let ready;
|
||||||
|
let unsubscribed = 0;
|
||||||
|
const timers = new Set();
|
||||||
|
const playerCalls = [];
|
||||||
|
const { media, root } = preloadFixture({
|
||||||
|
globalRef: { setTimeout(fn) { timers.add(fn); return fn; }, clearTimeout(fn) { timers.delete(fn); } },
|
||||||
|
facebookLoader: async () => ({
|
||||||
|
Event: { subscribe(_name, callback) { ready = callback; }, unsubscribe() { unsubscribed++; } },
|
||||||
|
XFBML: { parse() {} },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
media.warm({ sourceUrl: 'https://www.facebook.com/reel/1571491657757829' });
|
||||||
|
await Promise.resolve(); await Promise.resolve();
|
||||||
|
assert.equal(timers.size, 1);
|
||||||
|
media.pause();
|
||||||
|
assert.equal(root.children.length, 0);
|
||||||
|
assert.equal(timers.size, 0);
|
||||||
|
assert.equal(unsubscribed, 1);
|
||||||
|
ready({ type: 'video', instance: { play() { playerCalls.push('play'); }, pause() { playerCalls.push('pause'); } } });
|
||||||
|
assert.deepEqual(playerCalls, []);
|
||||||
|
media.destroy();
|
||||||
|
});
|
||||||
+1221
-537
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
import { claimPointer, releasePointer } from './inputOwnership.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { SOURCE_PATH_BEAT_IDS } from './bhoteKoshiShotPaths.js';
|
import { SOURCE_PATH_BEAT_IDS } from './bhoteKoshiShotPaths.js';
|
||||||
import {
|
import {
|
||||||
@@ -1777,6 +1778,13 @@ test('Debris-Dammed Lake card opens its source and is armed for approved local v
|
|||||||
beatReveal: 0.36,
|
beatReveal: 0.36,
|
||||||
}, { origin: 'scene' });
|
}, { origin: 'scene' });
|
||||||
await layer.enable(viewer, { origin: 'scene' });
|
await layer.enable(viewer, { origin: 'scene' });
|
||||||
|
const lease = claimPointer('draw');
|
||||||
|
try {
|
||||||
|
viewer._test.canvas.dispatch('click', { offsetX: 640, offsetY: 420 });
|
||||||
|
assert.equal(opened.length, 0, 'drawing owns the pointer over evidence cards');
|
||||||
|
} finally {
|
||||||
|
releasePointer(lease);
|
||||||
|
}
|
||||||
viewer._test.canvas.dispatch('click', { offsetX: 640, offsetY: 420 });
|
viewer._test.canvas.dispatch('click', { offsetX: 640, offsetY: 420 });
|
||||||
assert.deepEqual(opened, [[
|
assert.deepEqual(opened, [[
|
||||||
event.evidenceSpine[1].media.sourceUrl,
|
event.evidenceSpine[1].media.sourceUrl,
|
||||||
@@ -2185,7 +2193,7 @@ test('Bhote Koshi frame path keeps evidence lookup scalar and wrapper-free', asy
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
source,
|
source,
|
||||||
/const beatIndex = currentEvidenceIndex\(\);\s*const sceneBeatIndex = sceneDirected\s*\? _evidenceTimeline\.findIndex\(\(\{ observation: item \}\) => item\.id === _sceneBeatId\)\s*:\s*beatIndex;\s*const observation = _evidenceTimeline\[sceneBeatIndex\]\?\.observation \|\| null;/,
|
/const beatIndex = currentEvidenceIndex\(\);\s*const sceneBeatIndex = sceneDirected\s*\? _evidenceTimeline\.findIndex\(\s*\(\{ observation: item \}\) => item\.id === _sceneBeatId,?\s*\)\s*:\s*beatIndex;\s*const observation =\s*_evidenceTimeline\[sceneBeatIndex\]\?\.observation \|\| null;/,
|
||||||
'syncPanel must resolve authored scene beats to one scalar index before reading the observation',
|
'syncPanel must resolve authored scene beats to one scalar index before reading the observation',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,117 @@
|
|||||||
// Overview pins follow the event pack through Devighat.
|
// Overview pins follow the event pack through Devighat.
|
||||||
// Charaudi remains a separate evidence shot, and none of these observation
|
// Charaudi remains a separate evidence shot, and none of these observation
|
||||||
// locations extends the mapped flood path.
|
// locations extends the mapped flood path.
|
||||||
export const INCIDENT_OVERVIEW_PLACES = Object.freeze([
|
export const INCIDENT_OVERVIEW_PLACES = Object.freeze(
|
||||||
['immediate-collapse-viewpoint', 'COLLAPSE VIEWPOINT', 28.3632333, 85.4423639, 2850, 'left'],
|
[
|
||||||
['debris-dammed-lake', 'DAMMED LAKE', 28.3346111, 85.4861389, 2750, 'right'],
|
[
|
||||||
['second-landslide', 'SECOND LANDSLIDE', 28.3379706, 85.4688823, 2750, 'left'],
|
'immediate-collapse-viewpoint',
|
||||||
['gyirong-border-gate', 'BORDER GATE', 28.2808338, 85.3780248, 1818, 'right'],
|
'COLLAPSE VIEWPOINT',
|
||||||
['timure-cluster', 'TIMURE', 28.2519361, 85.3665833, 1689, 'left'],
|
28.3632333,
|
||||||
['syabru-besi', 'SYABRU BESI', 28.1602583333333, 85.3325666666667, 1417, 'right'],
|
85.4423639,
|
||||||
['dhunche', 'DHUNCHE', 28.1131861111111, 85.2953305555556, 1300, 'left'],
|
2850,
|
||||||
['mailung-upper-trishuli', 'UPPER TRISHULI', 28.090925, 85.2322916666667, 950, 'right'],
|
'left',
|
||||||
['mailung-bazzar', 'MAILUNG', 28.0761666666667, 85.2125277777778, 800, 'left'],
|
],
|
||||||
['dandagaun', 'DANDAGAUN', 28.0751944444444, 85.2076944444444, 780, 'right-lower'],
|
[
|
||||||
['dandagaun-viewpoint', 'DANDAGAUN VIEWPOINT', 28.0354, 85.1894361111111, 700, 'right'],
|
'debris-dammed-lake',
|
||||||
['betrawati-bazaar', 'BETRAWATI', 27.9758992572246, 85.1835331689033, 600, 'left'],
|
'DAMMED LAKE',
|
||||||
['bhainse', 'BHAINSE', 27.9623111111111, 85.1739777777778, 500, 'right-lower'],
|
28.3346111,
|
||||||
['bidur-trishuli-bridge', 'BIDUR BRIDGE', 27.920858, 85.148029, 510, 'left'],
|
85.4861389,
|
||||||
['devighat-taadi-khola-bridge', 'DEVIGHAT BRIDGE', 27.8585278, 85.1090318, 350, 'right'],
|
2750,
|
||||||
].map(([id, title, lat, lon, heightM, placement]) => Object.freeze({ id, title, lat, lon, heightM, placement })));
|
'right',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'second-landslide',
|
||||||
|
'SECOND LANDSLIDE',
|
||||||
|
28.3379706,
|
||||||
|
85.4688823,
|
||||||
|
2750,
|
||||||
|
'left',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'gyirong-border-gate',
|
||||||
|
'BORDER GATE',
|
||||||
|
28.2808338,
|
||||||
|
85.3780248,
|
||||||
|
1818,
|
||||||
|
'right',
|
||||||
|
],
|
||||||
|
['timure-cluster', 'TIMURE', 28.2519361, 85.3665833, 1689, 'left'],
|
||||||
|
[
|
||||||
|
'syabru-besi',
|
||||||
|
'SYABRU BESI',
|
||||||
|
28.1602583333333,
|
||||||
|
85.3325666666667,
|
||||||
|
1417,
|
||||||
|
'right',
|
||||||
|
],
|
||||||
|
['dhunche', 'DHUNCHE', 28.1131861111111, 85.2953305555556, 1300, 'left'],
|
||||||
|
[
|
||||||
|
'mailung-upper-trishuli',
|
||||||
|
'UPPER TRISHULI',
|
||||||
|
28.090925,
|
||||||
|
85.2322916666667,
|
||||||
|
950,
|
||||||
|
'right',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'mailung-bazzar',
|
||||||
|
'MAILUNG',
|
||||||
|
28.0761666666667,
|
||||||
|
85.2125277777778,
|
||||||
|
800,
|
||||||
|
'left',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dandagaun',
|
||||||
|
'DANDAGAUN',
|
||||||
|
28.0751944444444,
|
||||||
|
85.2076944444444,
|
||||||
|
780,
|
||||||
|
'right-lower',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dandagaun-viewpoint',
|
||||||
|
'DANDAGAUN VIEWPOINT',
|
||||||
|
28.0354,
|
||||||
|
85.1894361111111,
|
||||||
|
700,
|
||||||
|
'right',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'betrawati-bazaar',
|
||||||
|
'BETRAWATI',
|
||||||
|
27.9758992572246,
|
||||||
|
85.1835331689033,
|
||||||
|
600,
|
||||||
|
'left',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bhainse',
|
||||||
|
'BHAINSE',
|
||||||
|
27.9623111111111,
|
||||||
|
85.1739777777778,
|
||||||
|
500,
|
||||||
|
'right-lower',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bidur-trishuli-bridge',
|
||||||
|
'BIDUR BRIDGE',
|
||||||
|
27.920858,
|
||||||
|
85.148029,
|
||||||
|
510,
|
||||||
|
'left',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'devighat-taadi-khola-bridge',
|
||||||
|
'DEVIGHAT BRIDGE',
|
||||||
|
27.8585278,
|
||||||
|
85.1090318,
|
||||||
|
350,
|
||||||
|
'right',
|
||||||
|
],
|
||||||
|
].map(([id, title, lat, lon, heightM, placement]) =>
|
||||||
|
Object.freeze({ id, title, lat, lon, heightM, placement }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const INCIDENT_OVERVIEW_HOLD_SEC = 11;
|
export const INCIDENT_OVERVIEW_HOLD_SEC = 11;
|
||||||
+310
-140
@@ -12,14 +12,18 @@ import { BHOTE_KOSHI_FLOOD_PATH } from './bhoteKoshiFloodPath.js';
|
|||||||
import { INCIDENT_OVERVIEW_PLACES } from './bhoteKoshiIncidentPlaces.js';
|
import { INCIDENT_OVERVIEW_PLACES } from './bhoteKoshiIncidentPlaces.js';
|
||||||
|
|
||||||
export const BHOTE_KOSHI_LOCATOR_LAYER_ID = 'bhote-koshi-locator';
|
export const BHOTE_KOSHI_LOCATOR_LAYER_ID = 'bhote-koshi-locator';
|
||||||
export const BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID = 'bhote-koshi-locator-callout';
|
export const BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID =
|
||||||
|
'bhote-koshi-locator-callout';
|
||||||
export const NEPAL_CONTEXT_PRESENTATION = 'nepal-context';
|
export const NEPAL_CONTEXT_PRESENTATION = 'nepal-context';
|
||||||
export const BHOTE_KOSHI_REGIONAL_PRESENTATION = 'bhote-koshi-regional';
|
export const BHOTE_KOSHI_REGIONAL_PRESENTATION = 'bhote-koshi-regional';
|
||||||
export const BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION = 'bhote-koshi-city-context';
|
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_INCIDENT_PLACES_PRESENTATION =
|
||||||
|
'bhote-koshi-incident-places';
|
||||||
export const BHOTE_KOSHI_FLOOD_PATH_PRESENTATION = 'bhote-koshi-flood-path';
|
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_PATH_OVERVIEW_PRESENTATION =
|
||||||
export const BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION = 'bhote-koshi-trigger-record';
|
'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 LEGACY_PATH_BORDER_PRESENTATION = 'bhote-koshi-path-border';
|
||||||
|
|
||||||
@@ -42,7 +46,8 @@ const FLOOD_PATH_DRAW_DURATION_MS = 4000;
|
|||||||
const TRIGGER_APPROACH_ARM_MS = 500;
|
const TRIGGER_APPROACH_ARM_MS = 500;
|
||||||
const TRIGGER_APPROACH_DURATION_SEC = 4.6;
|
const TRIGGER_APPROACH_DURATION_SEC = 4.6;
|
||||||
const TRIGGER_ORBIT_DURATION_SEC = 2.8;
|
const TRIGGER_ORBIT_DURATION_SEC = 2.8;
|
||||||
const TRIGGER_SOURCE_URL = 'https://earthquake.usgs.gov/earthquakes/eventpage/us7000tbwb/executive';
|
const TRIGGER_SOURCE_URL =
|
||||||
|
'https://earthquake.usgs.gov/earthquakes/eventpage/us7000tbwb/executive';
|
||||||
|
|
||||||
const INCIDENT = Object.freeze({
|
const INCIDENT = Object.freeze({
|
||||||
lat: 28.3632333,
|
lat: 28.3632333,
|
||||||
@@ -86,9 +91,30 @@ const NEPAL = Object.freeze({
|
|||||||
title: 'NEPAL',
|
title: 'NEPAL',
|
||||||
});
|
});
|
||||||
const NEARBY_CITIES = Object.freeze([
|
const NEARBY_CITIES = Object.freeze([
|
||||||
Object.freeze({ id: 'kathmandu', title: 'KATHMANDU', lat: 27.7017, lon: 85.3206, heightM: 3600, placement: 'left' }),
|
Object.freeze({
|
||||||
Object.freeze({ id: 'bhaktapur', title: 'BHAKTAPUR', lat: 27.673, lon: 85.43, heightM: 3400, placement: 'right' }),
|
id: 'kathmandu',
|
||||||
Object.freeze({ id: 'dhulikhel', title: 'DHULIKHEL', lat: 27.6221, lon: 85.5428, heightM: 3600, placement: 'right-lower' }),
|
title: 'KATHMANDU',
|
||||||
|
lat: 27.7017,
|
||||||
|
lon: 85.3206,
|
||||||
|
heightM: 3600,
|
||||||
|
placement: 'left',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: 'bhaktapur',
|
||||||
|
title: 'BHAKTAPUR',
|
||||||
|
lat: 27.673,
|
||||||
|
lon: 85.43,
|
||||||
|
heightM: 3400,
|
||||||
|
placement: 'right',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: 'dhulikhel',
|
||||||
|
title: 'DHULIKHEL',
|
||||||
|
lat: 27.6221,
|
||||||
|
lon: 85.5428,
|
||||||
|
heightM: 3600,
|
||||||
|
placement: 'right-lower',
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
const BORDER_COLOR = Cesium.Color.fromCssColorString('#20e7f2');
|
const BORDER_COLOR = Cesium.Color.fromCssColorString('#20e7f2');
|
||||||
const BORDER_ACCENT = '#20e7f2';
|
const BORDER_ACCENT = '#20e7f2';
|
||||||
@@ -104,7 +130,10 @@ const DEFAULT_OVERLAY_HOST = Object.freeze({
|
|||||||
const DEFAULT_BOUNDARY_RESOLVER = async () => null;
|
const DEFAULT_BOUNDARY_RESOLVER = async () => null;
|
||||||
|
|
||||||
function abortError() {
|
function abortError() {
|
||||||
return new DOMException('Bhote Koshi locator activation cancelled', 'AbortError');
|
return new DOMException(
|
||||||
|
'Bhote Koshi locator activation cancelled',
|
||||||
|
'AbortError',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePresentation(value) {
|
function normalizePresentation(value) {
|
||||||
@@ -118,8 +147,10 @@ function normalizePresentation(value) {
|
|||||||
if (value === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION) {
|
if (value === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION) {
|
||||||
return BHOTE_KOSHI_FLOOD_PATH_PRESENTATION;
|
return BHOTE_KOSHI_FLOOD_PATH_PRESENTATION;
|
||||||
}
|
}
|
||||||
if (value === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION
|
if (
|
||||||
|| value === LEGACY_PATH_BORDER_PRESENTATION) {
|
value === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION ||
|
||||||
|
value === LEGACY_PATH_BORDER_PRESENTATION
|
||||||
|
) {
|
||||||
return BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION;
|
return BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION;
|
||||||
}
|
}
|
||||||
if (value === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) {
|
if (value === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) {
|
||||||
@@ -129,8 +160,10 @@ function normalizePresentation(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function presentationUsesBoundary(value) {
|
function presentationUsesBoundary(value) {
|
||||||
return value === BHOTE_KOSHI_REGIONAL_PRESENTATION
|
return (
|
||||||
|| value === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION;
|
value === BHOTE_KOSHI_REGIONAL_PRESENTATION ||
|
||||||
|
value === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function presentationAnimatesBoundary(value) {
|
function presentationAnimatesBoundary(value) {
|
||||||
@@ -145,7 +178,10 @@ function smoothstep(from, to, value) {
|
|||||||
|
|
||||||
/** Resolve the persistent text callout's dot, leader, and card entrance. */
|
/** Resolve the persistent text callout's dot, leader, and card entrance. */
|
||||||
export function anchoredCalloutPresentation(elapsedMs) {
|
export function anchoredCalloutPresentation(elapsedMs) {
|
||||||
const progress = Math.max(0, Math.min(1, Number(elapsedMs) / CALLOUT_REVEAL_DURATION_MS || 0));
|
const progress = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(1, Number(elapsedMs) / CALLOUT_REVEAL_DURATION_MS || 0),
|
||||||
|
);
|
||||||
const contentAlpha = smoothstep(0.34, 1, progress);
|
const contentAlpha = smoothstep(0.34, 1, progress);
|
||||||
return {
|
return {
|
||||||
alpha: smoothstep(0, 0.18, progress),
|
alpha: smoothstep(0, 0.18, progress),
|
||||||
@@ -165,12 +201,14 @@ export function revealPolylinePositions(positions, progress) {
|
|||||||
const revealed = positions.slice(0, completed + 1);
|
const revealed = positions.slice(0, completed + 1);
|
||||||
const next = Math.min(completed + 1, positions.length - 1);
|
const next = Math.min(completed + 1, positions.length - 1);
|
||||||
if (next > completed) {
|
if (next > completed) {
|
||||||
revealed.push(Cesium.Cartesian3.lerp(
|
revealed.push(
|
||||||
positions[completed],
|
Cesium.Cartesian3.lerp(
|
||||||
positions[next],
|
positions[completed],
|
||||||
scaled - completed,
|
positions[next],
|
||||||
new Cesium.Cartesian3(),
|
scaled - completed,
|
||||||
));
|
new Cesium.Cartesian3(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return revealed.length >= 2 ? revealed : [positions[0], positions[0]];
|
return revealed.length >= 2 ? revealed : [positions[0], positions[0]];
|
||||||
}
|
}
|
||||||
@@ -184,17 +222,19 @@ export function closeRegionRing(ring) {
|
|||||||
if (normalized.length < 3) return [];
|
if (normalized.length < 3) return [];
|
||||||
const [firstLon, firstLat] = normalized[0];
|
const [firstLon, firstLat] = normalized[0];
|
||||||
const [lastLon, lastLat] = normalized[normalized.length - 1];
|
const [lastLon, lastLat] = normalized[normalized.length - 1];
|
||||||
if (firstLon !== lastLon || firstLat !== lastLat) normalized.push([firstLon, firstLat]);
|
if (firstLon !== lastLon || firstLat !== lastLat)
|
||||||
|
normalized.push([firstLon, firstLat]);
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return the largest exterior ring from a Polygon or MultiPolygon GeoJSON. */
|
/** Return the largest exterior ring from a Polygon or MultiPolygon GeoJSON. */
|
||||||
export function largestGeoJsonRing(geojson) {
|
export function largestGeoJsonRing(geojson) {
|
||||||
const polygons = geojson?.type === 'Polygon'
|
const polygons =
|
||||||
? [geojson.coordinates]
|
geojson?.type === 'Polygon'
|
||||||
: geojson?.type === 'MultiPolygon'
|
? [geojson.coordinates]
|
||||||
? geojson.coordinates
|
: geojson?.type === 'MultiPolygon'
|
||||||
: [];
|
? geojson.coordinates
|
||||||
|
: [];
|
||||||
const rings = polygons
|
const rings = polygons
|
||||||
.map((polygon) => polygon?.[0])
|
.map((polygon) => polygon?.[0])
|
||||||
.filter((ring) => Array.isArray(ring) && ring.length >= 3);
|
.filter((ring) => Array.isArray(ring) && ring.length >= 3);
|
||||||
@@ -242,11 +282,15 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function publishOverlayEntries({ moving = false } = {}) {
|
function publishOverlayEntries({ moving = false } = {}) {
|
||||||
overlayHost.setEntries(BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID, _overlayEntries, {
|
overlayHost.setEntries(
|
||||||
cohortLimit: Math.max(1, _overlayEntries.length),
|
BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID,
|
||||||
collisionCapacity: Math.max(1, _overlayEntries.length),
|
_overlayEntries,
|
||||||
moving,
|
{
|
||||||
});
|
cohortLimit: Math.max(1, _overlayEntries.length),
|
||||||
|
collisionCapacity: Math.max(1, _overlayEntries.length),
|
||||||
|
moving,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTriggerApproach({ preserveCallout = false } = {}) {
|
function clearTriggerApproach({ preserveCallout = false } = {}) {
|
||||||
@@ -277,17 +321,27 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
function removeDataSource() {
|
function removeDataSource() {
|
||||||
stopAnimations();
|
stopAnimations();
|
||||||
overlayHost.clearSource(BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID);
|
overlayHost.clearSource(BHOTE_KOSHI_LOCATOR_OVERLAY_SOURCE_ID);
|
||||||
if (_dataSource && _viewer?.dataSources) _viewer.dataSources.remove(_dataSource, true);
|
if (_dataSource && _viewer?.dataSources)
|
||||||
|
_viewer.dataSources.remove(_dataSource, true);
|
||||||
_dataSource = null;
|
_dataSource = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCallout(dataSource, subject, color, {
|
function addCallout(
|
||||||
labelPlacement = 'right',
|
dataSource,
|
||||||
sequenceStartedAt = now(),
|
subject,
|
||||||
visibleAfterMs = 0,
|
color,
|
||||||
showAnchor = true,
|
{
|
||||||
} = {}) {
|
labelPlacement = 'right',
|
||||||
const position = Cesium.Cartesian3.fromDegrees(subject.lon, subject.lat, subject.heightM);
|
sequenceStartedAt = now(),
|
||||||
|
visibleAfterMs = 0,
|
||||||
|
showAnchor = true,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const position = Cesium.Cartesian3.fromDegrees(
|
||||||
|
subject.lon,
|
||||||
|
subject.lat,
|
||||||
|
subject.heightM,
|
||||||
|
);
|
||||||
const visibleAt = sequenceStartedAt + visibleAfterMs;
|
const visibleAt = sequenceStartedAt + visibleAfterMs;
|
||||||
const isVisible = () => now() >= visibleAt;
|
const isVisible = () => now() >= visibleAt;
|
||||||
const pulseProgress = () => {
|
const pulseProgress = () => {
|
||||||
@@ -295,7 +349,9 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
return (elapsed % CALLOUT_PULSE_DURATION_MS) / CALLOUT_PULSE_DURATION_MS;
|
return (elapsed % CALLOUT_PULSE_DURATION_MS) / CALLOUT_PULSE_DURATION_MS;
|
||||||
};
|
};
|
||||||
const ringSize = new Cesium.CallbackProperty(() => {
|
const ringSize = new Cesium.CallbackProperty(() => {
|
||||||
return CALLOUT_RING_MIN_SIZE_PX + CALLOUT_RING_SIZE_RANGE_PX * pulseProgress();
|
return (
|
||||||
|
CALLOUT_RING_MIN_SIZE_PX + CALLOUT_RING_SIZE_RANGE_PX * pulseProgress()
|
||||||
|
);
|
||||||
}, false);
|
}, false);
|
||||||
const ringColor = new Cesium.CallbackProperty((_time, result) => {
|
const ringColor = new Cesium.CallbackProperty((_time, result) => {
|
||||||
const dynamicColor = Cesium.Color.clone(color, result);
|
const dynamicColor = Cesium.Color.clone(color, result);
|
||||||
@@ -304,7 +360,9 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
}, false);
|
}, false);
|
||||||
const ringOutlineColor = new Cesium.CallbackProperty((_time, result) => {
|
const ringOutlineColor = new Cesium.CallbackProperty((_time, result) => {
|
||||||
const dynamicColor = Cesium.Color.clone(color, result);
|
const dynamicColor = Cesium.Color.clone(color, result);
|
||||||
dynamicColor.alpha = isVisible() ? 0.08 + 0.72 * (1 - pulseProgress()) : 0;
|
dynamicColor.alpha = isVisible()
|
||||||
|
? 0.08 + 0.72 * (1 - pulseProgress())
|
||||||
|
: 0;
|
||||||
return dynamicColor;
|
return dynamicColor;
|
||||||
}, false);
|
}, false);
|
||||||
if (showAnchor) {
|
if (showAnchor) {
|
||||||
@@ -318,7 +376,12 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
outlineColor: ringOutlineColor,
|
outlineColor: ringOutlineColor,
|
||||||
outlineWidth: 2,
|
outlineWidth: 2,
|
||||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
scaleByDistance: new Cesium.NearFarScalar(250000, 0.8, 25000000, 1.35),
|
scaleByDistance: new Cesium.NearFarScalar(
|
||||||
|
250000,
|
||||||
|
0.8,
|
||||||
|
25000000,
|
||||||
|
1.35,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -346,7 +409,12 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
minDistance: 0,
|
minDistance: 0,
|
||||||
maxDistance: 5_000_000,
|
maxDistance: 5_000_000,
|
||||||
distanceFadeStartRatio: 0.9,
|
distanceFadeStartRatio: 0.9,
|
||||||
distanceScale: { near: 1200, nearValue: 1, far: 4_000_000, farValue: 0.78 },
|
distanceScale: {
|
||||||
|
near: 1200,
|
||||||
|
nearValue: 1,
|
||||||
|
far: 4_000_000,
|
||||||
|
farValue: 0.78,
|
||||||
|
},
|
||||||
edgeFade: 'none',
|
edgeFade: 'none',
|
||||||
horizonCull: true,
|
horizonCull: true,
|
||||||
terrainOcclusion: false,
|
terrainOcclusion: false,
|
||||||
@@ -378,7 +446,12 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
minDistance: 0,
|
minDistance: 0,
|
||||||
maxDistance: 25_000_000,
|
maxDistance: 25_000_000,
|
||||||
distanceFadeStartRatio: 0.9,
|
distanceFadeStartRatio: 0.9,
|
||||||
distanceScale: { near: 250000, nearValue: 1.05, far: 25000000, farValue: 0.72 },
|
distanceScale: {
|
||||||
|
near: 250000,
|
||||||
|
nearValue: 1.05,
|
||||||
|
far: 25000000,
|
||||||
|
farValue: 0.72,
|
||||||
|
},
|
||||||
edgeFade: 'none',
|
edgeFade: 'none',
|
||||||
horizonCull: true,
|
horizonCull: true,
|
||||||
terrainOcclusion: false,
|
terrainOcclusion: false,
|
||||||
@@ -403,8 +476,15 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
_calloutPulseFrame = scheduleFrame(pulseFrame);
|
_calloutPulseFrame = scheduleFrame(pulseFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addIncidentCallout(dataSource, sequenceStartedAt, visibleAfterMs = 0) {
|
function addIncidentCallout(
|
||||||
addCallout(dataSource, INCIDENT, INCIDENT_COLOR, { sequenceStartedAt, visibleAfterMs });
|
dataSource,
|
||||||
|
sequenceStartedAt,
|
||||||
|
visibleAfterMs = 0,
|
||||||
|
) {
|
||||||
|
addCallout(dataSource, INCIDENT, INCIDENT_COLOR, {
|
||||||
|
sequenceStartedAt,
|
||||||
|
visibleAfterMs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTriggerRecordCallout(dataSource) {
|
function addTriggerRecordCallout(dataSource) {
|
||||||
@@ -414,22 +494,26 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
TRIGGER_RECORD.heightM,
|
TRIGGER_RECORD.heightM,
|
||||||
);
|
);
|
||||||
const isVisible = () => now() >= _triggerCalloutStartedAt;
|
const isVisible = () => now() >= _triggerCalloutStartedAt;
|
||||||
const presentation = () => anchoredCalloutPresentation(
|
const presentation = () =>
|
||||||
now() - _triggerCalloutStartedAt,
|
anchoredCalloutPresentation(now() - _triggerCalloutStartedAt);
|
||||||
);
|
|
||||||
const pulseProgress = () => {
|
const pulseProgress = () => {
|
||||||
if (!isVisible()) return 0;
|
if (!isVisible()) return 0;
|
||||||
return ((now() - _triggerCalloutStartedAt) % CALLOUT_PULSE_DURATION_MS)
|
return (
|
||||||
/ CALLOUT_PULSE_DURATION_MS;
|
((now() - _triggerCalloutStartedAt) % CALLOUT_PULSE_DURATION_MS) /
|
||||||
|
CALLOUT_PULSE_DURATION_MS
|
||||||
|
);
|
||||||
};
|
};
|
||||||
dataSource.entities.add({
|
dataSource.entities.add({
|
||||||
id: 'bhote-koshi-trigger-record-halo',
|
id: 'bhote-koshi-trigger-record-halo',
|
||||||
position,
|
position,
|
||||||
show: new Cesium.CallbackProperty(isVisible, false),
|
show: new Cesium.CallbackProperty(isVisible, false),
|
||||||
point: {
|
point: {
|
||||||
pixelSize: new Cesium.CallbackProperty(() => (
|
pixelSize: new Cesium.CallbackProperty(
|
||||||
CALLOUT_RING_MIN_SIZE_PX + CALLOUT_RING_SIZE_RANGE_PX * pulseProgress()
|
() =>
|
||||||
), false),
|
CALLOUT_RING_MIN_SIZE_PX +
|
||||||
|
CALLOUT_RING_SIZE_RANGE_PX * pulseProgress(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
color: new Cesium.CallbackProperty((_time, result) => {
|
color: new Cesium.CallbackProperty((_time, result) => {
|
||||||
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
||||||
color.alpha = 0.16 * (1 - pulseProgress());
|
color.alpha = 0.16 * (1 - pulseProgress());
|
||||||
@@ -463,9 +547,14 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
protected: true,
|
protected: true,
|
||||||
active: true,
|
active: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
accessibilityLabel: 'Open the USGS initial report for the 26 August 2026 Nepal landslide',
|
accessibilityLabel:
|
||||||
|
'Open the USGS initial report for the 26 August 2026 Nepal landslide',
|
||||||
activate: () => {
|
activate: () => {
|
||||||
const opened = window.open(TRIGGER_SOURCE_URL, '_blank', 'noopener,noreferrer');
|
const opened = window.open(
|
||||||
|
TRIGGER_SOURCE_URL,
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer',
|
||||||
|
);
|
||||||
if (opened) opened.opener = null;
|
if (opened) opened.opener = null;
|
||||||
},
|
},
|
||||||
collisionGroup: 'ambient-card',
|
collisionGroup: 'ambient-card',
|
||||||
@@ -484,8 +573,13 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startTriggerApproach(attempt) {
|
function startTriggerApproach(attempt) {
|
||||||
if (!_enabled || _presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
|
if (
|
||||||
|| _triggerApproachAttempt !== attempt || attempt.phase !== 'armed') return;
|
!_enabled ||
|
||||||
|
_presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION ||
|
||||||
|
_triggerApproachAttempt !== attempt ||
|
||||||
|
attempt.phase !== 'armed'
|
||||||
|
)
|
||||||
|
return;
|
||||||
attempt.phase = 'flying';
|
attempt.phase = 'flying';
|
||||||
if (_triggerApproachTimer !== null) cancelDelay(_triggerApproachTimer);
|
if (_triggerApproachTimer !== null) cancelDelay(_triggerApproachTimer);
|
||||||
_triggerApproachTimer = null;
|
_triggerApproachTimer = null;
|
||||||
@@ -518,9 +612,13 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
duration: TRIGGER_APPROACH_DURATION_SEC,
|
duration: TRIGGER_APPROACH_DURATION_SEC,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||||
complete: () => {
|
complete: () => {
|
||||||
if (_triggerApproachAttempt !== attempt || attempt.phase !== 'flying') return;
|
if (_triggerApproachAttempt !== attempt || attempt.phase !== 'flying')
|
||||||
if (!_enabled || generation !== _generation
|
return;
|
||||||
|| _presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) {
|
if (
|
||||||
|
!_enabled ||
|
||||||
|
generation !== _generation ||
|
||||||
|
_presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
|
||||||
|
) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -555,12 +653,16 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
_triggerApproachAttempt = attempt;
|
_triggerApproachAttempt = attempt;
|
||||||
const start = () => startTriggerApproach(attempt);
|
const start = () => startTriggerApproach(attempt);
|
||||||
const camera = _viewer?.camera;
|
const camera = _viewer?.camera;
|
||||||
if (!camera?.moveStart?.addEventListener || !camera?.moveEnd?.addEventListener) {
|
if (
|
||||||
|
!camera?.moveStart?.addEventListener ||
|
||||||
|
!camera?.moveEnd?.addEventListener
|
||||||
|
) {
|
||||||
start();
|
start();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_removeTriggerMoveStart = camera.moveStart.addEventListener(() => {
|
_removeTriggerMoveStart = camera.moveStart.addEventListener(() => {
|
||||||
if (_triggerApproachAttempt !== attempt || attempt.phase !== 'armed') return;
|
if (_triggerApproachAttempt !== attempt || attempt.phase !== 'armed')
|
||||||
|
return;
|
||||||
if (_triggerApproachTimer !== null) cancelDelay(_triggerApproachTimer);
|
if (_triggerApproachTimer !== null) cancelDelay(_triggerApproachTimer);
|
||||||
_triggerApproachTimer = null;
|
_triggerApproachTimer = null;
|
||||||
_removeTriggerMoveStart?.();
|
_removeTriggerMoveStart?.();
|
||||||
@@ -585,13 +687,19 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
|
|
||||||
function addNearbyCityCallouts(dataSource, sequenceStartedAt) {
|
function addNearbyCityCallouts(dataSource, sequenceStartedAt) {
|
||||||
NEARBY_CITIES.forEach((city, index) => {
|
NEARBY_CITIES.forEach((city, index) => {
|
||||||
const visibleAt = sequenceStartedAt
|
const visibleAt =
|
||||||
+ CITY_CALLOUT_START_DELAY_MS
|
sequenceStartedAt +
|
||||||
+ index * CITY_CALLOUT_STAGGER_MS;
|
CITY_CALLOUT_START_DELAY_MS +
|
||||||
const progress = () => smoothstep(0, 1, (now() - visibleAt) / CITY_CALLOUT_REVEAL_MS);
|
index * CITY_CALLOUT_STAGGER_MS;
|
||||||
|
const progress = () =>
|
||||||
|
smoothstep(0, 1, (now() - visibleAt) / CITY_CALLOUT_REVEAL_MS);
|
||||||
const labelOnLeft = city.placement === 'left';
|
const labelOnLeft = city.placement === 'left';
|
||||||
const labelLower = city.placement === 'right-lower';
|
const labelLower = city.placement === 'right-lower';
|
||||||
const position = Cesium.Cartesian3.fromDegrees(city.lon, city.lat, city.heightM);
|
const position = Cesium.Cartesian3.fromDegrees(
|
||||||
|
city.lon,
|
||||||
|
city.lat,
|
||||||
|
city.heightM,
|
||||||
|
);
|
||||||
queueOverlayEntry({
|
queueOverlayEntry({
|
||||||
id: `bhote-koshi-city-${city.id}`,
|
id: `bhote-koshi-city-${city.id}`,
|
||||||
position,
|
position,
|
||||||
@@ -612,7 +720,12 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
minDistance: 0,
|
minDistance: 0,
|
||||||
maxDistance: 3_000_000,
|
maxDistance: 3_000_000,
|
||||||
distanceFadeStartRatio: 0.82,
|
distanceFadeStartRatio: 0.82,
|
||||||
distanceScale: { near: 100000, nearValue: 1.08, far: 3000000, farValue: 0.78 },
|
distanceScale: {
|
||||||
|
near: 100000,
|
||||||
|
nearValue: 1.08,
|
||||||
|
far: 3000000,
|
||||||
|
farValue: 0.78,
|
||||||
|
},
|
||||||
edgeFade: 'keyhole',
|
edgeFade: 'keyhole',
|
||||||
horizonCull: true,
|
horizonCull: true,
|
||||||
terrainOcclusion: false,
|
terrainOcclusion: false,
|
||||||
@@ -622,32 +735,45 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addIncidentPlaceCallouts(dataSource, sequenceStartedAt, {
|
function addIncidentPlaceCallouts(
|
||||||
pulseHalos = false, places = INCIDENT_OVERVIEW_PLACES,
|
dataSource,
|
||||||
} = {}) {
|
sequenceStartedAt,
|
||||||
|
{ pulseHalos = false, places = INCIDENT_OVERVIEW_PLACES } = {},
|
||||||
|
) {
|
||||||
places.forEach((place, index) => {
|
places.forEach((place, index) => {
|
||||||
const visibleAt = sequenceStartedAt
|
const visibleAt =
|
||||||
+ PLACE_CALLOUT_START_DELAY_MS
|
sequenceStartedAt +
|
||||||
+ index * PLACE_CALLOUT_STAGGER_MS;
|
PLACE_CALLOUT_START_DELAY_MS +
|
||||||
const progress = () => smoothstep(0, 1, (now() - visibleAt) / PLACE_CALLOUT_REVEAL_MS);
|
index * PLACE_CALLOUT_STAGGER_MS;
|
||||||
|
const progress = () =>
|
||||||
|
smoothstep(0, 1, (now() - visibleAt) / PLACE_CALLOUT_REVEAL_MS);
|
||||||
const labelOnLeft = place.placement === 'left';
|
const labelOnLeft = place.placement === 'left';
|
||||||
const labelLower = place.placement === 'right-lower';
|
const labelLower = place.placement === 'right-lower';
|
||||||
const position = Cesium.Cartesian3.fromDegrees(place.lon, place.lat, place.heightM);
|
const position = Cesium.Cartesian3.fromDegrees(
|
||||||
|
place.lon,
|
||||||
|
place.lat,
|
||||||
|
place.heightM,
|
||||||
|
);
|
||||||
if (pulseHalos) {
|
if (pulseHalos) {
|
||||||
const pulseSequenceDurationMs = PLACE_CALLOUT_STAGGER_MS * places.length;
|
const pulseSequenceDurationMs =
|
||||||
|
PLACE_CALLOUT_STAGGER_MS * places.length;
|
||||||
const pulseWindowElapsed = () => {
|
const pulseWindowElapsed = () => {
|
||||||
const sequenceElapsed = Math.max(
|
const sequenceElapsed = Math.max(
|
||||||
0,
|
0,
|
||||||
now() - sequenceStartedAt - PLACE_CALLOUT_START_DELAY_MS,
|
now() - sequenceStartedAt - PLACE_CALLOUT_START_DELAY_MS,
|
||||||
);
|
);
|
||||||
return (sequenceElapsed % pulseSequenceDurationMs)
|
return (
|
||||||
- index * PLACE_CALLOUT_STAGGER_MS;
|
(sequenceElapsed % pulseSequenceDurationMs) -
|
||||||
|
index * PLACE_CALLOUT_STAGGER_MS
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const pulseIsActive = () => {
|
const pulseIsActive = () => {
|
||||||
const elapsed = pulseWindowElapsed();
|
const elapsed = pulseWindowElapsed();
|
||||||
return now() >= visibleAt
|
return (
|
||||||
&& elapsed >= 0
|
now() >= visibleAt &&
|
||||||
&& elapsed < PLACE_CALLOUT_STAGGER_MS;
|
elapsed >= 0 &&
|
||||||
|
elapsed < PLACE_CALLOUT_STAGGER_MS
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const pulseProgress = () => {
|
const pulseProgress = () => {
|
||||||
return Math.max(
|
return Math.max(
|
||||||
@@ -660,9 +786,12 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
position,
|
position,
|
||||||
show: new Cesium.CallbackProperty(pulseIsActive, false),
|
show: new Cesium.CallbackProperty(pulseIsActive, false),
|
||||||
point: {
|
point: {
|
||||||
pixelSize: new Cesium.CallbackProperty(() => (
|
pixelSize: new Cesium.CallbackProperty(
|
||||||
CALLOUT_RING_MIN_SIZE_PX + CALLOUT_RING_SIZE_RANGE_PX * pulseProgress()
|
() =>
|
||||||
), false),
|
CALLOUT_RING_MIN_SIZE_PX +
|
||||||
|
CALLOUT_RING_SIZE_RANGE_PX * pulseProgress(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
color: new Cesium.CallbackProperty((_time, result) => {
|
color: new Cesium.CallbackProperty((_time, result) => {
|
||||||
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
const color = Cesium.Color.clone(INCIDENT_COLOR, result);
|
||||||
color.alpha = 0.16 * (1 - pulseProgress());
|
color.alpha = 0.16 * (1 - pulseProgress());
|
||||||
@@ -699,7 +828,12 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
minDistance: 0,
|
minDistance: 0,
|
||||||
maxDistance: 500_000,
|
maxDistance: 500_000,
|
||||||
distanceFadeStartRatio: 0.82,
|
distanceFadeStartRatio: 0.82,
|
||||||
distanceScale: { near: 5000, nearValue: 1.08, far: 500000, farValue: 0.82 },
|
distanceScale: {
|
||||||
|
near: 5000,
|
||||||
|
nearValue: 1.08,
|
||||||
|
far: 500000,
|
||||||
|
farValue: 0.82,
|
||||||
|
},
|
||||||
edgeFade: 'keyhole',
|
edgeFade: 'keyhole',
|
||||||
horizonCull: true,
|
horizonCull: true,
|
||||||
terrainOcclusion: false,
|
terrainOcclusion: false,
|
||||||
@@ -710,16 +844,18 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addIncidentFloodPath(dataSource, sequenceStartedAt) {
|
function addIncidentFloodPath(dataSource, sequenceStartedAt) {
|
||||||
const positions = BHOTE_KOSHI_FLOOD_PATH.map(([lon, lat]) => (
|
const positions = BHOTE_KOSHI_FLOOD_PATH.map(([lon, lat]) =>
|
||||||
Cesium.Cartesian3.fromDegrees(lon, lat)
|
Cesium.Cartesian3.fromDegrees(lon, lat),
|
||||||
));
|
);
|
||||||
const drawStartedAt = sequenceStartedAt + FLOOD_PATH_START_DELAY_MS;
|
const drawStartedAt = sequenceStartedAt + FLOOD_PATH_START_DELAY_MS;
|
||||||
const animatedPositions = new Cesium.CallbackProperty(() => (
|
const animatedPositions = new Cesium.CallbackProperty(
|
||||||
revealPolylinePositions(
|
() =>
|
||||||
positions,
|
revealPolylinePositions(
|
||||||
(now() - drawStartedAt) / FLOOD_PATH_DRAW_DURATION_MS,
|
positions,
|
||||||
)
|
(now() - drawStartedAt) / FLOOD_PATH_DRAW_DURATION_MS,
|
||||||
), false);
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
dataSource.entities.add({
|
dataSource.entities.add({
|
||||||
id: 'bhote-koshi-flood-path',
|
id: 'bhote-koshi-flood-path',
|
||||||
show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false),
|
show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false),
|
||||||
@@ -739,9 +875,14 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
const positions = Cesium.Cartesian3.fromDegreesArray(closed.flat());
|
const positions = Cesium.Cartesian3.fromDegreesArray(closed.flat());
|
||||||
const hierarchy = new Cesium.PolygonHierarchy(positions.slice(0, -1));
|
const hierarchy = new Cesium.PolygonHierarchy(positions.slice(0, -1));
|
||||||
const drawStartedAt = sequenceStartedAt + BORDER_START_DELAY_MS;
|
const drawStartedAt = sequenceStartedAt + BORDER_START_DELAY_MS;
|
||||||
const animatedPositions = new Cesium.CallbackProperty(() => (
|
const animatedPositions = new Cesium.CallbackProperty(
|
||||||
revealPolylinePositions(positions, (now() - drawStartedAt) / BORDER_DRAW_DURATION_MS)
|
() =>
|
||||||
), false);
|
revealPolylinePositions(
|
||||||
|
positions,
|
||||||
|
(now() - drawStartedAt) / BORDER_DRAW_DURATION_MS,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
dataSource.entities.add({
|
dataSource.entities.add({
|
||||||
id: 'nepal-border-fill',
|
id: 'nepal-border-fill',
|
||||||
show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false),
|
show: new Cesium.CallbackProperty(() => now() >= drawStartedAt, false),
|
||||||
@@ -773,8 +914,12 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
});
|
});
|
||||||
const generation = _generation;
|
const generation = _generation;
|
||||||
const drawFrame = () => {
|
const drawFrame = () => {
|
||||||
if (!_enabled || generation !== _generation
|
if (
|
||||||
|| !presentationAnimatesBoundary(_presentation)) return;
|
!_enabled ||
|
||||||
|
generation !== _generation ||
|
||||||
|
!presentationAnimatesBoundary(_presentation)
|
||||||
|
)
|
||||||
|
return;
|
||||||
requestRender('bhote-koshi-locator-border-draw');
|
requestRender('bhote-koshi-locator-border-draw');
|
||||||
if (now() - drawStartedAt < BORDER_DRAW_DURATION_MS) {
|
if (now() - drawStartedAt < BORDER_DRAW_DURATION_MS) {
|
||||||
_borderAnimationFrame = scheduleFrame(drawFrame);
|
_borderAnimationFrame = scheduleFrame(drawFrame);
|
||||||
@@ -797,37 +942,43 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
} else if (_presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION) {
|
} else if (_presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION) {
|
||||||
const sequenceStartedAt = now();
|
const sequenceStartedAt = now();
|
||||||
addIncidentPlaceCallouts(_dataSource, sequenceStartedAt, {
|
addIncidentPlaceCallouts(_dataSource, sequenceStartedAt, {
|
||||||
pulseHalos: true, places: INCIDENT_OVERVIEW_PLACES,
|
pulseHalos: true,
|
||||||
|
places: INCIDENT_OVERVIEW_PLACES,
|
||||||
});
|
});
|
||||||
_placeRevealMoving = true;
|
_placeRevealMoving = true;
|
||||||
_placeRevealSettledAt = sequenceStartedAt
|
_placeRevealSettledAt =
|
||||||
+ PLACE_CALLOUT_START_DELAY_MS
|
sequenceStartedAt +
|
||||||
+ (INCIDENT_OVERVIEW_PLACES.length - 1) * PLACE_CALLOUT_STAGGER_MS
|
PLACE_CALLOUT_START_DELAY_MS +
|
||||||
+ PLACE_CALLOUT_REVEAL_MS;
|
(INCIDENT_OVERVIEW_PLACES.length - 1) * PLACE_CALLOUT_STAGGER_MS +
|
||||||
|
PLACE_CALLOUT_REVEAL_MS;
|
||||||
} else if (_presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION) {
|
} else if (_presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION) {
|
||||||
const sequenceStartedAt = now();
|
const sequenceStartedAt = now();
|
||||||
const settledPlacesAt = sequenceStartedAt
|
const settledPlacesAt =
|
||||||
- PLACE_CALLOUT_START_DELAY_MS
|
sequenceStartedAt -
|
||||||
- (INCIDENT_OVERVIEW_PLACES.length - 1) * PLACE_CALLOUT_STAGGER_MS
|
PLACE_CALLOUT_START_DELAY_MS -
|
||||||
- PLACE_CALLOUT_REVEAL_MS;
|
(INCIDENT_OVERVIEW_PLACES.length - 1) * PLACE_CALLOUT_STAGGER_MS -
|
||||||
|
PLACE_CALLOUT_REVEAL_MS;
|
||||||
addIncidentFloodPath(_dataSource, sequenceStartedAt);
|
addIncidentFloodPath(_dataSource, sequenceStartedAt);
|
||||||
addIncidentPlaceCallouts(_dataSource, settledPlacesAt);
|
addIncidentPlaceCallouts(_dataSource, settledPlacesAt);
|
||||||
} else if (_presentation === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION) {
|
} else if (_presentation === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION) {
|
||||||
const sequenceStartedAt = now();
|
const sequenceStartedAt = now();
|
||||||
const settledPathAt = sequenceStartedAt
|
const settledPathAt =
|
||||||
- FLOOD_PATH_START_DELAY_MS
|
sequenceStartedAt -
|
||||||
- FLOOD_PATH_DRAW_DURATION_MS;
|
FLOOD_PATH_START_DELAY_MS -
|
||||||
const settledPlacesAt = sequenceStartedAt
|
FLOOD_PATH_DRAW_DURATION_MS;
|
||||||
- PLACE_CALLOUT_START_DELAY_MS
|
const settledPlacesAt =
|
||||||
- (INCIDENT_OVERVIEW_PLACES.length - 1) * PLACE_CALLOUT_STAGGER_MS
|
sequenceStartedAt -
|
||||||
- PLACE_CALLOUT_REVEAL_MS;
|
PLACE_CALLOUT_START_DELAY_MS -
|
||||||
|
(INCIDENT_OVERVIEW_PLACES.length - 1) * PLACE_CALLOUT_STAGGER_MS -
|
||||||
|
PLACE_CALLOUT_REVEAL_MS;
|
||||||
addIncidentFloodPath(_dataSource, settledPathAt);
|
addIncidentFloodPath(_dataSource, settledPathAt);
|
||||||
addIncidentPlaceCallouts(_dataSource, settledPlacesAt);
|
addIncidentPlaceCallouts(_dataSource, settledPlacesAt);
|
||||||
} else if (_presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) {
|
} else if (_presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) {
|
||||||
const sequenceStartedAt = now();
|
const sequenceStartedAt = now();
|
||||||
const settledPathAt = sequenceStartedAt
|
const settledPathAt =
|
||||||
- FLOOD_PATH_START_DELAY_MS
|
sequenceStartedAt -
|
||||||
- FLOOD_PATH_DRAW_DURATION_MS;
|
FLOOD_PATH_START_DELAY_MS -
|
||||||
|
FLOOD_PATH_DRAW_DURATION_MS;
|
||||||
addIncidentFloodPath(_dataSource, settledPathAt);
|
addIncidentFloodPath(_dataSource, settledPathAt);
|
||||||
addTriggerRecordCallout(_dataSource);
|
addTriggerRecordCallout(_dataSource);
|
||||||
armTriggerApproach();
|
armTriggerApproach();
|
||||||
@@ -841,14 +992,22 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_presentation === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION) {
|
if (_presentation === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION) {
|
||||||
const settledAt = sequenceStartedAt - BORDER_START_DELAY_MS - BORDER_DRAW_DURATION_MS;
|
const settledAt =
|
||||||
if (_boundaryRing) addNepalBoundary(_dataSource, _boundaryRing, settledAt);
|
sequenceStartedAt - BORDER_START_DELAY_MS - BORDER_DRAW_DURATION_MS;
|
||||||
addIncidentCallout(_dataSource, sequenceStartedAt - CALLOUT_REVEAL_DURATION_MS);
|
if (_boundaryRing)
|
||||||
|
addNepalBoundary(_dataSource, _boundaryRing, settledAt);
|
||||||
|
addIncidentCallout(
|
||||||
|
_dataSource,
|
||||||
|
sequenceStartedAt - CALLOUT_REVEAL_DURATION_MS,
|
||||||
|
);
|
||||||
addNearbyCityCallouts(_dataSource, sequenceStartedAt);
|
addNearbyCityCallouts(_dataSource, sequenceStartedAt);
|
||||||
} else {
|
} else {
|
||||||
if (_boundaryRing) addNepalBoundary(_dataSource, _boundaryRing, sequenceStartedAt);
|
if (_boundaryRing)
|
||||||
|
addNepalBoundary(_dataSource, _boundaryRing, sequenceStartedAt);
|
||||||
const incidentDelayMs = _boundaryRing
|
const incidentDelayMs = _boundaryRing
|
||||||
? BORDER_START_DELAY_MS + BORDER_DRAW_DURATION_MS + INCIDENT_CALLOUT_GAP_MS
|
? BORDER_START_DELAY_MS +
|
||||||
|
BORDER_DRAW_DURATION_MS +
|
||||||
|
INCIDENT_CALLOUT_GAP_MS
|
||||||
: BORDER_START_DELAY_MS;
|
: BORDER_START_DELAY_MS;
|
||||||
addIncidentCallout(_dataSource, sequenceStartedAt, incidentDelayMs);
|
addIncidentCallout(_dataSource, sequenceStartedAt, incidentDelayMs);
|
||||||
}
|
}
|
||||||
@@ -880,7 +1039,8 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const boundary = await boundaryResolver(signal);
|
const boundary = await boundaryResolver(signal);
|
||||||
if (signal?.aborted || !_enabled || generation !== _generation) throw abortError();
|
if (signal?.aborted || !_enabled || generation !== _generation)
|
||||||
|
throw abortError();
|
||||||
_boundaryRing = closeRegionRing(boundary?.ring);
|
_boundaryRing = closeRegionRing(boundary?.ring);
|
||||||
_boundaryReady = _boundaryRing.length >= 4;
|
_boundaryReady = _boundaryRing.length >= 4;
|
||||||
_boundaryResolved = true;
|
_boundaryResolved = true;
|
||||||
@@ -897,7 +1057,10 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
if (generation === _generation) removeDataSource();
|
if (generation === _generation) removeDataSource();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
console.warn('[Data:BhoteKoshiLocator] Nepal boundary lookup failed:', error);
|
console.warn(
|
||||||
|
'[Data:BhoteKoshiLocator] Nepal boundary lookup failed:',
|
||||||
|
error,
|
||||||
|
);
|
||||||
_boundaryResolved = true;
|
_boundaryResolved = true;
|
||||||
_boundaryRing = null;
|
_boundaryRing = null;
|
||||||
if (presentationUsesBoundary(_presentation)) renderPresentation();
|
if (presentationUsesBoundary(_presentation)) renderPresentation();
|
||||||
@@ -940,8 +1103,9 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
},
|
},
|
||||||
setParams(params = {}) {
|
setParams(params = {}) {
|
||||||
_presentation = normalizePresentation(params.presentation);
|
_presentation = normalizePresentation(params.presentation);
|
||||||
_approachEnabled = _presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
|
_approachEnabled =
|
||||||
&& params.preview !== true;
|
_presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION &&
|
||||||
|
params.preview !== true;
|
||||||
if (_enabled) renderPresentation();
|
if (_enabled) renderPresentation();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -955,35 +1119,40 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
id: 'nepal-context',
|
id: 'nepal-context',
|
||||||
label: 'NEPAL',
|
label: 'NEPAL',
|
||||||
active: _presentation === NEPAL_CONTEXT_PRESENTATION,
|
active: _presentation === NEPAL_CONTEXT_PRESENTATION,
|
||||||
title: 'Show the Nepal context callout without a border or country fill',
|
title:
|
||||||
|
'Show the Nepal context callout without a border or country fill',
|
||||||
params: { presentation: NEPAL_CONTEXT_PRESENTATION },
|
params: { presentation: NEPAL_CONTEXT_PRESENTATION },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bhote-koshi-regional',
|
id: 'bhote-koshi-regional',
|
||||||
label: 'BHOTE',
|
label: 'BHOTE',
|
||||||
active: _presentation === BHOTE_KOSHI_REGIONAL_PRESENTATION,
|
active: _presentation === BHOTE_KOSHI_REGIONAL_PRESENTATION,
|
||||||
title: 'Introduce Nepal, draw its border, then reveal the Bhote Koshi incident',
|
title:
|
||||||
|
'Introduce Nepal, draw its border, then reveal the Bhote Koshi incident',
|
||||||
params: { presentation: BHOTE_KOSHI_REGIONAL_PRESENTATION },
|
params: { presentation: BHOTE_KOSHI_REGIONAL_PRESENTATION },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bhote-koshi-city-context',
|
id: 'bhote-koshi-city-context',
|
||||||
label: 'CITIES',
|
label: 'CITIES',
|
||||||
active: _presentation === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION,
|
active: _presentation === BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION,
|
||||||
title: 'Keep the regional incident context and introduce nearby city labels',
|
title:
|
||||||
|
'Keep the regional incident context and introduce nearby city labels',
|
||||||
params: { presentation: BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION },
|
params: { presentation: BHOTE_KOSHI_CITY_CONTEXT_PRESENTATION },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bhote-koshi-incident-places',
|
id: 'bhote-koshi-incident-places',
|
||||||
label: 'PLACES',
|
label: 'PLACES',
|
||||||
active: _presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION,
|
active: _presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION,
|
||||||
title: 'Introduce the reconstruction scene key places from trigger to downstream',
|
title:
|
||||||
|
'Introduce the reconstruction scene key places from trigger to downstream',
|
||||||
params: { presentation: BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION },
|
params: { presentation: BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bhote-koshi-flood-path',
|
id: 'bhote-koshi-flood-path',
|
||||||
label: 'PATH',
|
label: 'PATH',
|
||||||
active: _presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION,
|
active: _presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION,
|
||||||
title: 'Animate the sourced cyan river-centerline route to Trishuli Bazaar',
|
title:
|
||||||
|
'Animate the sourced cyan river-centerline route to Trishuli Bazaar',
|
||||||
params: { presentation: BHOTE_KOSHI_FLOOD_PATH_PRESENTATION },
|
params: { presentation: BHOTE_KOSHI_FLOOD_PATH_PRESENTATION },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -997,7 +1166,8 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
id: 'bhote-koshi-trigger-record',
|
id: 'bhote-koshi-trigger-record',
|
||||||
label: 'RECORD',
|
label: 'RECORD',
|
||||||
active: _presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION,
|
active: _presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION,
|
||||||
title: 'Reveal the trigger record while making a slow camera approach',
|
title:
|
||||||
|
'Reveal the trigger record while making a slow camera approach',
|
||||||
params: {
|
params: {
|
||||||
presentation: BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION,
|
presentation: BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION,
|
||||||
preview: true,
|
preview: true,
|
||||||
@@ -1012,11 +1182,11 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
count: _enabled ? 1 : 0,
|
count: _enabled ? 1 : 0,
|
||||||
status: !_enabled
|
status: !_enabled
|
||||||
? 'idle'
|
? 'idle'
|
||||||
: (_presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION
|
: _presentation === BHOTE_KOSHI_INCIDENT_PLACES_PRESENTATION ||
|
||||||
|| _presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION
|
_presentation === BHOTE_KOSHI_FLOOD_PATH_PRESENTATION ||
|
||||||
|| _presentation === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION
|
_presentation === BHOTE_KOSHI_PATH_OVERVIEW_PRESENTATION ||
|
||||||
|| _presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
|
_presentation === BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION ||
|
||||||
|| _boundaryReady)
|
_boundaryReady
|
||||||
? 'nominal'
|
? 'nominal'
|
||||||
: 'degraded',
|
: 'degraded',
|
||||||
source: 'OpenStreetMap + GeoPera',
|
source: 'OpenStreetMap + GeoPera',
|
||||||
|
|||||||
@@ -1,26 +1,40 @@
|
|||||||
import { BHOTE_KOSHI_FLOOD_PATH } from './bhoteKoshiFloodPath.js';
|
import { BHOTE_KOSHI_FLOOD_PATH } from './bhoteKoshiFloodPath.js';
|
||||||
|
|
||||||
export const SOURCE_PATH_BEAT_IDS = Object.freeze([
|
export const SOURCE_PATH_BEAT_IDS = Object.freeze([
|
||||||
'debris-dammed-lake', 'second-landslide', 'dhunche',
|
'debris-dammed-lake',
|
||||||
'mailung-upper-trishuli', 'mailung-bazzar', 'dandagaun',
|
'second-landslide',
|
||||||
'dandagaun-viewpoint', 'betrawati-bazaar', 'bhainse', 'bidur-trishuli-bridge',
|
'dhunche',
|
||||||
|
'mailung-upper-trishuli',
|
||||||
|
'mailung-bazzar',
|
||||||
|
'dandagaun',
|
||||||
|
'dandagaun-viewpoint',
|
||||||
|
'betrawati-bazaar',
|
||||||
|
'bhainse',
|
||||||
|
'bidur-trishuli-bridge',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function nearestVertex(point, route) {
|
function nearestVertex(point, route) {
|
||||||
if (!point || !Number.isFinite(point.lon) || !Number.isFinite(point.lat)) return -1;
|
if (!point || !Number.isFinite(point.lon) || !Number.isFinite(point.lat))
|
||||||
const longitudeScale = Math.cos(point.lat * Math.PI / 180);
|
return -1;
|
||||||
|
const longitudeScale = Math.cos((point.lat * Math.PI) / 180);
|
||||||
let nearest = -1;
|
let nearest = -1;
|
||||||
let distance = 1000; // Witness viewpoints can sit beside the river, not on it.
|
let distance = 1000; // Witness viewpoints can sit beside the river, not on it.
|
||||||
route.forEach(([lon, lat], index) => {
|
route.forEach(([lon, lat], index) => {
|
||||||
const metres = Math.hypot((lon - point.lon) * longitudeScale, lat - point.lat) * 111195;
|
const metres =
|
||||||
if (metres < distance) { distance = metres; nearest = index; }
|
Math.hypot((lon - point.lon) * longitudeScale, lat - point.lat) * 111195;
|
||||||
|
if (metres < distance) {
|
||||||
|
distance = metres;
|
||||||
|
nearest = index;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return nearest;
|
return nearest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Split the already-bundled GeoPera centreline; never connect witness pins. */
|
/** Split the already-bundled GeoPera centreline; never connect witness pins. */
|
||||||
export function buildSourceShotPaths(event, route = BHOTE_KOSHI_FLOOD_PATH) {
|
export function buildSourceShotPaths(event, route = BHOTE_KOSHI_FLOOD_PATH) {
|
||||||
const observations = new Map((event.evidenceSpine || []).map((point) => [point.id, point]));
|
const observations = new Map(
|
||||||
|
(event.evidenceSpine || []).map((point) => [point.id, point]),
|
||||||
|
);
|
||||||
const result = new Map();
|
const result = new Map();
|
||||||
const add = (id, from, to, { joinComparison = false } = {}) => {
|
const add = (id, from, to, { joinComparison = false } = {}) => {
|
||||||
let start = nearestVertex(from, route);
|
let start = nearestVertex(from, route);
|
||||||
@@ -36,24 +50,42 @@ export function buildSourceShotPaths(event, route = BHOTE_KOSHI_FLOOD_PATH) {
|
|||||||
// downstream of it, rather than replaying the upstream half-segment.
|
// downstream of it, rather than replaying the upstream half-segment.
|
||||||
const [lon, lat] = route[start];
|
const [lon, lat] = route[start];
|
||||||
const [nextLon, nextLat] = route[start + 1];
|
const [nextLon, nextLat] = route[start + 1];
|
||||||
const scale = Math.cos(from.lat * Math.PI / 180);
|
const scale = Math.cos((from.lat * Math.PI) / 180);
|
||||||
const along = (from.lon - lon) * (nextLon - lon) * scale * scale
|
const along =
|
||||||
+ (from.lat - lat) * (nextLat - lat);
|
(from.lon - lon) * (nextLon - lon) * scale * scale +
|
||||||
|
(from.lat - lat) * (nextLat - lat);
|
||||||
if (along > 0) start++;
|
if (along > 0) start++;
|
||||||
}
|
}
|
||||||
const coordinates = route.slice(start, end + 1);
|
const coordinates = route.slice(start, end + 1);
|
||||||
if (joinComparison && (coordinates[0][0] !== from.lon || coordinates[0][1] !== from.lat)) {
|
if (
|
||||||
|
joinComparison &&
|
||||||
|
(coordinates[0][0] !== from.lon || coordinates[0][1] !== from.lat)
|
||||||
|
) {
|
||||||
coordinates.unshift([from.lon, from.lat]);
|
coordinates.unshift([from.lon, from.lat]);
|
||||||
}
|
}
|
||||||
result.set(id, coordinates.map(([lon, lat], index) => ({
|
result.set(
|
||||||
lon, lat,
|
id,
|
||||||
// Stable source profile for explicit source-elevation shots; otherwise a
|
coordinates.map(([lon, lat], index) => ({
|
||||||
// fallback when neither rendered mesh nor elevation terrain can be sampled.
|
lon,
|
||||||
fallbackElevationM: startHeight + (endHeight - startHeight) * index / (coordinates.length - 1),
|
lat,
|
||||||
})));
|
// Stable source profile for explicit source-elevation shots; otherwise a
|
||||||
|
// fallback when neither rendered mesh nor elevation terrain can be sampled.
|
||||||
|
fallbackElevationM:
|
||||||
|
startHeight +
|
||||||
|
((endHeight - startHeight) * index) / (coordinates.length - 1),
|
||||||
|
})),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
add('debris-dammed-lake', observations.get('debris-dammed-lake'), observations.get('second-landslide'));
|
add(
|
||||||
add('second-landslide', observations.get('second-landslide'), observations.get('gyirong-border-gate'));
|
'debris-dammed-lake',
|
||||||
|
observations.get('debris-dammed-lake'),
|
||||||
|
observations.get('second-landslide'),
|
||||||
|
);
|
||||||
|
add(
|
||||||
|
'second-landslide',
|
||||||
|
observations.get('second-landslide'),
|
||||||
|
observations.get('gyirong-border-gate'),
|
||||||
|
);
|
||||||
let previous = event.reconstruction?.corridor?.at(-1);
|
let previous = event.reconstruction?.corridor?.at(-1);
|
||||||
for (const id of SOURCE_PATH_BEAT_IDS.slice(2)) {
|
for (const id of SOURCE_PATH_BEAT_IDS.slice(2)) {
|
||||||
const point = observations.get(id);
|
const point = observations.get(id);
|
||||||
|
|||||||
+20
-4
@@ -302,10 +302,26 @@ export const SHARE_TRACKING_RESTORE_POLICIES = Object.freeze({
|
|||||||
* owns stable URL ordering.
|
* owns stable URL ordering.
|
||||||
*/
|
*/
|
||||||
export const LAYER_STATE_REGISTRY = Object.freeze([
|
export const LAYER_STATE_REGISTRY = Object.freeze([
|
||||||
Object.freeze({ id: 'ais-live-vessels', token: 'a', disposition: 'enabled-only' }),
|
Object.freeze({
|
||||||
Object.freeze({ id: 'alpr-cameras', token: 'p', disposition: 'enabled-only' }),
|
id: 'ais-live-vessels',
|
||||||
Object.freeze({ id: 'bhote-koshi-2026', token: 'h', disposition: 'enabled-only' }),
|
token: 'a',
|
||||||
Object.freeze({ id: 'bhote-koshi-locator', token: 'z', disposition: 'enabled-only' }),
|
disposition: 'enabled-only',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: 'alpr-cameras',
|
||||||
|
token: 'p',
|
||||||
|
disposition: 'enabled-only',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: 'bhote-koshi-2026',
|
||||||
|
token: 'h',
|
||||||
|
disposition: 'enabled-only',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: 'bhote-koshi-locator',
|
||||||
|
token: 'z',
|
||||||
|
disposition: 'enabled-only',
|
||||||
|
}),
|
||||||
Object.freeze({ id: 'bikeshare', token: 'b', disposition: 'enabled-only' }),
|
Object.freeze({ id: 'bikeshare', token: 'b', disposition: 'enabled-only' }),
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
id: 'cctv',
|
id: 'cctv',
|
||||||
|
|||||||
@@ -504,18 +504,22 @@ export function normalizeOverlayEntry(sourceId, entry) {
|
|||||||
: 'keyhole',
|
: 'keyhole',
|
||||||
horizonCull: entry.horizonCull !== false,
|
horizonCull: entry.horizonCull !== false,
|
||||||
terrainOcclusion: entry.terrainOcclusion === true,
|
terrainOcclusion: entry.terrainOcclusion === true,
|
||||||
sourceAlpha: typeof (entry.sourceAlpha ?? entry.alpha) === 'function'
|
sourceAlpha:
|
||||||
? (entry.sourceAlpha ?? entry.alpha)
|
typeof (entry.sourceAlpha ?? entry.alpha) === 'function'
|
||||||
: clamp01(entry.sourceAlpha ?? entry.alpha, 1),
|
? (entry.sourceAlpha ?? entry.alpha)
|
||||||
presentationScale: typeof entry.presentationScale === 'function'
|
: clamp01(entry.sourceAlpha ?? entry.alpha, 1),
|
||||||
? entry.presentationScale
|
presentationScale:
|
||||||
: Math.max(0, Number(entry.presentationScale) || 1),
|
typeof entry.presentationScale === 'function'
|
||||||
leaderProgress: typeof entry.leaderProgress === 'function'
|
? entry.presentationScale
|
||||||
? entry.leaderProgress
|
: Math.max(0, Number(entry.presentationScale) || 1),
|
||||||
: clamp01(entry.leaderProgress, 1),
|
leaderProgress:
|
||||||
contentAlpha: typeof entry.contentAlpha === 'function'
|
typeof entry.leaderProgress === 'function'
|
||||||
? entry.contentAlpha
|
? entry.leaderProgress
|
||||||
: clamp01(entry.contentAlpha, 1),
|
: clamp01(entry.leaderProgress, 1),
|
||||||
|
contentAlpha:
|
||||||
|
typeof entry.contentAlpha === 'function'
|
||||||
|
? entry.contentAlpha
|
||||||
|
: clamp01(entry.contentAlpha, 1),
|
||||||
anchorDot: entry.anchorDot === true,
|
anchorDot: entry.anchorDot === true,
|
||||||
leaderStyle: entry.leaderStyle === 'elbow' ? 'elbow' : 'straight',
|
leaderStyle: entry.leaderStyle === 'elbow' ? 'elbow' : 'straight',
|
||||||
temporalAlpha: clamp01(entry.temporalAlpha, 1),
|
temporalAlpha: clamp01(entry.temporalAlpha, 1),
|
||||||
@@ -1805,19 +1809,36 @@ function resetFrameDomains() {
|
|||||||
// ordinary moving-source workload at a non-inlined call boundary.
|
// ordinary moving-source workload at a non-inlined call boundary.
|
||||||
function applyPresentation(entry, source, record) {
|
function applyPresentation(entry, source, record) {
|
||||||
try {
|
try {
|
||||||
const scale = Number(typeof entry.presentationScale === 'function'
|
const scale = Number(
|
||||||
? entry.presentationScale() : entry.presentationScale);
|
typeof entry.presentationScale === 'function'
|
||||||
|
? entry.presentationScale()
|
||||||
|
: entry.presentationScale,
|
||||||
|
);
|
||||||
record.paintScale *= Number.isFinite(scale) ? Math.max(0, scale) : 1;
|
record.paintScale *= Number.isFinite(scale) ? Math.max(0, scale) : 1;
|
||||||
const alpha = Number(typeof entry.sourceAlpha === 'function'
|
const alpha = Number(
|
||||||
? entry.sourceAlpha() : entry.sourceAlpha);
|
typeof entry.sourceAlpha === 'function'
|
||||||
record.sourceAlpha = source.options.alpha
|
? entry.sourceAlpha()
|
||||||
* (Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1);
|
: entry.sourceAlpha,
|
||||||
const leader = Number(typeof entry.leaderProgress === 'function'
|
);
|
||||||
? entry.leaderProgress() : entry.leaderProgress);
|
record.sourceAlpha =
|
||||||
record.leaderProgress = Number.isFinite(leader) ? Math.max(0, Math.min(1, leader)) : 1;
|
source.options.alpha *
|
||||||
const content = Number(typeof entry.contentAlpha === 'function'
|
(Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1);
|
||||||
? entry.contentAlpha() : entry.contentAlpha);
|
const leader = Number(
|
||||||
record.contentAlpha = Number.isFinite(content) ? Math.max(0, Math.min(1, content)) : 1;
|
typeof entry.leaderProgress === 'function'
|
||||||
|
? entry.leaderProgress()
|
||||||
|
: entry.leaderProgress,
|
||||||
|
);
|
||||||
|
record.leaderProgress = Number.isFinite(leader)
|
||||||
|
? Math.max(0, Math.min(1, leader))
|
||||||
|
: 1;
|
||||||
|
const content = Number(
|
||||||
|
typeof entry.contentAlpha === 'function'
|
||||||
|
? entry.contentAlpha()
|
||||||
|
: entry.contentAlpha,
|
||||||
|
);
|
||||||
|
record.contentAlpha = Number.isFinite(content)
|
||||||
|
? Math.max(0, Math.min(1, content))
|
||||||
|
: 1;
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
@@ -1942,19 +1963,31 @@ function snapshotAndProject(entry, source, viewProjection, keyhole) {
|
|||||||
}
|
}
|
||||||
record.paintScale *= altitudeFactor;
|
record.paintScale *= altitudeFactor;
|
||||||
}
|
}
|
||||||
record.altitudeAlpha = entry.altitudeFadeEnd === Number.POSITIVE_INFINITY
|
record.altitudeAlpha =
|
||||||
? (Number.isFinite(cameraAltitude) && cameraAltitude < entry.minAltitude ? 0 : 1)
|
entry.altitudeFadeEnd === Number.POSITIVE_INFINITY
|
||||||
: altitudeFade(cameraAltitude, record.altitudeOptions);
|
? Number.isFinite(cameraAltitude) && cameraAltitude < entry.minAltitude
|
||||||
|
? 0
|
||||||
|
: 1
|
||||||
|
: altitudeFade(cameraAltitude, record.altitudeOptions);
|
||||||
record.leaderProgress = 1;
|
record.leaderProgress = 1;
|
||||||
record.contentAlpha = 1;
|
record.contentAlpha = 1;
|
||||||
if (entry.presentationScale !== 1 || entry.leaderProgress !== 1
|
if (
|
||||||
|| entry.contentAlpha !== 1 || typeof entry.sourceAlpha === 'function') {
|
entry.presentationScale !== 1 ||
|
||||||
|
entry.leaderProgress !== 1 ||
|
||||||
|
entry.contentAlpha !== 1 ||
|
||||||
|
typeof entry.sourceAlpha === 'function'
|
||||||
|
) {
|
||||||
if (!applyPresentation(entry, source, record)) return null;
|
if (!applyPresentation(entry, source, record)) return null;
|
||||||
} else {
|
} else {
|
||||||
record.sourceAlpha = source.options.alpha * entry.sourceAlpha;
|
record.sourceAlpha = source.options.alpha * entry.sourceAlpha;
|
||||||
}
|
}
|
||||||
if (record.distanceAlpha <= 0 || record.paintScale <= 0
|
if (
|
||||||
|| record.altitudeAlpha <= 0 || record.sourceAlpha <= 0) return null;
|
record.distanceAlpha <= 0 ||
|
||||||
|
record.paintScale <= 0 ||
|
||||||
|
record.altitudeAlpha <= 0 ||
|
||||||
|
record.sourceAlpha <= 0
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
measureOverlayEntry(_ctx, entry, record.layout);
|
measureOverlayEntry(_ctx, entry, record.layout);
|
||||||
record.placementInput.anchorX = record.screen.x;
|
record.placementInput.anchorX = record.screen.x;
|
||||||
|
|||||||
@@ -572,13 +572,11 @@ function drawLeader(
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(startX, startY);
|
ctx.moveTo(startX, startY);
|
||||||
if (
|
if (
|
||||||
style === 'elbow'
|
style === 'elbow' &&
|
||||||
&& (placement.corner === 'left' || placement.corner === 'right')
|
(placement.corner === 'left' || placement.corner === 'right')
|
||||||
) {
|
) {
|
||||||
const elbowY = placement.rect.y + Math.min(
|
const elbowY =
|
||||||
Math.max(12, placement.rect.h * 0.12),
|
placement.rect.y + Math.min(Math.max(12, placement.rect.h * 0.12), 26);
|
||||||
26,
|
|
||||||
);
|
|
||||||
const verticalLength = Math.abs(elbowY - startY);
|
const verticalLength = Math.abs(elbowY - startY);
|
||||||
const horizontalLength = Math.abs(placement.leadToX - startX);
|
const horizontalLength = Math.abs(placement.leadToX - startX);
|
||||||
const totalLength = verticalLength + horizontalLength;
|
const totalLength = verticalLength + horizontalLength;
|
||||||
@@ -590,7 +588,10 @@ function drawLeader(
|
|||||||
ctx.lineTo(startX, elbowY);
|
ctx.lineTo(startX, elbowY);
|
||||||
const remaining = Math.max(0, visibleLength - verticalLength);
|
const remaining = Math.max(0, visibleLength - verticalLength);
|
||||||
const direction = Math.sign(placement.leadToX - startX);
|
const direction = Math.sign(placement.leadToX - startX);
|
||||||
ctx.lineTo(startX + Math.min(horizontalLength, remaining) * direction, elbowY);
|
ctx.lineTo(
|
||||||
|
startX + Math.min(horizontalLength, remaining) * direction,
|
||||||
|
elbowY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
return;
|
return;
|
||||||
@@ -615,10 +616,17 @@ function drawAnchorDot(ctx, entry, placement) {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCardChrome(ctx, entry, placement, selected = false, drawLeaderLine = true) {
|
function drawCardChrome(
|
||||||
|
ctx,
|
||||||
|
entry,
|
||||||
|
placement,
|
||||||
|
selected = false,
|
||||||
|
drawLeaderLine = true,
|
||||||
|
) {
|
||||||
const { x, y, w, h } = placement.rect;
|
const { x, y, w, h } = placement.rect;
|
||||||
const accent = entry.accent || WORLD_OVERLAY_STYLE.accent;
|
const accent = entry.accent || WORLD_OVERLAY_STYLE.accent;
|
||||||
if (drawLeaderLine) drawLeader(ctx, placement, accent, 1, 1, entry.leaderStyle);
|
if (drawLeaderLine)
|
||||||
|
drawLeader(ctx, placement, accent, 1, 1, entry.leaderStyle);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
roundedRectPath(ctx, x, y, w, h);
|
roundedRectPath(ctx, x, y, w, h);
|
||||||
ctx.fillStyle = selected
|
ctx.fillStyle = selected
|
||||||
@@ -893,7 +901,8 @@ export function paintCard(
|
|||||||
entry.leaderStyle,
|
entry.leaderStyle,
|
||||||
);
|
);
|
||||||
drawAnchorDot(ctx, entry, placement);
|
drawAnchorDot(ctx, entry, placement);
|
||||||
ctx.globalAlpha = alpha * Math.max(0, Math.min(1, Number(contentAlpha) || 0));
|
ctx.globalAlpha =
|
||||||
|
alpha * Math.max(0, Math.min(1, Number(contentAlpha) || 0));
|
||||||
drawCardChrome(ctx, entry, placement, false, false);
|
drawCardChrome(ctx, entry, placement, false, false);
|
||||||
} else {
|
} else {
|
||||||
drawCardChrome(ctx, entry, placement, false);
|
drawCardChrome(ctx, entry, placement, false);
|
||||||
@@ -1112,15 +1121,33 @@ export function paintOverlayEntry(
|
|||||||
leaderProgress = 1,
|
leaderProgress = 1,
|
||||||
contentAlpha = 1,
|
contentAlpha = 1,
|
||||||
) {
|
) {
|
||||||
if (entry.cardStyle === 'tactical') return paintTacticalCard(ctx, entry, placement, alpha);
|
if (entry.cardStyle === 'tactical')
|
||||||
if (entry.variant === 'tracked') return paintTracked(ctx, entry, placement, alpha);
|
return paintTacticalCard(ctx, entry, placement, alpha);
|
||||||
if (entry.selected || entry.variant === 'selected') return paintSelected(ctx, entry, placement, alpha);
|
if (entry.variant === 'tracked')
|
||||||
|
return paintTracked(ctx, entry, placement, alpha);
|
||||||
|
if (entry.selected || entry.variant === 'selected')
|
||||||
|
return paintSelected(ctx, entry, placement, alpha);
|
||||||
if (entry.variant === 'thumbnail') {
|
if (entry.variant === 'thumbnail') {
|
||||||
return paintThumbnail(ctx, entry, placement, alpha, leaderProgress, contentAlpha);
|
return paintThumbnail(
|
||||||
|
ctx,
|
||||||
|
entry,
|
||||||
|
placement,
|
||||||
|
alpha,
|
||||||
|
leaderProgress,
|
||||||
|
contentAlpha,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (entry.variant === 'card') {
|
if (entry.variant === 'card') {
|
||||||
return paintCard(ctx, entry, placement, alpha, leaderProgress, contentAlpha);
|
return paintCard(
|
||||||
|
ctx,
|
||||||
|
entry,
|
||||||
|
placement,
|
||||||
|
alpha,
|
||||||
|
leaderProgress,
|
||||||
|
contentAlpha,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (entry.variant === 'track') return paintTrack(ctx, entry, placement, alpha);
|
if (entry.variant === 'track')
|
||||||
|
return paintTrack(ctx, entry, placement, alpha);
|
||||||
return paintLabel(ctx, entry, placement, alpha);
|
return paintLabel(ctx, entry, placement, alpha);
|
||||||
}
|
}
|
||||||
+701
-287
File diff suppressed because it is too large.
Load diff
+109
-83
@@ -13,7 +13,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 10145,
|
alt: 10145,
|
||||||
heading: 224,
|
heading: 224,
|
||||||
pitch: -38,
|
pitch: -38,
|
||||||
hold: 11
|
hold: 11,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dhunche',
|
id: 'dhunche',
|
||||||
@@ -23,7 +23,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 6212,
|
alt: 6212,
|
||||||
heading: 208,
|
heading: 208,
|
||||||
pitch: -39,
|
pitch: -39,
|
||||||
hold: 16
|
hold: 16,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mailung-upper-trishuli',
|
id: 'mailung-upper-trishuli',
|
||||||
@@ -33,7 +33,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 6353,
|
alt: 6353,
|
||||||
heading: 211,
|
heading: 211,
|
||||||
pitch: -40,
|
pitch: -40,
|
||||||
hold: 13
|
hold: 13,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mailung-bazzar',
|
id: 'mailung-bazzar',
|
||||||
@@ -43,7 +43,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 6331,
|
alt: 6331,
|
||||||
heading: 212,
|
heading: 212,
|
||||||
pitch: -40,
|
pitch: -40,
|
||||||
hold: 15
|
hold: 15,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dandagaun',
|
id: 'dandagaun',
|
||||||
@@ -53,7 +53,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 6311,
|
alt: 6311,
|
||||||
heading: 214,
|
heading: 214,
|
||||||
pitch: -40,
|
pitch: -40,
|
||||||
hold: 15
|
hold: 15,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dandagaun-viewpoint',
|
id: 'dandagaun-viewpoint',
|
||||||
@@ -63,7 +63,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 6740,
|
alt: 6740,
|
||||||
heading: 215,
|
heading: 215,
|
||||||
pitch: -41,
|
pitch: -41,
|
||||||
hold: 13
|
hold: 13,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'betrawati-bazaar',
|
id: 'betrawati-bazaar',
|
||||||
@@ -73,7 +73,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 7034,
|
alt: 7034,
|
||||||
heading: 216,
|
heading: 216,
|
||||||
pitch: -41,
|
pitch: -41,
|
||||||
hold: 12
|
hold: 12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bhainse',
|
id: 'bhainse',
|
||||||
@@ -83,7 +83,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 8051,
|
alt: 8051,
|
||||||
heading: 219,
|
heading: 219,
|
||||||
pitch: -41,
|
pitch: -41,
|
||||||
hold: 13
|
hold: 13,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'devighat-taadi-khola-bridge',
|
id: 'devighat-taadi-khola-bridge',
|
||||||
@@ -93,7 +93,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 9056,
|
alt: 9056,
|
||||||
heading: 224,
|
heading: 224,
|
||||||
pitch: -42,
|
pitch: -42,
|
||||||
hold: 13
|
hold: 13,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'charaudi',
|
id: 'charaudi',
|
||||||
@@ -103,7 +103,7 @@ const ADDITIONAL_SHOTS = [
|
|||||||
alt: 10347,
|
alt: 10347,
|
||||||
heading: 232,
|
heading: 232,
|
||||||
pitch: -42,
|
pitch: -42,
|
||||||
hold: 11
|
hold: 11,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'final-view',
|
id: 'final-view',
|
||||||
@@ -116,8 +116,8 @@ const ADDITIONAL_SHOTS = [
|
|||||||
roll: 174,
|
roll: 174,
|
||||||
duration: 4,
|
duration: 4,
|
||||||
hold: 11,
|
hold: 11,
|
||||||
locatorPresentation: 'bhote-koshi-incident-places'
|
locatorPresentation: 'bhote-koshi-incident-places',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
const EVIDENCE_ORDER = [
|
const EVIDENCE_ORDER = [
|
||||||
'immediate-collapse-viewpoint',
|
'immediate-collapse-viewpoint',
|
||||||
@@ -136,7 +136,7 @@ const EVIDENCE_ORDER = [
|
|||||||
'bidur-trishuli-bridge',
|
'bidur-trishuli-bridge',
|
||||||
'devighat-taadi-khola-bridge',
|
'devighat-taadi-khola-bridge',
|
||||||
'charaudi',
|
'charaudi',
|
||||||
'final-view'
|
'final-view',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Per-reach motion timing keeps the schematic front ahead of the fixed 4.2s
|
// Per-reach motion timing keeps the schematic front ahead of the fixed 4.2s
|
||||||
@@ -147,60 +147,66 @@ const PATH_DURATION_BY_BEAT = Object.freeze({
|
|||||||
'second-landslide': 6.5,
|
'second-landslide': 6.5,
|
||||||
'timure-cluster': 2.1,
|
'timure-cluster': 2.1,
|
||||||
'syabru-besi': 2.8,
|
'syabru-besi': 2.8,
|
||||||
'dhunche': 2.5,
|
dhunche: 2.5,
|
||||||
'mailung-upper-trishuli': 2.9,
|
'mailung-upper-trishuli': 2.9,
|
||||||
'mailung-bazzar': 2.1,
|
'mailung-bazzar': 2.1,
|
||||||
'dandagaun': 1.2,
|
dandagaun: 1.2,
|
||||||
'dandagaun-viewpoint': 2.5,
|
'dandagaun-viewpoint': 2.5,
|
||||||
'betrawati-bazaar': 2.9,
|
'betrawati-bazaar': 2.9,
|
||||||
'bhainse': 1.5,
|
bhainse: 1.5,
|
||||||
'bidur-trishuli-bridge': 2.6,
|
'bidur-trishuli-bridge': 2.6,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Extend the six-beat Nepal pack without replacing existing authored shots. */
|
/** Extend the six-beat Nepal pack without replacing existing authored shots. */
|
||||||
export function expandNepalEvidencePack(base) {
|
export function expandNepalEvidencePack(base) {
|
||||||
const layerId = base.requiredSourcePackLayerId;
|
const layerId = base.requiredSourcePackLayerId;
|
||||||
const extraShots = ADDITIONAL_SHOTS.map(({
|
const extraShots = ADDITIONAL_SHOTS.map(
|
||||||
id,
|
({ id, hold, duration = 4.2, locatorPresentation, ...camera }) => ({
|
||||||
hold,
|
...camera,
|
||||||
duration = 4.2,
|
roll: Number.isFinite(Number(camera.roll)) ? Number(camera.roll) : 0,
|
||||||
locatorPresentation,
|
duration,
|
||||||
...camera
|
hold,
|
||||||
}) => ({
|
mapStack: 'photoreal',
|
||||||
...camera,
|
layers: {
|
||||||
roll: Number.isFinite(Number(camera.roll)) ? Number(camera.roll) : 0,
|
[layerId]: {
|
||||||
duration,
|
|
||||||
hold,
|
|
||||||
mapStack: 'photoreal',
|
|
||||||
layers: {
|
|
||||||
[layerId]: { enabled: true, params: {
|
|
||||||
presentation: 'scene-beat',
|
|
||||||
beatId: id,
|
|
||||||
beatReveal: 0.36,
|
|
||||||
split: 0.5,
|
|
||||||
} },
|
|
||||||
...(locatorPresentation ? {
|
|
||||||
'bhote-koshi-locator': {
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
params: { presentation: locatorPresentation },
|
params: {
|
||||||
|
presentation: 'scene-beat',
|
||||||
|
beatId: id,
|
||||||
|
beatReveal: 0.36,
|
||||||
|
split: 0.5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} : {}),
|
...(locatorPresentation
|
||||||
},
|
? {
|
||||||
}));
|
'bhote-koshi-locator': {
|
||||||
const byBeat = new Map([...base.cameraPath, ...extraShots].map((shot) => [
|
enabled: true,
|
||||||
shot.layers[layerId].params.beatId, shot,
|
params: { presentation: locatorPresentation },
|
||||||
]));
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const byBeat = new Map(
|
||||||
|
[...base.cameraPath, ...extraShots].map((shot) => [
|
||||||
|
shot.layers[layerId].params.beatId,
|
||||||
|
shot,
|
||||||
|
]),
|
||||||
|
);
|
||||||
const cameraPath = EVIDENCE_ORDER.map((id) => byBeat.get(id));
|
const cameraPath = EVIDENCE_ORDER.map((id) => byBeat.get(id));
|
||||||
const runtimeControlsByBeat = { ...base.runtimeControlsByBeat };
|
const runtimeControlsByBeat = { ...base.runtimeControlsByBeat };
|
||||||
const cameraLedPathBeats = new Set([
|
const cameraLedPathBeats = new Set([
|
||||||
'timure-cluster', 'syabru-besi', ...SOURCE_PATH_BEAT_IDS.slice(2),
|
'timure-cluster',
|
||||||
]);
|
'syabru-besi',
|
||||||
const mediaLedPathBeats = new Set([
|
...SOURCE_PATH_BEAT_IDS.slice(2),
|
||||||
'debris-dammed-lake', 'second-landslide',
|
|
||||||
]);
|
]);
|
||||||
|
const mediaLedPathBeats = new Set(['debris-dammed-lake', 'second-landslide']);
|
||||||
const finalSourcedPathBeatId = SOURCE_PATH_BEAT_IDS.at(-1);
|
const finalSourcedPathBeatId = SOURCE_PATH_BEAT_IDS.at(-1);
|
||||||
const postSourceHistoryBeats = new Set([
|
const postSourceHistoryBeats = new Set([
|
||||||
'devighat-taadi-khola-bridge', 'charaudi', 'final-view',
|
'devighat-taadi-khola-bridge',
|
||||||
|
'charaudi',
|
||||||
|
'final-view',
|
||||||
]);
|
]);
|
||||||
for (const shot of cameraPath) {
|
for (const shot of cameraPath) {
|
||||||
const id = shot.layers[layerId].params.beatId;
|
const id = shot.layers[layerId].params.beatId;
|
||||||
@@ -209,33 +215,43 @@ export function expandNepalEvidencePack(base) {
|
|||||||
runtimeControlsByBeat[id] = {
|
runtimeControlsByBeat[id] = {
|
||||||
...runtimeControlsByBeat[id],
|
...runtimeControlsByBeat[id],
|
||||||
evidenceMediaAutoplay: !isFinalView,
|
evidenceMediaAutoplay: !isFinalView,
|
||||||
...(isFinalView ? {} : {
|
...(isFinalView
|
||||||
mediaPlaybackHoldSec: id === 'mailung-bazzar' ? 7 : 6,
|
? {}
|
||||||
mediaExitDurationSec: 0.65,
|
: {
|
||||||
}),
|
mediaPlaybackHoldSec: id === 'mailung-bazzar' ? 7 : 6,
|
||||||
|
mediaExitDurationSec: 0.65,
|
||||||
|
}),
|
||||||
deferEvidenceUntilCameraSettled: !isFinalView,
|
deferEvidenceUntilCameraSettled: !isFinalView,
|
||||||
evidenceRevealDurationSec: 0.9,
|
evidenceRevealDurationSec: 0.9,
|
||||||
evidencePath: SOURCE_PATH_BEAT_IDS.includes(id) ? 'source'
|
evidencePath: SOURCE_PATH_BEAT_IDS.includes(id)
|
||||||
: ['gyirong-border-gate', 'timure-cluster', 'syabru-besi'].includes(id) ? 'comparison' : 'none',
|
? 'source'
|
||||||
...(cameraLedPathBeats.has(id) ? {
|
: ['gyirong-border-gate', 'timure-cluster', 'syabru-besi'].includes(id)
|
||||||
evidencePathDuringCamera: true,
|
? 'comparison'
|
||||||
evidencePathPersistent: true,
|
: 'none',
|
||||||
...(authoredPathDurationSec
|
...(cameraLedPathBeats.has(id)
|
||||||
? { evidencePathTravelDurationSec: authoredPathDurationSec }
|
? {
|
||||||
: {}),
|
evidencePathDuringCamera: true,
|
||||||
} : {}),
|
evidencePathPersistent: true,
|
||||||
...(mediaLedPathBeats.has(id) ? {
|
...(authoredPathDurationSec
|
||||||
evidencePathDuringMedia: true,
|
? { evidencePathTravelDurationSec: authoredPathDurationSec }
|
||||||
evidencePathPersistent: true,
|
: {}),
|
||||||
evidencePathStartDelaySec: 0,
|
}
|
||||||
} : {}),
|
: {}),
|
||||||
|
...(mediaLedPathBeats.has(id)
|
||||||
|
? {
|
||||||
|
evidencePathDuringMedia: true,
|
||||||
|
evidencePathPersistent: true,
|
||||||
|
evidencePathStartDelaySec: 0,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
if (SOURCE_PATH_BEAT_IDS.includes(id)) {
|
if (SOURCE_PATH_BEAT_IDS.includes(id)) {
|
||||||
runtimeControlsByBeat[id] = {
|
runtimeControlsByBeat[id] = {
|
||||||
...runtimeControlsByBeat[id],
|
...runtimeControlsByBeat[id],
|
||||||
evidenceHoldCard: true,
|
evidenceHoldCard: true,
|
||||||
evidencePathHistory: true,
|
evidencePathHistory: true,
|
||||||
evidencePathDurationSec: authoredPathDurationSec || Math.max(4, shot.hold),
|
evidencePathDurationSec:
|
||||||
|
authoredPathDurationSec || Math.max(4, shot.hold),
|
||||||
minimumHoldSec: Math.max(4, shot.hold),
|
minimumHoldSec: Math.max(4, shot.hold),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -256,33 +272,40 @@ export function expandNepalEvidencePack(base) {
|
|||||||
evidencePathElevation: 'source',
|
evidencePathElevation: 'source',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const pathDurationSec = Number(runtimeControlsByBeat[id].evidencePathDurationSec)
|
const pathDurationSec =
|
||||||
|| (!cameraLedPathBeats.has(id) && runtimeControlsByBeat[id].evidenceSequence === true
|
Number(runtimeControlsByBeat[id].evidencePathDurationSec) ||
|
||||||
|
(!cameraLedPathBeats.has(id) &&
|
||||||
|
runtimeControlsByBeat[id].evidenceSequence === true
|
||||||
? Number(runtimeControlsByBeat[id].evidenceSequenceDurationSec) || 0
|
? Number(runtimeControlsByBeat[id].evidenceSequenceDurationSec) || 0
|
||||||
: 0);
|
: 0);
|
||||||
const mediaPresentationSec = isFinalView
|
const mediaPresentationSec = isFinalView
|
||||||
? 0
|
? 0
|
||||||
: (id === 'mailung-bazzar' ? 7 : 6) + 0.65;
|
: (id === 'mailung-bazzar' ? 7 : 6) + 0.65;
|
||||||
const mediaAndPathHoldSec = pathDurationSec > 0
|
const mediaAndPathHoldSec =
|
||||||
&& !cameraLedPathBeats.has(id)
|
pathDurationSec > 0 && !cameraLedPathBeats.has(id)
|
||||||
? mediaLedPathBeats.has(id)
|
? mediaLedPathBeats.has(id)
|
||||||
? Math.max(mediaPresentationSec, pathDurationSec)
|
? Math.max(mediaPresentationSec, pathDurationSec)
|
||||||
: mediaPresentationSec + pathDurationSec
|
: mediaPresentationSec + pathDurationSec
|
||||||
: mediaPresentationSec;
|
: mediaPresentationSec;
|
||||||
runtimeControlsByBeat[id] = {
|
runtimeControlsByBeat[id] = {
|
||||||
...runtimeControlsByBeat[id],
|
...runtimeControlsByBeat[id],
|
||||||
minimumHoldSec: id === 'mailung-bazzar' ? mediaAndPathHoldSec : Math.max(
|
minimumHoldSec:
|
||||||
Number(runtimeControlsByBeat[id].minimumHoldSec) || 0,
|
id === 'mailung-bazzar'
|
||||||
mediaAndPathHoldSec,
|
? mediaAndPathHoldSec
|
||||||
),
|
: Math.max(
|
||||||
|
Number(runtimeControlsByBeat[id].minimumHoldSec) || 0,
|
||||||
|
mediaAndPathHoldSec,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Keep the downstream sequence on its sourced river profile. Coarse Google
|
// Keep the downstream sequence on its sourced river profile. Coarse Google
|
||||||
// mesh samples can contain cliff-height spikes that reverse apparent motion.
|
// mesh samples can contain cliff-height spikes that reverse apparent motion.
|
||||||
// Preserve each authored readable hold after the media-first timing floor.
|
// Preserve each authored readable hold after the media-first timing floor.
|
||||||
for (const [id, seconds, sequenceSeconds = seconds] of [
|
for (const [id, seconds, sequenceSeconds = seconds] of [
|
||||||
['immediate-collapse-viewpoint', 10], ['gyirong-border-gate', 8, 2.5],
|
['immediate-collapse-viewpoint', 10],
|
||||||
['timure-cluster', 12], ['syabru-besi', 11],
|
['gyirong-border-gate', 8, 2.5],
|
||||||
|
['timure-cluster', 12],
|
||||||
|
['syabru-besi', 11],
|
||||||
]) {
|
]) {
|
||||||
runtimeControlsByBeat[id] = {
|
runtimeControlsByBeat[id] = {
|
||||||
...runtimeControlsByBeat[id],
|
...runtimeControlsByBeat[id],
|
||||||
@@ -296,7 +319,10 @@ export function expandNepalEvidencePack(base) {
|
|||||||
}
|
}
|
||||||
const finalView = byBeat.get('final-view');
|
const finalView = byBeat.get('final-view');
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
...base, version: 18, cameraPath, runtimeControlsByBeat,
|
...base,
|
||||||
|
version: 18,
|
||||||
|
cameraPath,
|
||||||
|
runtimeControlsByBeat,
|
||||||
// Clip trims must also replace older saved holds, not just lower a floor.
|
// Clip trims must also replace older saved holds, not just lower a floor.
|
||||||
runtimeHoldSecByBeat: { 'mailung-bazzar': 7 + 0.65 },
|
runtimeHoldSecByBeat: { 'mailung-bazzar': 7 + 0.65 },
|
||||||
expansionFromVersion: 17,
|
expansionFromVersion: 17,
|
||||||
|
|||||||
+492
-492
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user