mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor(app): share application components and request services
This commit is contained in:
1 parent
a0a0128a59
commit
a67bc60a29
84 files changed
+2798
-1731
No files matched your search
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
import { StyleManager } from '../ui/composition.js';
|
||||
import { flyToAustin } from '../camera.js';
|
||||
import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
|
||||
|
||||
/** Construct the existing controls and camera presentation. */
|
||||
export function createApplicationControls({
|
||||
scene: { viewer, mapStackController },
|
||||
loaderStatus,
|
||||
Controls = StyleManager,
|
||||
services,
|
||||
placeSearch,
|
||||
defer,
|
||||
}) {
|
||||
// Initialize the style manager (post-processing, HUD, locations, share links)
|
||||
const styleManager = new Controls(viewer, {
|
||||
services,
|
||||
mapStackController,
|
||||
placeSearch,
|
||||
});
|
||||
defer(() => styleManager.orbitController.stop());
|
||||
defer(() => styleManager.hud.destroy());
|
||||
defer(() => styleManager.dispose());
|
||||
// The previous multi-canvas weather compositor remains disabled. Cockpit
|
||||
// clouds use a separate, capped low-resolution GPU pass that never attaches
|
||||
// Cesium fog or post-process stages and is fully stopped in map mode.
|
||||
const weatherEffects = null;
|
||||
const cockpitCloudEffects = initCockpitCloudEffects(viewer);
|
||||
defer(() => cockpitCloudEffects?.destroy());
|
||||
|
||||
// If no share link state, do default fly-to Austin
|
||||
if (!styleManager.hasShareState) {
|
||||
loaderStatus.textContent = 'Flying to Austin, TX...';
|
||||
defer(flyToAustin(viewer));
|
||||
} else {
|
||||
loaderStatus.textContent = 'Restoring shared view...';
|
||||
}
|
||||
|
||||
return { styleManager, weatherEffects, cockpitCloudEffects };
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { DataLayerManager } from '../data/manager.js';
|
||||
import flightsLayer from '../data/flights.js';
|
||||
import militaryFlightsLayer from '../data/militaryFlights.js';
|
||||
import alprCamerasLayer from '../data/alprCameras.js';
|
||||
import earthquakesLayer from '../data/earthquakes.js';
|
||||
import satellitesLayer from '../data/satellites.js';
|
||||
import rocketLaunchesLayer from '../data/rocketLaunches.js';
|
||||
import trafficLayer from '../data/traffic.js';
|
||||
import cctvLayer from '../data/cctv.js';
|
||||
import radioLayer from '../data/radio.js';
|
||||
import bikeshareLayer from '../data/bikeshare.js';
|
||||
import aisLiveVesselsLayer from '../data/aisLiveVessels.js';
|
||||
import militaryInstallationsLayer from '../data/militaryInstallations.js';
|
||||
import militaryAwarenessLayer from '../data/militaryAwareness.js';
|
||||
import localDataLayers from '../data/localLayers.js';
|
||||
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
||||
|
||||
/** Register the application layer catalog before allowing state restoration. */
|
||||
export function createApplicationData({
|
||||
scene: { viewer },
|
||||
controls: { styleManager },
|
||||
allowQaRegistration,
|
||||
onData,
|
||||
defer,
|
||||
}) {
|
||||
// Initialize data layer manager
|
||||
const dataManager = new DataLayerManager(viewer, {
|
||||
allowQaRegistration,
|
||||
});
|
||||
defer(async () => {
|
||||
await dataManager.destroyAll();
|
||||
if (dataManager.layers.size)
|
||||
throw new Error(
|
||||
`Data layers could not be destroyed: ${[...dataManager.layers.keys()].join(', ')}`,
|
||||
);
|
||||
});
|
||||
onData?.(dataManager);
|
||||
dataManager.register(flightsLayer);
|
||||
dataManager.register(militaryFlightsLayer);
|
||||
dataManager.register(earthquakesLayer);
|
||||
dataManager.register(alprCamerasLayer);
|
||||
dataManager.register(satellitesLayer);
|
||||
dataManager.register(rocketLaunchesLayer);
|
||||
rocketLaunchesLayer.attachDataManager(dataManager);
|
||||
dataManager.register(trafficLayer);
|
||||
dataManager.register(cctvLayer);
|
||||
dataManager.register(radioLayer);
|
||||
dataManager.register(bikeshareLayer);
|
||||
dataManager.register(aisLiveVesselsLayer);
|
||||
dataManager.register(militaryInstallationsLayer);
|
||||
dataManager.register(militaryAwarenessLayer);
|
||||
militaryAwarenessLayer.attachDataManager(dataManager);
|
||||
for (const layer of localDataLayers) {
|
||||
dataManager.register(layer);
|
||||
}
|
||||
// Restoration starts only after the complete production registry is sealed.
|
||||
dataManager.finalizeRegistrations(LAYER_STATE_REGISTRY);
|
||||
if (allowQaRegistration) {
|
||||
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
||||
if (targetManager !== dataManager)
|
||||
throw new Error('QA layer manager mismatch');
|
||||
return dataManager.registerForQa(layerModule);
|
||||
};
|
||||
window.__gevQaUnregisterLayer = (targetManager, layerId) => {
|
||||
if (targetManager !== dataManager)
|
||||
throw new Error('QA layer manager mismatch');
|
||||
return dataManager.unregisterForQa(layerId);
|
||||
};
|
||||
const register = window.__gevQaRegisterLayer;
|
||||
const unregister = window.__gevQaUnregisterLayer;
|
||||
defer(() => {
|
||||
if (window.__gevQaRegisterLayer === register)
|
||||
delete window.__gevQaRegisterLayer;
|
||||
if (window.__gevQaUnregisterLayer === unregister)
|
||||
delete window.__gevQaUnregisterLayer;
|
||||
});
|
||||
}
|
||||
dataManager.buildTogglePanel(document.getElementById('data-toggles'));
|
||||
styleManager.attachDataManager(dataManager);
|
||||
|
||||
return { dataManager };
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Extract a human-readable error message from any thrown value.
|
||||
* Handles Error objects, strings, and plain objects with message/error fields.
|
||||
* @param {*} error — caught exception value
|
||||
* @returns {string} best-effort error description
|
||||
*/
|
||||
export function describeError(error) {
|
||||
if (!error) return 'Unknown initialization error';
|
||||
if (error instanceof Error) {
|
||||
if (error.message && error.message.trim()) return error.message.trim();
|
||||
return error.name || 'Initialization error';
|
||||
}
|
||||
if (typeof error === 'string' && error.trim()) return error.trim();
|
||||
if (typeof error === 'object') {
|
||||
const maybeMessage = String(error.message || error.error || '').trim();
|
||||
if (maybeMessage) return maybeMessage;
|
||||
try {
|
||||
const serialized = JSON.stringify(error);
|
||||
if (serialized && serialized !== '{}') return serialized;
|
||||
} catch {
|
||||
// ignore serialization error
|
||||
}
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { createApplicationViewer } from '../app/viewer.js';
|
||||
import { registerDataCredits } from '../data/dataCredits.js';
|
||||
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
|
||||
import { MapStackController } from '../mapStackController.js';
|
||||
import { loadPhotorealisticTileset } from '../mapStartup.js';
|
||||
import { initLogoGaze } from '../logoGaze.js';
|
||||
import {
|
||||
uninstallRenderGovernor,
|
||||
governorRequestRender,
|
||||
} from '../renderGovernor.js';
|
||||
import { describeError } from './errors.js';
|
||||
|
||||
/** Construct the application globe using the caller's local configuration. */
|
||||
export async function createApplicationScene({
|
||||
googleApiKey,
|
||||
cesiumToken,
|
||||
credits,
|
||||
MapController = MapStackController,
|
||||
mapOptions = {},
|
||||
loaderStatus,
|
||||
signal,
|
||||
defer,
|
||||
}) {
|
||||
defer(initLogoGaze());
|
||||
const previousKey = window.__GOOGLE_MAPS_API_KEY__;
|
||||
if (googleApiKey) {
|
||||
window.__GOOGLE_MAPS_API_KEY__ = googleApiKey;
|
||||
defer(() => {
|
||||
if (window.__GOOGLE_MAPS_API_KEY__ !== googleApiKey) return;
|
||||
if (previousKey === undefined) delete window.__GOOGLE_MAPS_API_KEY__;
|
||||
else window.__GOOGLE_MAPS_API_KEY__ = previousKey;
|
||||
});
|
||||
}
|
||||
loaderStatus.textContent = 'Configuring viewer...';
|
||||
// Provider attribution stays visible, including clean-view and recording.
|
||||
const creditContainer = document.createElement('div');
|
||||
creditContainer.id = 'cesium-credits';
|
||||
document.body.appendChild(creditContainer);
|
||||
defer(() => creditContainer.remove());
|
||||
const viewer = createApplicationViewer({
|
||||
container: 'cesiumContainer',
|
||||
creditContainer,
|
||||
});
|
||||
defer(() => {
|
||||
uninstallRenderGovernor(viewer);
|
||||
if (!viewer.isDestroyed()) viewer.destroy();
|
||||
});
|
||||
registerDataCredits(viewer, credits);
|
||||
configureCreditKeyboardAccess(document);
|
||||
loaderStatus.textContent =
|
||||
googleApiKey || cesiumToken
|
||||
? 'Loading Google 3D Tiles...'
|
||||
: 'Loading the keyless globe...';
|
||||
const photoreal = await loadPhotorealisticTileset(Cesium, {
|
||||
googleApiKey,
|
||||
cesiumToken,
|
||||
});
|
||||
const tileset = photoreal.tileset;
|
||||
// A provider can finish after cancellation; retain ownership of its result.
|
||||
defer(() => {
|
||||
if (tileset && !tileset.isDestroyed()) {
|
||||
if (!viewer.scene.primitives.remove(tileset)) tileset.destroy();
|
||||
}
|
||||
});
|
||||
signal.throwIfAborted();
|
||||
if (tileset) {
|
||||
viewer.scene.primitives.add(tileset);
|
||||
// NOTE: Cesium World Terrain intentionally disabled — conflicts with Google 3D Tiles at high zoom.
|
||||
// Google Photorealistic 3D Tiles provide their own terrain/elevation.
|
||||
viewer.scene.globe.show = false;
|
||||
console.info(`[Init] Google 3D Tiles loaded via ${photoreal.route}.`);
|
||||
} else {
|
||||
if (photoreal.errors.length) {
|
||||
const tileError = photoreal.errors.at(-1);
|
||||
console.warn(
|
||||
'[Init] Google 3D Tiles unavailable, using the keyless globe:',
|
||||
tileError,
|
||||
);
|
||||
const tileErrorDetail = describeError(tileError);
|
||||
loaderStatus.textContent = `Google 3D Tiles unavailable (${tileErrorDetail}). Loading the keyless globe...`;
|
||||
}
|
||||
viewer.scene.globe.show = true;
|
||||
}
|
||||
|
||||
loaderStatus.textContent = 'Initializing systems...';
|
||||
|
||||
const mapStackController = new MapController(viewer, {
|
||||
requestRender: governorRequestRender,
|
||||
...mapOptions,
|
||||
googleTileset: tileset,
|
||||
cesiumToken,
|
||||
initialStack: tileset ? 'photoreal' : 'esri-imagery',
|
||||
// Task 5 (height-datum fix): rebroadcast stack changes as a window
|
||||
// CustomEvent so data layers (CCTV per-regime ground resolution) can
|
||||
// react without coupling MapStackController to layer modules. Fires on
|
||||
// 'switching'/'ready'/'error'; listeners derive the surface regime from
|
||||
// live scene state, so intermediate emissions are harmless.
|
||||
onChange: (state) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('gev:map-stack-changed', { detail: state }),
|
||||
);
|
||||
},
|
||||
onError: (message) => console.warn('[MapStack]', message),
|
||||
});
|
||||
defer(() => mapStackController.destroy());
|
||||
await mapStackController.setStack(tileset ? 'photoreal' : 'esri-imagery', {
|
||||
silent: true,
|
||||
});
|
||||
|
||||
signal.throwIfAborted();
|
||||
return { viewer, tileset, mapStackController };
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/** Bind a source contract once for the application without replacing its layer instance. */
|
||||
export function createSourceSlot(
|
||||
initial,
|
||||
methods,
|
||||
label = 'Source',
|
||||
optional = {},
|
||||
) {
|
||||
let active = initial;
|
||||
let binding = null;
|
||||
const source = Object.fromEntries(
|
||||
[...methods, ...Object.keys(optional)].map((method) => [
|
||||
method,
|
||||
(...args) => {
|
||||
if (typeof active?.[method] !== 'function' && optional[method])
|
||||
return optional[method](...args);
|
||||
if (typeof active?.[method] !== 'function')
|
||||
throw new Error(`${label} is not configured`);
|
||||
const owner = binding;
|
||||
const provider = active;
|
||||
const value = provider[method](...args);
|
||||
if (value && typeof value.then === 'function')
|
||||
return value.then((result) => {
|
||||
if (binding !== owner || active !== provider)
|
||||
throw new DOMException(`${label} was replaced`, 'AbortError');
|
||||
return result;
|
||||
});
|
||||
return value;
|
||||
},
|
||||
]),
|
||||
);
|
||||
for (const property of ['label', 'attribution']) {
|
||||
Object.defineProperty(source, property, {
|
||||
enumerable: true,
|
||||
get: () => active?.[property],
|
||||
});
|
||||
}
|
||||
return {
|
||||
source,
|
||||
configure(next) {
|
||||
if (methods.some((method) => typeof next?.[method] !== 'function'))
|
||||
throw new TypeError(`Invalid ${label.toLowerCase()}`);
|
||||
const owner = Symbol(label);
|
||||
active = next;
|
||||
binding = owner;
|
||||
return () => {
|
||||
if (binding !== owner) return;
|
||||
active = null;
|
||||
binding = null;
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createSourceSlot } from './sourceSlot.js';
|
||||
|
||||
test('replacing a source rejects its late result and old cleanup cannot clear the new source', async () => {
|
||||
let finish;
|
||||
const slot = createSourceSlot({ read: async () => 'default' }, ['read']);
|
||||
const old = slot.configure({ read: () => new Promise(resolve => { finish = resolve; }) });
|
||||
const pending = slot.source.read();
|
||||
const stop = slot.configure({ read: async () => 'replacement', attribution: { name: 'Replacement' } });
|
||||
old();
|
||||
finish('stale');
|
||||
await assert.rejects(pending, { name: 'AbortError' });
|
||||
assert.equal(await slot.source.read(), 'replacement');
|
||||
assert.equal(slot.source.attribution.name, 'Replacement');
|
||||
stop();
|
||||
assert.throws(() => slot.source.read(), /not configured/);
|
||||
});
|
||||
|
||||
test('an invalid replacement leaves the working source intact', async () => {
|
||||
const slot = createSourceSlot({ read: async () => 42 }, ['read']);
|
||||
assert.throws(() => slot.configure({}), TypeError);
|
||||
assert.equal(await slot.source.read(), 42);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import flights from '../data/flights.js';
|
||||
import military from '../data/militaryFlights.js';
|
||||
import vessels from '../data/aisLiveVessels.js';
|
||||
import { configureAlprSource } from '../data/alprCameras.js';
|
||||
import { configureCctvSource } from '../data/cctv.js';
|
||||
import { configureRadioSource } from '../data/radio.js';
|
||||
import { configureTrafficSource } from '../data/traffic.js';
|
||||
import { configureBikeshareSource } from '../data/bikeshare.js';
|
||||
import { configureInstallationSource } from '../data/militaryInstallations.js';
|
||||
import { configureSatelliteSource } from '../data/satellites.js';
|
||||
import { configureLaunchSource } from '../data/rocketLaunches.js';
|
||||
import { configureFirmsSource } from '../data/firmsHeatmap.js';
|
||||
import { configureMilitaryRegistrySource } from '../data/militaryRegistry.js';
|
||||
const configure = {
|
||||
alpr: configureAlprSource,
|
||||
cctv: configureCctvSource,
|
||||
radio: configureRadioSource,
|
||||
traffic: configureTrafficSource,
|
||||
bikeshare: configureBikeshareSource,
|
||||
installations: configureInstallationSource,
|
||||
satellites: configureSatelliteSource,
|
||||
launches: configureLaunchSource,
|
||||
firms: configureFirmsSource,
|
||||
};
|
||||
/** Configure sources before any registration or state restoration starts. */
|
||||
export function configureApplicationSources({
|
||||
layers = {},
|
||||
live = {},
|
||||
signal,
|
||||
defer,
|
||||
}) {
|
||||
for (const [name, source] of Object.entries(layers)) {
|
||||
if (!configure[name]) throw new TypeError(`Unknown layer source: ${name}`);
|
||||
defer(configure[name](source));
|
||||
}
|
||||
if (live.flights) flights.setSource(live.flights);
|
||||
if (live.military) {
|
||||
military.setSource(live.military);
|
||||
defer(configureMilitaryRegistrySource(live.military, { signal }));
|
||||
}
|
||||
if (live.vessels) vessels.setSource(live.vessels);
|
||||
}
|
||||
export const liveLayers = Object.freeze({ flights, military, vessels });
|
||||
@@ -0,0 +1,54 @@
|
||||
import { initFirstRunExperience } from '../firstRunExperience.js';
|
||||
|
||||
/** Reveal welcome controls only after restoration and the loading transition. */
|
||||
export function startApplicationChrome({
|
||||
loadingScreen,
|
||||
styleManager,
|
||||
dataManager,
|
||||
signal,
|
||||
initializeWelcome = initFirstRunExperience,
|
||||
initializeSettings,
|
||||
}) {
|
||||
let disposed = false;
|
||||
let firstRun;
|
||||
let revealTimer;
|
||||
let resolveDelay;
|
||||
const minimumDelay = new Promise((resolve) => {
|
||||
resolveDelay = resolve;
|
||||
});
|
||||
const delayTimer = setTimeout(resolveDelay, 1000);
|
||||
const revealFirstRun = () => {
|
||||
if (disposed || signal.aborted || firstRun) return;
|
||||
firstRun = initializeWelcome?.({ styleManager, dataManager });
|
||||
clearTimeout(revealTimer);
|
||||
loadingScreen.removeEventListener('transitionend', revealFirstRun);
|
||||
};
|
||||
void Promise.all([styleManager.initialRestorePromise, minimumDelay])
|
||||
.catch(() => {
|
||||
/* Restoration reports its own outcome through the controls. */
|
||||
})
|
||||
.then(() => {
|
||||
if (disposed || signal.aborted) return;
|
||||
loadingScreen.classList.add('hidden');
|
||||
loadingScreen.addEventListener('transitionend', revealFirstRun, {
|
||||
once: true,
|
||||
});
|
||||
revealTimer = setTimeout(revealFirstRun, 900);
|
||||
});
|
||||
const keySetup = Promise.resolve(
|
||||
signal.aborted ? null : initializeSettings?.({ signal }),
|
||||
);
|
||||
// Own the pending initializer too; it must not reveal a dialog after abort.
|
||||
void keySetup.catch(() =>
|
||||
console.error('Provider settings initialization failed'),
|
||||
);
|
||||
return async () => {
|
||||
disposed = true;
|
||||
clearTimeout(delayTimer);
|
||||
clearTimeout(revealTimer);
|
||||
resolveDelay();
|
||||
loadingScreen.removeEventListener('transitionend', revealFirstRun);
|
||||
firstRun?.destroy();
|
||||
(await keySetup.catch(() => null))?.destroy();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { SceneDirector } from '../scenes/director.js';
|
||||
import { initAnnotations } from '../annotations/index.js';
|
||||
import { initGevVoiceCommands } from '../voice/gevRealtime.js';
|
||||
import { installScopeMask, destroyScopeMask } from '../scopeMask.js';
|
||||
import {
|
||||
installRenderGovernor,
|
||||
getRenderGovernorDiagnostics,
|
||||
governorRequestRender,
|
||||
holdContinuousRender,
|
||||
releaseContinuousRender,
|
||||
} from '../renderGovernor.js';
|
||||
|
||||
/** Attach scene tools, rendering listeners and the application debug handle. */
|
||||
export function createApplicationTools({
|
||||
scene,
|
||||
controls,
|
||||
data,
|
||||
loadingScreen,
|
||||
placeSearch,
|
||||
voice = {},
|
||||
startChrome,
|
||||
onSceneDirector,
|
||||
signal,
|
||||
defer,
|
||||
}) {
|
||||
const { viewer, tileset, mapStackController } = scene;
|
||||
const { styleManager, weatherEffects, cockpitCloudEffects } = controls;
|
||||
const { dataManager } = data;
|
||||
const sceneDirector = new SceneDirector(viewer, styleManager, dataManager);
|
||||
defer(() => sceneDirector.destroy());
|
||||
onSceneDirector?.(sceneDirector);
|
||||
const annotations = initAnnotations({ viewer, tileset, placeSearch });
|
||||
defer(() => {
|
||||
if (window.__gevAnnotations === annotations) delete window.__gevAnnotations;
|
||||
annotations.destroy();
|
||||
});
|
||||
if (startChrome)
|
||||
defer(startChrome({ loadingScreen, styleManager, dataManager, signal }));
|
||||
// Idle render governor: flips the scene into requestRenderMode whenever
|
||||
// nothing animates per frame. Installed AFTER every module above has had
|
||||
// its chance to register pre-install holds. (perf wave 2)
|
||||
installRenderGovernor(viewer);
|
||||
|
||||
// Install the explicit scope mask used by the DISPLAY controls.
|
||||
installScopeMask(viewer);
|
||||
defer(() => destroyScopeMask());
|
||||
|
||||
// The follow camera recomputes the tracked target's dead-reckon position
|
||||
// every frame — tracking anything is a per-frame animation. (perf wave 2)
|
||||
const removeTrackingListener = viewer.trackedEntityChanged.addEventListener(
|
||||
() => {
|
||||
if (viewer.trackedEntity) holdContinuousRender('tracked-entity');
|
||||
else releaseContinuousRender('tracked-entity');
|
||||
},
|
||||
);
|
||||
|
||||
// Hidden-state suspension (perf wave 2): when the window/tab is hidden,
|
||||
// stop the default render loop outright — a hidden canvas repaints for
|
||||
// nobody, and browser rAF throttling still lets throttled frames burn
|
||||
// GPU. Holder/data state is untouched, so return is seamless: restore
|
||||
// the loop, refresh the one DOM surface we gated, render a frame.
|
||||
const syncVisibilitySuspension = () => {
|
||||
const hidden = document.hidden;
|
||||
viewer.useDefaultRenderLoop = !hidden;
|
||||
cockpitCloudEffects?.setSuspended?.(hidden);
|
||||
if (!hidden) {
|
||||
if (dataManager._panelRefreshPendingOnVisible) {
|
||||
dataManager._panelRefreshPendingOnVisible = false;
|
||||
dataManager._refreshTogglePanel();
|
||||
}
|
||||
governorRequestRender('visibility-restore');
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', syncVisibilitySuspension);
|
||||
defer(() =>
|
||||
document.removeEventListener('visibilitychange', syncVisibilitySuspension),
|
||||
);
|
||||
defer(() => {
|
||||
removeTrackingListener();
|
||||
releaseContinuousRender('tracked-entity');
|
||||
});
|
||||
// Apply the CURRENT state too — bootstrap can complete while the tab is
|
||||
// already hidden, and waiting for the next transition would leave the
|
||||
// loop burning behind a hidden tab. (perf wave 2 fix)
|
||||
syncVisibilitySuspension();
|
||||
|
||||
window.__godsEyeView = {
|
||||
viewer,
|
||||
styleManager,
|
||||
tileset,
|
||||
dataManager,
|
||||
sceneDirector,
|
||||
mapStackController,
|
||||
annotations,
|
||||
weatherEffects,
|
||||
cockpitCloudEffects,
|
||||
getRenderGovernorDiagnostics,
|
||||
requestRender: governorRequestRender,
|
||||
};
|
||||
const debug = window.__godsEyeView;
|
||||
defer(() => {
|
||||
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
||||
});
|
||||
const voiceCommands = initGevVoiceCommands({
|
||||
...voice,
|
||||
signal,
|
||||
placeSearch,
|
||||
viewer,
|
||||
styleManager,
|
||||
dataManager,
|
||||
sceneDirector,
|
||||
annotations,
|
||||
});
|
||||
defer(() => {
|
||||
voiceCommands.stop({ removeUi: true });
|
||||
if (window.__gevVoiceCommands === voiceCommands)
|
||||
delete window.__gevVoiceCommands;
|
||||
});
|
||||
debug.voiceCommands = voiceCommands;
|
||||
return { sceneDirector, annotations, voiceCommands };
|
||||
}
|
||||
Reference in new issue
Block a user