mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Carry the Nepal event images, playback and media across globes
This commit is contained in:
1 parent
811bcaff85
commit
4e09559337
19 files changed
+426
-84
No files matched your search
@@ -3680,6 +3680,7 @@
|
|||||||
"src/layers/submarineCables/render/navara.js",
|
"src/layers/submarineCables/render/navara.js",
|
||||||
"src/layers/directions/render/navara.js",
|
"src/layers/directions/render/navara.js",
|
||||||
"src/layers/shared/render/navaraStory.js",
|
"src/layers/shared/render/navaraStory.js",
|
||||||
|
"src/layers/bhoteKoshi/render/navara.js",
|
||||||
"src/layers/weather/render/navara.js",
|
"src/layers/weather/render/navara.js",
|
||||||
"src/layers/weather/render/navaraSurface.js",
|
"src/layers/weather/render/navaraSurface.js",
|
||||||
"src/layers/wind/render/navara.js",
|
"src/layers/wind/render/navara.js",
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||||
|
|
||||||
|
const base = new URL(process.argv[2]);
|
||||||
|
if (
|
||||||
|
!['localhost', '127.0.0.1'].includes(base.hostname) ||
|
||||||
|
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||||
|
base.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw new Error('Supply an unreserved local keyless preview URL');
|
||||||
|
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||||
|
base.hash =
|
||||||
|
'v=2&lat=28.2110&lon=85.3380&alt=9000&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
|
||||||
|
const b = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--use-angle=metal', '--enable-gpu'],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const p = await b.newPage();
|
||||||
|
await blockPaidQaMaps(p);
|
||||||
|
await p.setViewport({ width: 1024, height: 768 });
|
||||||
|
const errors = [];
|
||||||
|
p.on('pageerror', (e) => {
|
||||||
|
errors.push(e.message);
|
||||||
|
console.log('error', e.message.split('?')[0]);
|
||||||
|
});
|
||||||
|
p.on('console', (msg) => {
|
||||||
|
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
|
||||||
|
errors.push(msg.text());
|
||||||
|
});
|
||||||
|
const assets = [];
|
||||||
|
p.on('request', (req) => {
|
||||||
|
const path = new URL(req.url()).pathname;
|
||||||
|
if (
|
||||||
|
/\/events\/bhote-koshi-2026\/(event\.json|pre\.webp|post\.webp)$/.test(
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assets.push(path);
|
||||||
|
});
|
||||||
|
await p.goto(base.href);
|
||||||
|
const ready = async (kind) => {
|
||||||
|
await p.waitForFunction(
|
||||||
|
(k) =>
|
||||||
|
window.__godsEyeView?.engine.kind === k &&
|
||||||
|
!document.querySelector('#globe-engine')?.disabled &&
|
||||||
|
!document.querySelector('.engine-switch-frame') &&
|
||||||
|
document.querySelector('#loading-screen.hidden'),
|
||||||
|
{ timeout: 90000 },
|
||||||
|
kind,
|
||||||
|
);
|
||||||
|
await p.evaluate(
|
||||||
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await ready('navara');
|
||||||
|
await p.evaluate(async () => {
|
||||||
|
const { tileset, dataManager, sceneDirector } = window.__godsEyeView;
|
||||||
|
if (!sceneDirector?.listScenes().length) throw Error('No scenes available on Navara');
|
||||||
|
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
|
||||||
|
throw Error('Keyless preview required');
|
||||||
|
const mod = dataManager.layers.get('bhote-koshi-2026').module;
|
||||||
|
window.qaEvent = mod;
|
||||||
|
await dataManager.setEnabled(mod.id, true, { origin: 'user' });
|
||||||
|
mod.setParams({
|
||||||
|
presentation: 'standalone',
|
||||||
|
progress: 0.52,
|
||||||
|
split: 0.27,
|
||||||
|
autoPlay: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const sample = async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
return sharp(await p.screenshot())
|
||||||
|
.extract({ left: 490, top: 370, width: 25, height: 25 })
|
||||||
|
.removeAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer();
|
||||||
|
};
|
||||||
|
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||||
|
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||||
|
await p.select('#globe-engine', kind);
|
||||||
|
await ready(kind);
|
||||||
|
}
|
||||||
|
const state = await p.evaluate(() => {
|
||||||
|
const { dataManager } = window.__godsEyeView;
|
||||||
|
return {
|
||||||
|
same:
|
||||||
|
window.qaEvent === dataManager.layers.get('bhote-koshi-2026').module,
|
||||||
|
state: window.qaEvent.getPlaybackState(),
|
||||||
|
player:
|
||||||
|
document.querySelector('#bhote-koshi-embedded-media-root') ===
|
||||||
|
window.qaPlayer,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert.equal(state.same, true);
|
||||||
|
assert.equal(state.state.enabled, true);
|
||||||
|
assert.equal(state.state.split, 0.27);
|
||||||
|
assert.equal(state.state.playing, false);
|
||||||
|
assert.equal(state.state.progress, 0.52);
|
||||||
|
await p.evaluate(() => {
|
||||||
|
window.qaPlayer ??= document.querySelector(
|
||||||
|
'#bhote-koshi-embedded-media-root',
|
||||||
|
);
|
||||||
|
window.qaEvent.setParams({ split: 0.75 });
|
||||||
|
});
|
||||||
|
assert.ok(await p.evaluate(()=>Boolean(window.qaPlayer)), 'persistent embedded media root');
|
||||||
|
const before = await sample();
|
||||||
|
await p.evaluate(() => window.qaEvent.setParams({ split: 0.27 }));
|
||||||
|
const after = await sample();
|
||||||
|
const diff =
|
||||||
|
before.reduce((n, v, i) => n + Math.abs(v - after[i]), 0) / before.length;
|
||||||
|
console.log(kind, state.state, 'imagery difference', diff);
|
||||||
|
assert.ok(diff > 6, 'divider changes the actual imagery in the viewport');
|
||||||
|
assert.equal(
|
||||||
|
await p.evaluate(
|
||||||
|
() =>
|
||||||
|
document.querySelector('#bhote-koshi-embedded-media-root') ===
|
||||||
|
window.qaPlayer,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const suffix of ['/event.json', '/pre.webp', '/post.webp'])
|
||||||
|
assert.equal(
|
||||||
|
assets.filter((a) => a.endsWith(suffix)).length,
|
||||||
|
1,
|
||||||
|
JSON.stringify(assets),
|
||||||
|
);
|
||||||
|
await p.evaluate(() => window.qaEvent.setParams({ autoPlay: true }));
|
||||||
|
await p.waitForFunction(
|
||||||
|
() => window.qaEvent.getPlaybackState().progress > 0.53,
|
||||||
|
);
|
||||||
|
await p.select('#globe-engine', 'cesium');
|
||||||
|
await ready('cesium');
|
||||||
|
const live = await p.evaluate(() => window.qaEvent.getPlaybackState());
|
||||||
|
assert.ok(live.playing && live.progress > 0.53);
|
||||||
|
await p.evaluate(() =>
|
||||||
|
window.__godsEyeView.dataManager.setEnabled('bhote-koshi-2026', false, {
|
||||||
|
origin: 'user',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(
|
||||||
|
'PASS event timeline, paused and playing handoff, image pair, divider, player host and single asset acquisition',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await b.close();
|
||||||
|
}
|
||||||
@@ -160,7 +160,9 @@ export function createApplicationCatalog({
|
|||||||
// Catalog order: [layer id(s), construction]. Only the layers the
|
// Catalog order: [layer id(s), construction]. Only the layers the
|
||||||
// engine can show are constructed (renderers.js).
|
// engine can show are constructed (renderers.js).
|
||||||
const entries = [
|
const entries = [
|
||||||
['bhote-koshi-2026', () => createBhoteKoshiEventLayer()],
|
['bhote-koshi-2026', () => sessionModel('bhote-koshi-2026', () => createBhoteKoshiEventLayer({
|
||||||
|
createNativeRenderer: (engine) => rendererFor('bhote-koshi-2026', engine),
|
||||||
|
}))],
|
||||||
[
|
[
|
||||||
'bhote-koshi-locator',
|
'bhote-koshi-locator',
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
+2
-2
@@ -45,10 +45,10 @@ export function createApplicationData({
|
|||||||
onData?.(dataManager);
|
onData?.(dataManager);
|
||||||
if (!catalog?.layers || !catalog?.metadata)
|
if (!catalog?.layers || !catalog?.metadata)
|
||||||
throw new TypeError('An application layer catalog is required');
|
throw new TypeError('An application layer catalog is required');
|
||||||
const attached = dataManager.bindCatalog(viewer, engine, catalog);
|
|
||||||
for (const layer of catalog.layers) layer.attachDataManager?.(dataManager);
|
|
||||||
for (const layer of catalog.layers)
|
for (const layer of catalog.layers)
|
||||||
layer.attachMapStackController?.(mapStackController);
|
layer.attachMapStackController?.(mapStackController);
|
||||||
|
const attached = dataManager.bindCatalog(viewer, engine, catalog);
|
||||||
|
for (const layer of catalog.layers) layer.attachDataManager?.(dataManager);
|
||||||
|
|
||||||
if (allowQaRegistration) {
|
if (allowQaRegistration) {
|
||||||
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ test('the Navara mode registers migrated renderers once', async () => {
|
|||||||
.available()
|
.available()
|
||||||
.map(({ id }) => id),
|
.map(({ id }) => id),
|
||||||
[
|
[
|
||||||
|
'bhote-koshi-2026',
|
||||||
'bhote-koshi-locator',
|
'bhote-koshi-locator',
|
||||||
'flights',
|
'flights',
|
||||||
'military',
|
'military',
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ export function navaraModeNotice(availability) {
|
|||||||
const names = availability.available().map(({ name }) => name);
|
const names = availability.available().map(({ name }) => name);
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
label: t('Experimental Navara engine'),
|
label: t('Experimental Navara engine'),
|
||||||
detail: names.length
|
detail: names.length ? `Data layers: ${names.join(', ')}` : 'No registered data layers',
|
||||||
? `Data layers: ${names.join(', ')}; the other layers and director are unavailable`
|
|
||||||
: 'No data layers on this engine yet; cockpit and director are unavailable',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +118,10 @@ export async function registerNavaraLayerRenderers() {
|
|||||||
return (engine, options) => createWeatherNavaraRenderer(engine, navaraViews.get(engine), options);
|
return (engine, options) => createWeatherNavaraRenderer(engine, navaraViews.get(engine), options);
|
||||||
},
|
},
|
||||||
])),
|
])),
|
||||||
|
'bhote-koshi-2026': async () => {
|
||||||
|
const { createNepalEventDisplay } = await import('../layers/bhoteKoshi/render/navara.js');
|
||||||
|
return (engine) => createNepalEventDisplay(engine, navaraViews.get(engine));
|
||||||
|
},
|
||||||
'bhote-koshi-locator': async () => {
|
'bhote-koshi-locator': async () => {
|
||||||
const { drawStorySource } = await import('../layers/shared/render/navaraStory.js');
|
const { drawStorySource } = await import('../layers/shared/render/navaraStory.js');
|
||||||
return (engine, { data }) => drawStorySource(engine, navaraViews.get(engine), data);
|
return (engine, { data }) => drawStorySource(engine, navaraViews.get(engine), data);
|
||||||
@@ -331,6 +333,7 @@ export function createViewerlessTools({
|
|||||||
placeSearch,
|
placeSearch,
|
||||||
voice = {},
|
voice = {},
|
||||||
startChrome,
|
startChrome,
|
||||||
|
sceneDirector,
|
||||||
signal,
|
signal,
|
||||||
defer,
|
defer,
|
||||||
}) {
|
}) {
|
||||||
@@ -402,7 +405,7 @@ export function createViewerlessTools({
|
|||||||
styleManager,
|
styleManager,
|
||||||
tileset: null,
|
tileset: null,
|
||||||
dataManager,
|
dataManager,
|
||||||
sceneDirector: null,
|
sceneDirector,
|
||||||
mapStackController,
|
mapStackController,
|
||||||
annotations: null,
|
annotations: null,
|
||||||
weatherEffects: null,
|
weatherEffects: null,
|
||||||
@@ -428,7 +431,7 @@ export function createViewerlessTools({
|
|||||||
engine,
|
engine,
|
||||||
styleManager,
|
styleManager,
|
||||||
dataManager,
|
dataManager,
|
||||||
sceneDirector: null,
|
sceneDirector,
|
||||||
annotations: null,
|
annotations: null,
|
||||||
});
|
});
|
||||||
defer(() => {
|
defer(() => {
|
||||||
@@ -437,5 +440,5 @@ export function createViewerlessTools({
|
|||||||
delete window.__gevVoiceCommands;
|
delete window.__gevVoiceCommands;
|
||||||
});
|
});
|
||||||
debug.voiceCommands = voiceCommands;
|
debug.voiceCommands = voiceCommands;
|
||||||
return { sceneDirector: null, annotations: null, voiceCommands };
|
return { sceneDirector, annotations: null, voiceCommands };
|
||||||
}
|
}
|
||||||
+17
-16
@@ -41,22 +41,6 @@ export function createApplicationTools({
|
|||||||
const { viewer, engine, tileset, mapStackController, operations } = scene;
|
const { viewer, engine, tileset, mapStackController, operations } = scene;
|
||||||
const { styleManager, weatherEffects, cockpitCloudEffects } = controls;
|
const { styleManager, weatherEffects, cockpitCloudEffects } = controls;
|
||||||
const { dataManager } = data;
|
const { dataManager } = data;
|
||||||
// Without a viewer (the Navara mode) the tools load with that mode's code.
|
|
||||||
if (!viewer) {
|
|
||||||
return import('./navaraMode.js').then(({ createViewerlessTools }) =>
|
|
||||||
createViewerlessTools({
|
|
||||||
scene,
|
|
||||||
controls,
|
|
||||||
data,
|
|
||||||
loadingScreen,
|
|
||||||
placeSearch,
|
|
||||||
voice,
|
|
||||||
startChrome,
|
|
||||||
signal,
|
|
||||||
defer,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const sceneDirector = new SceneDirector(viewer, styleManager, dataManager, {
|
const sceneDirector = new SceneDirector(viewer, styleManager, dataManager, {
|
||||||
engine,
|
engine,
|
||||||
dataPacks: sceneDataPacks,
|
dataPacks: sceneDataPacks,
|
||||||
@@ -68,6 +52,23 @@ export function createApplicationTools({
|
|||||||
?.module.attachSceneController(sceneDirector);
|
?.module.attachSceneController(sceneDirector);
|
||||||
defer(() => sceneDirector.destroy());
|
defer(() => sceneDirector.destroy());
|
||||||
onSceneDirector?.(sceneDirector);
|
onSceneDirector?.(sceneDirector);
|
||||||
|
// Without a viewer (the Navara mode) the tools load with that mode's code.
|
||||||
|
if (!viewer) {
|
||||||
|
return import('./navaraMode.js').then(({ createViewerlessTools }) =>
|
||||||
|
createViewerlessTools({
|
||||||
|
scene,
|
||||||
|
controls,
|
||||||
|
data,
|
||||||
|
loadingScreen,
|
||||||
|
placeSearch,
|
||||||
|
voice,
|
||||||
|
sceneDirector,
|
||||||
|
startChrome,
|
||||||
|
signal,
|
||||||
|
defer,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
const annotations = initAnnotations({
|
const annotations = initAnnotations({
|
||||||
viewer,
|
viewer,
|
||||||
tileset,
|
tileset,
|
||||||
|
|||||||
@@ -17,11 +17,15 @@ import {
|
|||||||
// The tools without a viewer (the experimental Navara engine); they load
|
// The tools without a viewer (the experimental Navara engine); they load
|
||||||
// from src/app/navaraMode.js.
|
// from src/app/navaraMode.js.
|
||||||
test('without a viewer the tools run the governor on the engine, show the notice, create no viewer tools and run voice on the engine', async (t) => {
|
test('without a viewer the tools run the governor on the engine, show the notice, create no viewer tools and run voice on the engine', async (t) => {
|
||||||
const saved = { window: globalThis.window, document: globalThis.document };
|
const saved = { window: globalThis.window, document: globalThis.document, localStorage:globalThis.localStorage };
|
||||||
const listeners = new Map();
|
const listeners = new Map();
|
||||||
globalThis.window = {};
|
globalThis.window = {};
|
||||||
|
globalThis.localStorage = { getItem:()=>null,setItem(){},removeItem(){} };
|
||||||
globalThis.document = {
|
globalThis.document = {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
getElementById: () => null,
|
||||||
|
querySelector: () => null,
|
||||||
|
body: {classList:{add(){},remove(){},toggle(){},contains(){return false;}},appendChild(){}},
|
||||||
addEventListener: (type, fn) => listeners.set(type, fn),
|
addEventListener: (type, fn) => listeners.set(type, fn),
|
||||||
removeEventListener: (type, fn) => {
|
removeEventListener: (type, fn) => {
|
||||||
if (listeners.get(type) === fn) listeners.delete(type);
|
if (listeners.get(type) === fn) listeners.delete(type);
|
||||||
@@ -84,7 +88,7 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
|||||||
signal: new AbortController().signal,
|
signal: new AbortController().signal,
|
||||||
defer: (dispose) => cleanups.push(dispose),
|
defer: (dispose) => cleanups.push(dispose),
|
||||||
});
|
});
|
||||||
assert.equal(tools.sceneDirector, null);
|
assert.ok(tools.sceneDirector);
|
||||||
assert.equal(tools.annotations, null);
|
assert.equal(tools.annotations, null);
|
||||||
assert.ok(tools.voiceCommands, 'voice runs on the engine');
|
assert.ok(tools.voiceCommands, 'voice runs on the engine');
|
||||||
assert.equal(window.__gevVoiceCommands, tools.voiceCommands);
|
assert.equal(window.__gevVoiceCommands, tools.voiceCommands);
|
||||||
@@ -94,8 +98,7 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
|||||||
const scene = await voice.runner('control_scene', {
|
const scene = await voice.runner('control_scene', {
|
||||||
action: 'list',
|
action: 'list',
|
||||||
});
|
});
|
||||||
assert.equal(scene.ok, false);
|
assert.equal(scene.ok, true);
|
||||||
assert.equal(scene.code, 'UNAVAILABLE_ON_ENGINE');
|
|
||||||
assert.equal(chrome.length, 1);
|
assert.equal(chrome.length, 1);
|
||||||
assert.equal(chrome[0].initializeWelcome, null, 'no first-run welcome');
|
assert.equal(chrome[0].initializeWelcome, null, 'no first-run welcome');
|
||||||
assert.equal(chrome[0].styleManager, styleManager);
|
assert.equal(chrome[0].styleManager, styleManager);
|
||||||
@@ -112,14 +115,14 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
|||||||
]);
|
]);
|
||||||
assert.match(
|
assert.match(
|
||||||
`${notice.label}: ${notice.detail}`,
|
`${notice.label}: ${notice.detail}`,
|
||||||
/^Experimental Navara engine: no data layers on this engine yet; cockpit and director are unavailable$/i,
|
/^Experimental Navara engine: no registered data layers$/i,
|
||||||
);
|
);
|
||||||
// With Navara renderers, the notice names the layers it shows.
|
// With Navara renderers, the notice names the layers it shows.
|
||||||
const registry = createRendererRegistry();
|
const registry = createRendererRegistry();
|
||||||
registry.register('navara', { satellites: () => ({}), flights: () => ({}) });
|
registry.register('navara', { satellites: () => ({}), flights: () => ({}) });
|
||||||
assert.equal(
|
assert.equal(
|
||||||
navaraModeNotice(createLayerAvailability('navara', { registry })).detail,
|
navaraModeNotice(createLayerAvailability('navara', { registry })).detail,
|
||||||
'Data layers: Live Flights, Satellites; the other layers and director are unavailable',
|
'Data layers: Live Flights, Satellites',
|
||||||
);
|
);
|
||||||
// The governor drives the engine's continuous rendering.
|
// The governor drives the engine's continuous rendering.
|
||||||
assert.equal(isRenderGovernorInstalled(engine), true);
|
assert.equal(isRenderGovernorInstalled(engine), true);
|
||||||
@@ -130,7 +133,7 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
|||||||
const debug = window.__godsEyeView;
|
const debug = window.__godsEyeView;
|
||||||
assert.equal(debug.viewer, null);
|
assert.equal(debug.viewer, null);
|
||||||
assert.equal(debug.engine, engine);
|
assert.equal(debug.engine, engine);
|
||||||
assert.equal(debug.sceneDirector, null);
|
assert.equal(debug.sceneDirector, tools.sceneDirector);
|
||||||
assert.equal(debug.dataManager, dataManager);
|
assert.equal(debug.dataManager, dataManager);
|
||||||
assert.equal(debug.voiceCommands, tools.voiceCommands);
|
assert.equal(debug.voiceCommands, tools.voiceCommands);
|
||||||
listeners.get('visibilitychange')();
|
listeners.get('visibilitychange')();
|
||||||
|
|||||||
@@ -538,9 +538,10 @@ function ensureProviderConnections(documentRef, provider) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function anchorOnScreen(viewer, anchor, root) {
|
function anchorOnScreen(viewer, engine, anchor, root) {
|
||||||
if (!viewer?.scene || !anchor || !root) return null;
|
if ((!viewer?.scene && !engine) || !anchor || !root) return null;
|
||||||
const camera = viewer.camera;
|
const camera = viewer?.camera;
|
||||||
|
if (engine && !engine.projector.horizonVisible(anchor)) return null;
|
||||||
if (camera?.positionWC && camera?.directionWC) {
|
if (camera?.positionWC && camera?.directionWC) {
|
||||||
const delta = Cesium.Cartesian3.subtract(
|
const delta = Cesium.Cartesian3.subtract(
|
||||||
anchor,
|
anchor,
|
||||||
@@ -549,12 +550,12 @@ function anchorOnScreen(viewer, anchor, root) {
|
|||||||
);
|
);
|
||||||
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(
|
const pixel = engine ? engine.projector.worldToWindow(anchor, {}) : Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||||
viewer.scene,
|
viewer.scene,
|
||||||
anchor,
|
anchor,
|
||||||
);
|
);
|
||||||
if (!pixel) return null;
|
if (!pixel) return null;
|
||||||
const canvasBounds = viewer.canvas?.getBoundingClientRect?.();
|
const canvasBounds = (engine?.host.canvas ?? 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;
|
||||||
@@ -637,7 +638,7 @@ function createRoot(documentRef, viewer) {
|
|||||||
const root = documentRef.createElement('div');
|
const root = documentRef.createElement('div');
|
||||||
root.id = 'bhote-koshi-embedded-media-root';
|
root.id = 'bhote-koshi-embedded-media-root';
|
||||||
root.setAttribute('aria-live', 'polite');
|
root.setAttribute('aria-live', 'polite');
|
||||||
(viewer?.container || documentRef.body)?.append(root);
|
(documentRef.getElementById?.('cesiumContainer') || viewer?.container || documentRef.body)?.append(root);
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,6 +649,7 @@ function createRoot(documentRef, viewer) {
|
|||||||
*/
|
*/
|
||||||
export function createBhoteKoshiEmbeddedMedia({
|
export function createBhoteKoshiEmbeddedMedia({
|
||||||
viewer,
|
viewer,
|
||||||
|
engine = null,
|
||||||
documentRef = globalThis.document,
|
documentRef = globalThis.document,
|
||||||
globalRef = globalThis,
|
globalRef = globalThis,
|
||||||
xLoader = loadXWidgets,
|
xLoader = loadXWidgets,
|
||||||
@@ -660,7 +662,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
const pinokioShell = /(?:^|\s)Pinokio\/[^\s]+/i.test(
|
const pinokioShell = /(?:^|\s)Pinokio\/[^\s]+/i.test(
|
||||||
globalRef.navigator?.userAgent || '',
|
globalRef.navigator?.userAgent || '',
|
||||||
);
|
);
|
||||||
if (pinokioShell || !documentRef?.createElement || !viewer) {
|
if (pinokioShell || !documentRef?.createElement || (!viewer && !engine)) {
|
||||||
return {
|
return {
|
||||||
supportsPlayback: false,
|
supportsPlayback: false,
|
||||||
warm: () => false,
|
warm: () => false,
|
||||||
@@ -671,7 +673,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
destroy: () => {},
|
destroy: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const root = createRoot(documentRef, viewer);
|
const root = createRoot(documentRef, engine?.host ?? viewer);
|
||||||
let record = null;
|
let record = null;
|
||||||
let warmRecord = null;
|
let warmRecord = null;
|
||||||
let generation = 0;
|
let generation = 0;
|
||||||
@@ -848,7 +850,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
|
|
||||||
const position = () => {
|
const position = () => {
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
const projected = anchorOnScreen(viewer, record.anchor, root);
|
const projected = anchorOnScreen(viewer, engine, record.anchor, root);
|
||||||
record.card.hidden = !projected;
|
record.card.hidden = !projected;
|
||||||
if (projected) {
|
if (projected) {
|
||||||
placeCard(record.card, record.leader, projected);
|
placeCard(record.card, record.leader, projected);
|
||||||
@@ -864,8 +866,16 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const removePostRender =
|
let removePostRender;
|
||||||
viewer.scene?.postRender?.addEventListener?.(position);
|
function attachScene(nextViewer, nextEngine) {
|
||||||
|
removePostRender?.();
|
||||||
|
viewer = nextViewer;
|
||||||
|
engine = nextEngine;
|
||||||
|
const frames = engine?.frames.postRender ?? viewer?.scene?.postRender;
|
||||||
|
removePostRender = frames?.addEventListener?.(position);
|
||||||
|
position();
|
||||||
|
}
|
||||||
|
attachScene(viewer, engine);
|
||||||
|
|
||||||
function hide({ immediate = false, preserveWarm = false } = {}) {
|
function hide({ immediate = false, preserveWarm = false } = {}) {
|
||||||
if (!preserveWarm) clearWarm();
|
if (!preserveWarm) clearWarm();
|
||||||
@@ -1223,6 +1233,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
attachScene,
|
||||||
supportsPlayback: true,
|
supportsPlayback: true,
|
||||||
warm,
|
warm,
|
||||||
show,
|
show,
|
||||||
|
|||||||
+111
-22
@@ -889,7 +889,7 @@ function openExternal(url) {
|
|||||||
if (target) target.opener = null;
|
if (target) target.opener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusView(viewer, viewpoint) {
|
function focusView(viewer, viewpoint, engine) {
|
||||||
announceNavigationAuthority('bhote-koshi-event-focus');
|
announceNavigationAuthority('bhote-koshi-event-focus');
|
||||||
const target = Cesium.Cartesian3.fromDegrees(
|
const target = Cesium.Cartesian3.fromDegrees(
|
||||||
viewpoint.lon,
|
viewpoint.lon,
|
||||||
@@ -901,6 +901,17 @@ function focusView(viewer, viewpoint) {
|
|||||||
Cesium.Math.toRadians(viewpoint.pitchDeg),
|
Cesium.Math.toRadians(viewpoint.pitchDeg),
|
||||||
viewpoint.rangeM,
|
viewpoint.rangeM,
|
||||||
);
|
);
|
||||||
|
if (engine) {
|
||||||
|
engine.camera.cancelFlight();
|
||||||
|
engine.camera.flyToSphere({center: target, radius: 80}, {
|
||||||
|
heading:offset.heading, pitch:offset.pitch, range:offset.range, duration:2.2,
|
||||||
|
complete: () => {
|
||||||
|
engine.camera.lookAt(target, offset);
|
||||||
|
engine.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
viewer.camera.cancelFlight?.();
|
viewer.camera.cancelFlight?.();
|
||||||
viewer.camera.flyToBoundingSphere(new Cesium.BoundingSphere(target, 80), {
|
viewer.camera.flyToBoundingSphere(new Cesium.BoundingSphere(target, 80), {
|
||||||
offset,
|
offset,
|
||||||
@@ -994,9 +1005,16 @@ export function createBhoteKoshiEventLayer({
|
|||||||
evidenceFrameUpdater = updateEvidenceFrame,
|
evidenceFrameUpdater = updateEvidenceFrame,
|
||||||
videoFactory = DEFAULT_VIDEO_FACTORY,
|
videoFactory = DEFAULT_VIDEO_FACTORY,
|
||||||
embeddedMediaFactory = createBhoteKoshiEmbeddedMedia,
|
embeddedMediaFactory = createBhoteKoshiEmbeddedMedia,
|
||||||
|
createNativeRenderer,
|
||||||
panelStore = useBhoteKoshiPanelStore(applicationPinia()),
|
panelStore = useBhoteKoshiPanelStore(applicationPinia()),
|
||||||
} = {}) {
|
} = {}) {
|
||||||
let _viewer = null;
|
let _viewer = null;
|
||||||
|
let _engine = null;
|
||||||
|
let _native = null;
|
||||||
|
let _nativeFlood = null;
|
||||||
|
let _providers = null;
|
||||||
|
const cameraForScene = () => _engine?.camera ?? _viewer?.camera;
|
||||||
|
const canvasForScene = () => _engine?.host.canvas ?? _viewer?.scene?.canvas;
|
||||||
let _dataManager = null;
|
let _dataManager = null;
|
||||||
let _mapStackController = null;
|
let _mapStackController = null;
|
||||||
let _event = null;
|
let _event = null;
|
||||||
@@ -1160,7 +1178,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
event.imagery.rectangle.east,
|
event.imagery.rectangle.east,
|
||||||
event.imagery.rectangle.north,
|
event.imagery.rectangle.north,
|
||||||
);
|
);
|
||||||
const [beforeProvider, afterProvider] = await Promise.all([
|
const [beforeProvider, afterProvider] = await (_providers ??= Promise.all([
|
||||||
imageryProviderFactory(eventAsset(event.imagery.before.path), {
|
imageryProviderFactory(eventAsset(event.imagery.before.path), {
|
||||||
rectangle,
|
rectangle,
|
||||||
credit: 'Vantor WorldView Open Data · CC BY-NC 4.0',
|
credit: 'Vantor WorldView Open Data · CC BY-NC 4.0',
|
||||||
@@ -1169,8 +1187,13 @@ export function createBhoteKoshiEventLayer({
|
|||||||
rectangle,
|
rectangle,
|
||||||
credit: 'Vantor WorldView Open Data · CC BY-NC 4.0',
|
credit: 'Vantor WorldView Open Data · CC BY-NC 4.0',
|
||||||
}),
|
}),
|
||||||
]);
|
]));
|
||||||
if (!_enabled) return;
|
if (!_enabled) return;
|
||||||
|
if (_native) {
|
||||||
|
await _native.loadImages(event.imagery.rectangle, [beforeProvider, afterProvider]);
|
||||||
|
setSplit(_split);
|
||||||
|
return;
|
||||||
|
}
|
||||||
_imageryLayerCollection = _viewer.imageryLayers;
|
_imageryLayerCollection = _viewer.imageryLayers;
|
||||||
_beforeLayer = _imageryLayerCollection.addImageryProvider(beforeProvider);
|
_beforeLayer = _imageryLayerCollection.addImageryProvider(beforeProvider);
|
||||||
_afterLayer = _imageryLayerCollection.addImageryProvider(afterProvider);
|
_afterLayer = _imageryLayerCollection.addImageryProvider(afterProvider);
|
||||||
@@ -1187,7 +1210,11 @@ export function createBhoteKoshiEventLayer({
|
|||||||
: [];
|
: [];
|
||||||
let sampled = null;
|
let sampled = null;
|
||||||
try {
|
try {
|
||||||
sampled = await terrainSampler(_viewer, observations, { signal });
|
sampled = _engine && !_viewer
|
||||||
|
? observations.map(({lon,lat}) => ({
|
||||||
|
height: _engine.surface.terrainHeight(Cesium.Cartographic.fromDegrees(lon,lat)),
|
||||||
|
}))
|
||||||
|
: await terrainSampler(_viewer, observations, { signal });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.name === 'AbortError') throw error;
|
if (error?.name === 'AbortError') throw error;
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -1574,13 +1601,13 @@ export function createBhoteKoshiEventLayer({
|
|||||||
if (_sceneControls.evidencePathElevation !== 'source') {
|
if (_sceneControls.evidencePathElevation !== 'source') {
|
||||||
// Sample once while preparing the shot, never from an animation frame.
|
// Sample once while preparing the shot, never from an animation frame.
|
||||||
try {
|
try {
|
||||||
height = _viewer?.scene?.sampleHeight?.(
|
height = _engine?.surface?.sampleHeight?.(position) ?? _viewer?.scene?.sampleHeight?.(
|
||||||
position,
|
position,
|
||||||
[_floodCore, _floodHalo, _surgePoint].filter(Boolean),
|
[_floodCore, _floodHalo, _surgePoint].filter(Boolean),
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
if (!Number.isFinite(height))
|
if (!Number.isFinite(height))
|
||||||
height = _viewer?.scene?.globe?.getHeight?.(position);
|
height = _engine?.surface?.terrainHeight(position) ?? _viewer?.scene?.globe?.getHeight?.(position);
|
||||||
}
|
}
|
||||||
if (!Number.isFinite(height)) height = point.fallbackElevationM;
|
if (!Number.isFinite(height)) height = point.fallbackElevationM;
|
||||||
return Cesium.Cartesian3.fromDegrees(point.lon, point.lat, height + 8);
|
return Cesium.Cartesian3.fromDegrees(point.lon, point.lat, height + 8);
|
||||||
@@ -2582,13 +2609,16 @@ export function createBhoteKoshiEventLayer({
|
|||||||
disableDepthTestDistance: tileSafe ? Number.POSITIVE_INFINITY : 0,
|
disableDepthTestDistance: tileSafe ? Number.POSITIVE_INFINITY : 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
_viewer.dataSources.add(_floodDataSource);
|
if (_native) _nativeFlood = _native.draw(_floodDataSource);
|
||||||
|
else _viewer.dataSources.add(_floodDataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncFloodSurfaceMode() {
|
function syncFloodSurfaceMode() {
|
||||||
const nextMode = usesPhotorealFloodSurface() ? 'tiles' : 'terrain';
|
const nextMode = usesPhotorealFloodSurface() ? 'tiles' : 'terrain';
|
||||||
if (!_viewer || !_floodDataSource || _floodSurfaceMode === nextMode) return;
|
if ((!_viewer && !_native) || !_floodDataSource || _floodSurfaceMode === nextMode) return;
|
||||||
_viewer.dataSources.remove(_floodDataSource, true);
|
_nativeFlood?.destroy();
|
||||||
|
_nativeFlood = null;
|
||||||
|
_viewer?.dataSources.remove(_floodDataSource, true);
|
||||||
_floodDataSource = null;
|
_floodDataSource = null;
|
||||||
_floodHalo = null;
|
_floodHalo = null;
|
||||||
_floodCore = null;
|
_floodCore = null;
|
||||||
@@ -2601,7 +2631,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
|
|
||||||
function createSplitControl(event) {
|
function createSplitControl(event) {
|
||||||
_splitControl = createImagerySplit({
|
_splitControl = createImagerySplit({
|
||||||
scene: _viewer.scene,
|
scene: _viewer?.scene,
|
||||||
initialValue: _split,
|
initialValue: _split,
|
||||||
id: 'bhote-koshi-split-line',
|
id: 'bhote-koshi-split-line',
|
||||||
handleClass: 'bhote-koshi-split-handle',
|
handleClass: 'bhote-koshi-split-handle',
|
||||||
@@ -2621,7 +2651,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
function applyCinematicCamera() {
|
function applyCinematicCamera() {
|
||||||
if (
|
if (
|
||||||
!_cinematicActive ||
|
!_cinematicActive ||
|
||||||
!_viewer?.camera ||
|
!cameraForScene() ||
|
||||||
_cinematicKeyframes.length === 0
|
_cinematicKeyframes.length === 0
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
@@ -2635,7 +2665,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_cinematicOffset.heading = Cesium.Math.toRadians(pose.headingDeg);
|
_cinematicOffset.heading = Cesium.Math.toRadians(pose.headingDeg);
|
||||||
_cinematicOffset.pitch = Cesium.Math.toRadians(pose.pitchDeg);
|
_cinematicOffset.pitch = Cesium.Math.toRadians(pose.pitchDeg);
|
||||||
_cinematicOffset.range = pose.rangeM;
|
_cinematicOffset.range = pose.rangeM;
|
||||||
_viewer.camera.lookAt(pose.target, _cinematicOffset);
|
cameraForScene().lookAt(pose.target, _cinematicOffset);
|
||||||
_lastCinematicProgress = _progress;
|
_lastCinematicProgress = _progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2705,7 +2735,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_cinematicActive = true;
|
_cinematicActive = true;
|
||||||
_lastCinematicProgress = null;
|
_lastCinematicProgress = null;
|
||||||
announceNavigationAuthority('bhote-koshi-cinematic');
|
announceNavigationAuthority('bhote-koshi-cinematic');
|
||||||
_viewer?.camera?.cancelFlight?.();
|
cameraForScene()?.cancelFlight?.();
|
||||||
}
|
}
|
||||||
syncCinematicButton();
|
syncCinematicButton();
|
||||||
return true;
|
return true;
|
||||||
@@ -2723,7 +2753,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_cinematicActive = false;
|
_cinematicActive = false;
|
||||||
_lastCinematicProgress = null;
|
_lastCinematicProgress = null;
|
||||||
if (releaseCamera)
|
if (releaseCamera)
|
||||||
_viewer?.camera?.lookAtTransform?.(Cesium.Matrix4.IDENTITY);
|
cameraForScene()?.lookAtTransform?.(Cesium.Matrix4.IDENTITY);
|
||||||
syncCinematicButton();
|
syncCinematicButton();
|
||||||
renderHost.request('bhote-koshi-cinematic-stop');
|
renderHost.request('bhote-koshi-cinematic-stop');
|
||||||
}
|
}
|
||||||
@@ -2757,7 +2787,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attachCameraInputRelease() {
|
function attachCameraInputRelease() {
|
||||||
_cameraInputTarget = _viewer?.scene?.canvas || null;
|
_cameraInputTarget = canvasForScene() || null;
|
||||||
for (const type of CINEMATIC_INPUT_EVENTS) {
|
for (const type of CINEMATIC_INPUT_EVENTS) {
|
||||||
_cameraInputTarget?.addEventListener?.(type, handleManualCameraInput);
|
_cameraInputTarget?.addEventListener?.(type, handleManualCameraInput);
|
||||||
}
|
}
|
||||||
@@ -2803,7 +2833,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attachEvidenceCardActivation() {
|
function attachEvidenceCardActivation() {
|
||||||
_evidenceClickTarget = _viewer?.scene?.canvas || null;
|
_evidenceClickTarget = canvasForScene() || null;
|
||||||
_evidenceClickTarget?.addEventListener?.('click', handleEvidenceCardClick);
|
_evidenceClickTarget?.addEventListener?.('click', handleEvidenceCardClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3273,6 +3303,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
if (_panelRefs?.progressInput)
|
if (_panelRefs?.progressInput)
|
||||||
_panelRefs.progressInput.hidden = !scrubberAvailable;
|
_panelRefs.progressInput.hidden = !scrubberAvailable;
|
||||||
_splitControl?.setVisible(imageryAvailable);
|
_splitControl?.setVisible(imageryAvailable);
|
||||||
|
_native?.setVisible(imageryAvailable);
|
||||||
if (_beforeLayer) _beforeLayer.show = imageryAvailable;
|
if (_beforeLayer) _beforeLayer.show = imageryAvailable;
|
||||||
if (_afterLayer) _afterLayer.show = imageryAvailable;
|
if (_afterLayer) _afterLayer.show = imageryAvailable;
|
||||||
if (_panelRefs?.caveat) {
|
if (_panelRefs?.caveat) {
|
||||||
@@ -3391,6 +3422,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
function setSplit(split) {
|
function setSplit(split) {
|
||||||
_split = clampUnit(split);
|
_split = clampUnit(split);
|
||||||
_splitControl?.setValue(_split);
|
_splitControl?.setValue(_split);
|
||||||
|
_native?.setSplit(_split);
|
||||||
syncPanel();
|
syncPanel();
|
||||||
renderHost.request('bhote-koshi-split');
|
renderHost.request('bhote-koshi-split');
|
||||||
}
|
}
|
||||||
@@ -3451,7 +3483,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_embeddedEvidenceIndex = -1;
|
_embeddedEvidenceIndex = -1;
|
||||||
_embeddedMedia?.hide?.();
|
_embeddedMedia?.hide?.();
|
||||||
setSplit(0.5);
|
setSplit(0.5);
|
||||||
focusView(_viewer, _event.viewpoints.corridor);
|
if (cameraForScene()) focusView(_viewer, _event.viewpoints.corridor, _engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeImagery() {
|
function removeImagery() {
|
||||||
@@ -3462,9 +3494,59 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_imageryLayerCollection = null;
|
_imageryLayerCollection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init(viewer) {
|
async function attachRenderer(viewer, {engine = null} = {}) {
|
||||||
_viewer = viewer;
|
_viewer = viewer;
|
||||||
_embeddedMedia = embeddedMediaFactory({ viewer });
|
_engine = engine;
|
||||||
|
if (!_viewer && _engine) _native = createNativeRenderer(engine);
|
||||||
|
_embeddedMedia?.attachScene?.(viewer,engine);
|
||||||
|
if (!_enabled) return;
|
||||||
|
// The catalog has rebound the incoming map controller before this call.
|
||||||
|
if (_mapStackController) {
|
||||||
|
_comparisonLease = acquireImageryComparison(_mapStackController, {
|
||||||
|
owner: BHOTE_KOSHI_LAYER_ID,
|
||||||
|
switchPolicy: _presentation === BHOTE_KOSHI_SCENE_PRESENTATION ? 'preserve' : 'esri',
|
||||||
|
});
|
||||||
|
await _comparisonLease.ready;
|
||||||
|
}
|
||||||
|
await createImagery(_event);
|
||||||
|
if (_floodDataSource) {
|
||||||
|
if (_native) _nativeFlood = _native.draw(_floodDataSource);
|
||||||
|
else await _viewer.dataSources.add(_floodDataSource);
|
||||||
|
}
|
||||||
|
registerDynamicCredit(engine ?? viewer, BHOTE_KOSHI_CREDIT);
|
||||||
|
_scenePathPositionCache.clear();
|
||||||
|
_lastCinematicProgress = null;
|
||||||
|
await syncTerrainComparison();
|
||||||
|
createSplitControl(_event);
|
||||||
|
syncPresentationMode();
|
||||||
|
attachCameraInputRelease();
|
||||||
|
attachEvidenceCardActivation();
|
||||||
|
updateProgress(_progress);
|
||||||
|
setSplit(_split);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detachRenderer() {
|
||||||
|
detachCameraInputRelease();
|
||||||
|
detachEvidenceCardActivation();
|
||||||
|
_splitControl?.destroy();
|
||||||
|
_splitControl = null;
|
||||||
|
_embeddedMedia?.attachScene?.(null,null);
|
||||||
|
removeImagery();
|
||||||
|
_nativeFlood?.destroy();
|
||||||
|
_nativeFlood = null;
|
||||||
|
if (_floodDataSource && _viewer) _viewer.dataSources.remove(_floodDataSource,false);
|
||||||
|
_native?.destroy();
|
||||||
|
_native = null;
|
||||||
|
_engine = _viewer = null;
|
||||||
|
_mapStackController?.clearTerrainComparison?.();
|
||||||
|
const lease = _comparisonLease;
|
||||||
|
_comparisonLease = _mapStackController = null;
|
||||||
|
await lease?.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init(viewer, options) {
|
||||||
|
await attachRenderer(viewer,options);
|
||||||
|
_embeddedMedia = embeddedMediaFactory({ viewer, engine: _engine });
|
||||||
await loadEvent();
|
await loadEvent();
|
||||||
console.log('[Data:BhoteKoshi] Event pack ready');
|
console.log('[Data:BhoteKoshi] Event pack ready');
|
||||||
return true;
|
return true;
|
||||||
@@ -3480,7 +3562,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
try {
|
try {
|
||||||
const event = await loadEvent();
|
const event = await loadEvent();
|
||||||
throwIfEnableCancelled(signal, _enabled);
|
throwIfEnableCancelled(signal, _enabled);
|
||||||
registerDynamicCredit(viewer, BHOTE_KOSHI_CREDIT);
|
registerDynamicCredit(_engine ?? viewer, BHOTE_KOSHI_CREDIT);
|
||||||
if (_mapStackController) {
|
if (_mapStackController) {
|
||||||
_comparisonLease = acquireImageryComparison(_mapStackController, {
|
_comparisonLease = acquireImageryComparison(_mapStackController, {
|
||||||
owner: BHOTE_KOSHI_LAYER_ID,
|
owner: BHOTE_KOSHI_LAYER_ID,
|
||||||
@@ -3618,7 +3700,11 @@ export function createBhoteKoshiEventLayer({
|
|||||||
closeFrame(slot.poster);
|
closeFrame(slot.poster);
|
||||||
}
|
}
|
||||||
_evidenceFrameSlots = [];
|
_evidenceFrameSlots = [];
|
||||||
if (_floodDataSource) _viewer.dataSources.remove(_floodDataSource, true);
|
_nativeFlood?.destroy();
|
||||||
|
_nativeFlood = null;
|
||||||
|
_native?.clear();
|
||||||
|
_providers = null;
|
||||||
|
if (_floodDataSource && _viewer) _viewer.dataSources.remove(_floodDataSource, true);
|
||||||
_floodDataSource = null;
|
_floodDataSource = null;
|
||||||
_floodSurfaceMode = null;
|
_floodSurfaceMode = null;
|
||||||
_floodHalo = null;
|
_floodHalo = null;
|
||||||
@@ -3672,7 +3758,8 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_embeddedMedia?.destroy?.();
|
_embeddedMedia?.destroy?.();
|
||||||
_embeddedMedia = null;
|
_embeddedMedia = null;
|
||||||
_event = null;
|
_event = null;
|
||||||
_viewer = null;
|
_native?.destroy();
|
||||||
|
_native = _engine = _viewer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3849,6 +3936,8 @@ export function createBhoteKoshiEventLayer({
|
|||||||
source: 'Vantor + GeoPera',
|
source: 'Vantor + GeoPera',
|
||||||
updateInterval: 0,
|
updateInterval: 0,
|
||||||
init,
|
init,
|
||||||
|
attachRenderer,
|
||||||
|
detachRenderer,
|
||||||
enable,
|
enable,
|
||||||
disable,
|
disable,
|
||||||
update: async () => true,
|
update: async () => true,
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export class LayerLifecycle {
|
|||||||
for (const [id, entry] of [...this.layers]) {
|
for (const [id, entry] of [...this.layers]) {
|
||||||
await entry.toggleChain.catch(() => {});
|
await entry.toggleChain.catch(() => {});
|
||||||
if (entry.module.attachRenderer && entry.module.detachRenderer) {
|
if (entry.module.attachRenderer && entry.module.detachRenderer) {
|
||||||
if (entry.initialized) entry.module.detachRenderer(this.viewer);
|
if (entry.initialized) await entry.module.detachRenderer(this.viewer);
|
||||||
} else if (!(await this.destroyLayer(id))) {
|
} else if (!(await this.destroyLayer(id))) {
|
||||||
throw new Error(`Cannot detach layer ${id}`);
|
throw new Error(`Cannot detach layer ${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Texture, LinearFilter } from 'three';
|
||||||
|
import { createImageryGround } from '../../recentImagery/render/navaraGround.js';
|
||||||
|
import { drawStorySource } from '../../shared/render/navaraStory.js';
|
||||||
|
|
||||||
|
export function createNepalEventDisplay(engine, native) {
|
||||||
|
const owner = { slots: { a: null, b: null }, preview: null, split: 0.5 };
|
||||||
|
const ground = createImageryGround(engine, native, owner, 'gevNepalEvent');
|
||||||
|
let disposed = false;
|
||||||
|
function clear() {
|
||||||
|
for (const id of ['a', 'b']) {
|
||||||
|
owner.slots[id]?.atlas.texture.dispose();
|
||||||
|
owner.slots[id] = null;
|
||||||
|
}
|
||||||
|
if (!disposed) ground.sync();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
draw: (data) => drawStorySource(engine, native, data),
|
||||||
|
async loadImages(box, providers) {
|
||||||
|
const images = await Promise.all(
|
||||||
|
providers.map((p) => p.requestImage(0, 0, 0)),
|
||||||
|
);
|
||||||
|
if (disposed) return;
|
||||||
|
clear();
|
||||||
|
for (const [i, id] of ['a', 'b'].entries()) {
|
||||||
|
const texture = new Texture(images[i]);
|
||||||
|
texture.flipY = false;
|
||||||
|
texture.minFilter = texture.magFilter = LinearFilter;
|
||||||
|
texture.generateMipmaps = false;
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
owner.slots[id] = {
|
||||||
|
atlas: {
|
||||||
|
texture,
|
||||||
|
plan: {
|
||||||
|
geographic: true,
|
||||||
|
bounds: [
|
||||||
|
box.west,
|
||||||
|
box.north,
|
||||||
|
box.east - box.west,
|
||||||
|
box.south - box.north,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
box,
|
||||||
|
alpha: 0.96,
|
||||||
|
split: i === 0 ? 'left' : 'right',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ground.sync();
|
||||||
|
engine.render.request('nepal-imagery');
|
||||||
|
},
|
||||||
|
setVisible(show) {
|
||||||
|
for (const slot of Object.values(owner.slots))
|
||||||
|
if (slot) slot.alpha = show ? 0.96 : 0;
|
||||||
|
engine.render.request('nepal-imagery');
|
||||||
|
},
|
||||||
|
setSplit(x) {
|
||||||
|
owner.split = x;
|
||||||
|
engine.render.request('nepal-split');
|
||||||
|
},
|
||||||
|
clear,
|
||||||
|
destroy() {
|
||||||
|
disposed = true;
|
||||||
|
clear();
|
||||||
|
ground.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -199,7 +199,7 @@ function buildState(discs) {
|
|||||||
function groundEffectClass(EffectDesc, NavaraEffect) {
|
function groundEffectClass(EffectDesc, NavaraEffect) {
|
||||||
return class GroundEffectDesc extends EffectDesc {
|
return class GroundEffectDesc extends EffectDesc {
|
||||||
static key = KEY;
|
static key = KEY;
|
||||||
static insertAfter = ['gevRecentImagery', 'toneMapping'];
|
static insertAfter = ['gevObservedWeather', 'gevNepalEvent', 'gevRecentImagery', 'toneMapping'];
|
||||||
static insertBefore = [
|
static insertBefore = [
|
||||||
'gevFirmsGround',
|
'gevFirmsGround',
|
||||||
'gevPreSharpenOverlay',
|
'gevPreSharpenOverlay',
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import {
|
|||||||
} from '../../shared/render/navaraGroundSurface.js';
|
} from '../../shared/render/navaraGroundSurface.js';
|
||||||
|
|
||||||
const KEY = 'gevRecentImagery',
|
const KEY = 'gevRecentImagery',
|
||||||
registered = new WeakSet();
|
registered = new WeakMap();
|
||||||
const shader = /* glsl */ `
|
const shader = /* glsl */ `
|
||||||
uniform sampler2D tAllDepth,tSurfaceDepth,tA,tB;
|
uniform sampler2D tAllDepth,tSurfaceDepth,tA,tB;
|
||||||
uniform mat4 uInverseProjection,uWorld;
|
uniform mat4 uInverseProjection,uWorld;
|
||||||
uniform float uLogDepthFC,uSplit,uPixelRatio;
|
uniform float uLogDepthFC,uSplit,uPixelRatio;
|
||||||
uniform vec4 uBoundsA,uBoundsB,uBoxA,uBoxB,uPreview;
|
uniform vec4 uBoundsA,uBoundsB,uBoxA,uBoxB,uPreview;
|
||||||
uniform vec2 uLookA,uLookB;
|
uniform vec2 uLookA,uLookB;
|
||||||
uniform bool uPreviewVisible;
|
uniform bool uPreviewVisible,uGeographicA,uGeographicB;
|
||||||
vec3 displayColor(vec3 c) {
|
vec3 displayColor(vec3 c) {
|
||||||
return mix(c*12.92,1.055*pow(max(c,vec3(0.0)),vec3(1.0/2.4))-0.055,step(vec3(0.0031308),c));
|
return mix(c*12.92,1.055*pow(max(c,vec3(0.0)),vec3(1.0/2.4))-0.055,step(vec3(0.0031308),c));
|
||||||
}
|
}
|
||||||
@@ -21,10 +21,10 @@ vec3 linearColor(vec3 c) {
|
|||||||
return mix(c/12.92,pow(max((c+0.055)/1.055,vec3(0.0)),vec3(2.4)),step(vec3(0.04045),c));
|
return mix(c/12.92,pow(max((c+0.055)/1.055,vec3(0.0)),vec3(2.4)),step(vec3(0.04045),c));
|
||||||
}
|
}
|
||||||
bool inBox(vec2 geo,vec4 box) { return geo.x>=box.x && geo.y>=box.y && geo.x<=box.z && geo.y<=box.w; }
|
bool inBox(vec2 geo,vec4 box) { return geo.x>=box.x && geo.y>=box.y && geo.x<=box.z && geo.y<=box.w; }
|
||||||
vec3 imageColor(vec3 color,sampler2D image,vec4 bounds,vec4 box,vec2 look,vec2 geo,vec2 mercator,float x) {
|
vec3 imageColor(vec3 color,sampler2D image,vec4 bounds,vec4 box,vec2 look,vec2 geo,vec2 mercator,float x,bool geographic) {
|
||||||
if(look.x<=0.0 || !inBox(geo,box)) return color;
|
if(look.x<=0.0 || !inBox(geo,box)) return color;
|
||||||
if((look.y<0.0 && x>uSplit)||(look.y>0.0 && x<uSplit)) return color;
|
if((look.y<0.0 && x>uSplit)||(look.y>0.0 && x<uSplit)) return color;
|
||||||
vec2 at=(mercator-bounds.xy)/bounds.zw;
|
vec2 at=((geographic?geo:mercator)-bounds.xy)/bounds.zw;
|
||||||
if(any(lessThan(at,vec2(0.0)))||any(greaterThan(at,vec2(1.0)))) return color;
|
if(any(lessThan(at,vec2(0.0)))||any(greaterThan(at,vec2(1.0)))) return color;
|
||||||
vec4 pixel=texture2D(image,at);
|
vec4 pixel=texture2D(image,at);
|
||||||
return mix(color,pixel.rgb,pixel.a*look.x);
|
return mix(color,pixel.rgb,pixel.a*look.x);
|
||||||
@@ -42,8 +42,8 @@ void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor) {
|
|||||||
vec2 geo=vec2(atan(point.y,point.x),latitude)*57.29577951308232;
|
vec2 geo=vec2(atan(point.y,point.x),latitude)*57.29577951308232;
|
||||||
vec2 mercator=vec2((geo.x+180.0)/360.0,(1.0-log(tan(0.7853981633974483+latitude*0.5))/3.141592653589793)/2.0);
|
vec2 mercator=vec2((geo.x+180.0)/360.0,(1.0-log(tan(0.7853981633974483+latitude*0.5))/3.141592653589793)/2.0);
|
||||||
vec3 color=displayColor(inputColor.rgb);
|
vec3 color=displayColor(inputColor.rgb);
|
||||||
color=imageColor(color,tA,uBoundsA,uBoxA,uLookA,geo,mercator,uv.x);
|
color=imageColor(color,tA,uBoundsA,uBoxA,uLookA,geo,mercator,uv.x,uGeographicA);
|
||||||
color=imageColor(color,tB,uBoundsB,uBoxB,uLookB,geo,mercator,uv.x);
|
color=imageColor(color,tB,uBoundsB,uBoxB,uLookB,geo,mercator,uv.x,uGeographicB);
|
||||||
if(uPreviewVisible && inBox(geo,uPreview)) {
|
if(uPreviewVisible && inBox(geo,uPreview)) {
|
||||||
vec2 pixelWidth=max(fwidth(geo),vec2(1e-7));
|
vec2 pixelWidth=max(fwidth(geo),vec2(1e-7));
|
||||||
vec2 edges=min(geo-uPreview.xy,uPreview.zw-geo)/pixelWidth;
|
vec2 edges=min(geo-uPreview.xy,uPreview.zw-geo)/pixelWidth;
|
||||||
@@ -61,14 +61,18 @@ export function createImageryGround(
|
|||||||
engine,
|
engine,
|
||||||
{ view, EffectDesc, NavaraEffect },
|
{ view, EffectDesc, NavaraEffect },
|
||||||
owner,
|
owner,
|
||||||
|
key = KEY,
|
||||||
) {
|
) {
|
||||||
if (!view || !EffectDesc || !NavaraEffect)
|
if (!view || !EffectDesc || !NavaraEffect)
|
||||||
throw new TypeError('Imagery requires Navara ground effects');
|
throw new TypeError('Imagery requires Navara ground effects');
|
||||||
if (!registered.has(view)) {
|
if (!registered.has(view)) registered.set(view, new Set());
|
||||||
|
if (!registered.get(view).has(key)) {
|
||||||
class ImageryEffect extends EffectDesc {
|
class ImageryEffect extends EffectDesc {
|
||||||
static key = KEY;
|
static key = key;
|
||||||
static insertAfter = ['toneMapping'];
|
static insertAfter = ['toneMapping'];
|
||||||
static insertBefore = [
|
static insertBefore = [
|
||||||
|
'gevNepalEvent',
|
||||||
|
'gevObservedWeather',
|
||||||
'gevEarthquakeGround',
|
'gevEarthquakeGround',
|
||||||
'gevFirmsGround',
|
'gevFirmsGround',
|
||||||
'gevPreSharpenOverlay',
|
'gevPreSharpenOverlay',
|
||||||
@@ -80,13 +84,15 @@ export function createImageryGround(
|
|||||||
];
|
];
|
||||||
constructor(view, context, config) {
|
constructor(view, context, config) {
|
||||||
super(view, context, config);
|
super(view, context, config);
|
||||||
this.owner = config[KEY].owner;
|
this.owner = config[key].owner;
|
||||||
}
|
}
|
||||||
createPass() {
|
createPass() {
|
||||||
this.effect = new Effect('GevRecentImagery', shader, {
|
this.effect = new Effect('GevRecentImagery', shader, {
|
||||||
attributes: EffectAttribute.CONVOLUTION,
|
attributes: EffectAttribute.CONVOLUTION,
|
||||||
uniforms: new Map([
|
uniforms: new Map([
|
||||||
...groundSurfaceUniforms(),
|
...groundSurfaceUniforms(),
|
||||||
|
['uGeographicA', new Uniform(false)],
|
||||||
|
['uGeographicB', new Uniform(false)],
|
||||||
['uWorld', new Uniform(new Matrix4())],
|
['uWorld', new Uniform(new Matrix4())],
|
||||||
['uSplit', new Uniform(0.5)],
|
['uSplit', new Uniform(0.5)],
|
||||||
['uPixelRatio', new Uniform(1)],
|
['uPixelRatio', new Uniform(1)],
|
||||||
@@ -115,6 +121,7 @@ export function createImageryGround(
|
|||||||
u.get('uPreviewVisible').value = Boolean(o.preview);
|
u.get('uPreviewVisible').value = Boolean(o.preview);
|
||||||
for (const id of ['A', 'B']) {
|
for (const id of ['A', 'B']) {
|
||||||
const r = o.slots[id.toLowerCase()];
|
const r = o.slots[id.toLowerCase()];
|
||||||
|
u.get(`uGeographic${id}`).value = r?.atlas?.plan.geographic === true;
|
||||||
u.get(`t${id}`).value = r?.atlas?.texture || o.empty;
|
u.get(`t${id}`).value = r?.atlas?.texture || o.empty;
|
||||||
u.get(`uBounds${id}`).value = r?.atlas?.plan.bounds || [0, 0, 1, 1];
|
u.get(`uBounds${id}`).value = r?.atlas?.plan.bounds || [0, 0, 1, 1];
|
||||||
u.get(`uBox${id}`).value = boxVector(r?.box);
|
u.get(`uBox${id}`).value = boxVector(r?.box);
|
||||||
@@ -129,8 +136,11 @@ export function createImageryGround(
|
|||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
view.registerEffect(KEY, ImageryEffect);
|
class NepalEffect extends ImageryEffect {
|
||||||
registered.add(view);
|
static insertAfter = ['gevRecentImagery', 'toneMapping'];
|
||||||
|
}
|
||||||
|
view.registerEffect(key, key === KEY ? ImageryEffect : NepalEffect);
|
||||||
|
registered.get(view).add(key);
|
||||||
}
|
}
|
||||||
owner.empty = new DataTexture(new Uint8Array(4), 1, 1);
|
owner.empty = new DataTexture(new Uint8Array(4), 1, 1);
|
||||||
owner.empty.needsUpdate = true;
|
owner.empty.needsUpdate = true;
|
||||||
@@ -141,7 +151,7 @@ export function createImageryGround(
|
|||||||
sync() {
|
sync() {
|
||||||
const visible = Boolean(owner.slots.a || owner.slots.b || owner.preview);
|
const visible = Boolean(owner.slots.a || owner.slots.b || owner.preview);
|
||||||
if (visible && !handle)
|
if (visible && !handle)
|
||||||
handle = view.addEffect({ [KEY]: { owner }, visible: true });
|
handle = view.addEffect({ [key]: { owner }, visible: true });
|
||||||
else handle?.update({ visible });
|
else handle?.update({ visible });
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
|
|||||||
@@ -212,6 +212,8 @@ function groundEffectClass(EffectDesc, NavaraEffect) {
|
|||||||
static key = KEY;
|
static key = KEY;
|
||||||
static insertAfter = [
|
static insertAfter = [
|
||||||
'gevEarthquakeGround',
|
'gevEarthquakeGround',
|
||||||
|
'gevObservedWeather',
|
||||||
|
'gevNepalEvent',
|
||||||
'gevRecentImagery',
|
'gevRecentImagery',
|
||||||
'toneMapping',
|
'toneMapping',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import fs from 'node:fs/promises';
|
|||||||
// minimal look, whose pipeline has no toneMapping anchor.
|
// minimal look, whose pipeline has no toneMapping anchor.
|
||||||
const declarations = [
|
const declarations = [
|
||||||
['gevRecentImagery', '../../recentImagery/render/navaraGround.js'],
|
['gevRecentImagery', '../../recentImagery/render/navaraGround.js'],
|
||||||
|
['gevNepalEvent', '../../recentImagery/render/navaraGround.js'],
|
||||||
|
['gevObservedWeather', '../../weather/render/navaraSurface.js'],
|
||||||
['gevEarthquakeGround', '../../earthquakes/render/navara.js'],
|
['gevEarthquakeGround', '../../earthquakes/render/navara.js'],
|
||||||
['gevFirmsGround', './navaraGroundCells.js'],
|
['gevFirmsGround', './navaraGroundCells.js'],
|
||||||
['gevPreSharpenOverlay', '../../../globe/navara/overlay.js'],
|
['gevPreSharpenOverlay', '../../../globe/navara/overlay.js'],
|
||||||
@@ -18,7 +20,7 @@ const specs = await Promise.all(
|
|||||||
source = source.split('class GevPreSharpenOverlayEffectDesc')[1];
|
source = source.split('class GevPreSharpenOverlayEffectDesc')[1];
|
||||||
const anchors = (name) =>
|
const anchors = (name) =>
|
||||||
[
|
[
|
||||||
...source
|
...(key === 'gevNepalEvent' && name === 'insertAfter' ? source.split('class NepalEffect')[1] : source)
|
||||||
.match(new RegExp(`static ${name} = \\[([^\\]]*)\\]`))[1]
|
.match(new RegExp(`static ${name} = \\[([^\\]]*)\\]`))[1]
|
||||||
.matchAll(/'([^']+)'/g),
|
.matchAll(/'([^']+)'/g),
|
||||||
].map((m) => m[1]);
|
].map((m) => m[1]);
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ function weatherPass(engine, { view, EffectDesc, NavaraEffect }) {
|
|||||||
empty = null;
|
empty = null;
|
||||||
class WeatherEffect extends EffectDesc {
|
class WeatherEffect extends EffectDesc {
|
||||||
static key = KEY;
|
static key = KEY;
|
||||||
static insertAfter = ['toneMapping', 'gevRecentImagery'];
|
static insertAfter = ['gevNepalEvent', 'gevRecentImagery', 'toneMapping'];
|
||||||
static insertBefore = [
|
static insertBefore = [
|
||||||
'gevEarthquakeGround',
|
'gevEarthquakeGround',
|
||||||
'gevFirmsGround',
|
'gevFirmsGround',
|
||||||
|
|||||||
@@ -234,7 +234,6 @@ const FRAME_TARGETS = new Map([
|
|||||||
* refusal (UNAVAILABLE_ON_ENGINE) instead of running.
|
* refusal (UNAVAILABLE_ON_ENGINE) instead of running.
|
||||||
*/
|
*/
|
||||||
export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
export const VIEWER_ONLY_ACTIONS = Object.freeze([
|
||||||
'control_scene',
|
|
||||||
'annotate_map',
|
'annotate_map',
|
||||||
'clear_annotations',
|
'clear_annotations',
|
||||||
'fly_route',
|
'fly_route',
|
||||||
@@ -470,7 +469,7 @@ export function createGevActionRunner({
|
|||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
!viewer &&
|
!viewer &&
|
||||||
(VIEWER_ONLY_ACTIONS.includes(name) || CESIUM_LOOK_ACTIONS.includes(name))
|
((name === 'control_scene' && !sceneDirector) || VIEWER_ONLY_ACTIONS.includes(name) || CESIUM_LOOK_ACTIONS.includes(name))
|
||||||
)
|
)
|
||||||
return unavailableOnEngine(name, engine);
|
return unavailableOnEngine(name, engine);
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ const follow = (engine, position) =>
|
|||||||
|
|
||||||
test('the viewer-only actions are one exported list of real tools', () => {
|
test('the viewer-only actions are one exported list of real tools', () => {
|
||||||
assert.deepEqual(VIEWER_ONLY_ACTIONS, [
|
assert.deepEqual(VIEWER_ONLY_ACTIONS, [
|
||||||
'control_scene',
|
|
||||||
'annotate_map',
|
'annotate_map',
|
||||||
'clear_annotations',
|
'clear_annotations',
|
||||||
'fly_route',
|
'fly_route',
|
||||||
|
|||||||
Reference in new issue
Block a user