mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
- Imported metadata stays inert: descriptions, native labels, properties and KML extended data are dropped from every entity; names are kept as short plain text. GeoJSON loads with a describe that builds no HTML, and simplestyle (including marker-symbol) is dropped, so no icon asset is fetched for a file and GeoJSON draws in its row's color. - Bound geometry before loading: features and vertices are counted in the parsed GeoJSON, the KML document and while reading GPX, and a file over 20,000 features or 250,000 vertices is refused before Cesium builds it. - Imports run one batch at a time, so concurrent picks or drops cannot pass the 12-file cap. - Fly-to framing walks long tracks by index instead of spreading them into a call. - Fly-to is an immediate navigation through the application shell; the fly after an import is a deferred navigation stamped when the batch starts and re-asserted just before flying. - Dated imports never change the shared clock: imported sources expose no clock, entities are available for all time, and a KML gx:Track is drawn as its line. - GPX is read into GeoJSON with one line per track segment, without times. - KML/KMZ rows show a hatched swatch (they keep their own styles); keyboard focus is kept on the same control after hide/show and moves to the next row after remove. - Unit regressions for each, and the rendered QA now imports hostile metadata, dated KML/GPX and a two-segment track, and fails on any request for a file's contents or icon assets, a clock change, or surviving metadata. Co-Authored-By: Claude Opus 5.5 <[email protected]>
202 lines
7.3 KiB
JavaScript
202 lines
7.3 KiB
JavaScript
import { SceneDirector } from '../scenes/director.js';
|
|
import { initAnnotations } from '../annotations/index.js';
|
|
import { initDrawTool } from '../annotations/drawTool.js';
|
|
import { initGeoFileImport } from '../ui/geoFileImport.js';
|
|
import { initImageryBoxTool } from '../ui/imageryBoxTool.js';
|
|
import { createRecentImageryPanel } from '../ui/recentImagery.js';
|
|
import { initGevVoiceCommands } from '../voice/gevRealtime.js';
|
|
import { installViews, isEmbeddedInline } from './embed.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,
|
|
sceneDataPacks,
|
|
signal,
|
|
defer,
|
|
}) {
|
|
const { viewer, tileset, mapStackController, operations } = scene;
|
|
const { styleManager, weatherEffects, cockpitCloudEffects } = controls;
|
|
const { dataManager } = data;
|
|
const sceneDirector = new SceneDirector(viewer, styleManager, dataManager, {
|
|
dataPacks: sceneDataPacks,
|
|
isMapStackAvailable: (id) =>
|
|
mapStackController?.isStackAvailable(id) === true,
|
|
});
|
|
dataManager.layers
|
|
.get('bhote-koshi-2026')
|
|
?.module.attachSceneController(sceneDirector);
|
|
defer(() => sceneDirector.destroy());
|
|
onSceneDirector?.(sceneDirector);
|
|
const annotations = initAnnotations({
|
|
viewer,
|
|
tileset,
|
|
placeSearch,
|
|
resolver: operations.annotationResolver,
|
|
});
|
|
defer(() => {
|
|
if (window.__gevAnnotations === annotations) delete window.__gevAnnotations;
|
|
annotations.destroy();
|
|
});
|
|
// DISPLAY ▸ Draw: the same whiteboard, drawn by hand. It claims the pointer
|
|
// while a session is open, so its teardown belongs to the application
|
|
// lifetime rather than to whoever last pressed the button.
|
|
const drawTool = initDrawTool({ viewer, annotations });
|
|
defer(() => drawTool?.destroy());
|
|
// DISPLAY ▸ Import: the person's own GeoJSON/KML/KMZ/GPX files, each its
|
|
// own data source. They live for the page session, so the application
|
|
// lifetime removes them and the drop listeners on the viewer container.
|
|
const geoFileImport = initGeoFileImport({
|
|
viewer,
|
|
navigation: styleManager,
|
|
});
|
|
defer(() => geoFileImport?.destroy());
|
|
// DATA ▸ Recent Imagery: the box tool claims the pointer like Draw and the
|
|
// panel lives on the right rail, so both belong to the application
|
|
// lifetime. The tileset lets the layer drape while the globe is hidden.
|
|
const recentImagery = dataManager.layers.get('recent-imagery')?.module;
|
|
if (recentImagery) {
|
|
recentImagery.attachTileset(tileset);
|
|
const imageryBoxTool = initImageryBoxTool({
|
|
viewer,
|
|
onBox: (box) => recentImagery.setBox(box),
|
|
onCancel: (reason, message, box) => {
|
|
if (message) recentImagery.reportBoxRefusal(message, box);
|
|
},
|
|
onActive: (active) => recentImagery.setToolActive(active),
|
|
// The tool takes Escape in a capture listener, so the panel's order
|
|
// (clear a preview before cancelling the tool) is applied here.
|
|
onEscape: () => recentImagery.clearPreview(),
|
|
});
|
|
// The readout mounts in its rail body through the layer panel, like the
|
|
// weather readout.
|
|
data.presentation.attachRecentImagery((container) =>
|
|
createRecentImageryPanel({
|
|
container,
|
|
viewer,
|
|
layer: recentImagery,
|
|
tool: imageryBoxTool,
|
|
}),
|
|
);
|
|
// The live gate's handle (scripts/qa-recent-imagery.mjs).
|
|
const recentImageryHandle = { layer: recentImagery, tool: imageryBoxTool };
|
|
window.__gevRecentImagery = recentImageryHandle;
|
|
defer(() => {
|
|
if (window.__gevRecentImagery === recentImageryHandle)
|
|
delete window.__gevRecentImagery;
|
|
data.presentation.attachRecentImagery(null);
|
|
imageryBoxTool?.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 = () => {
|
|
// A panel's host may report it hidden while it is on screen; the panel
|
|
// keeps drawing itself (see keepPanelRendering in embed.js).
|
|
const hidden = document.hidden && !isEmbeddedInline();
|
|
viewer.useDefaultRenderLoop = !hidden;
|
|
cockpitCloudEffects?.setSuspended?.(hidden);
|
|
if (!hidden) {
|
|
data.presentation.flushVisible();
|
|
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,
|
|
surfaceServices: operations.surface,
|
|
requestRender: governorRequestRender,
|
|
};
|
|
const debug = window.__godsEyeView;
|
|
defer(() => {
|
|
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
|
});
|
|
const voiceCommands = initGevVoiceCommands({
|
|
...voice,
|
|
floorServices: operations.surface.groundFloor,
|
|
annotationResolver: operations.annotationResolver,
|
|
searchNavigation: operations.searchAndFlyTo,
|
|
signal,
|
|
placeSearch,
|
|
viewer,
|
|
styleManager,
|
|
dataManager,
|
|
sceneDirector,
|
|
annotations,
|
|
});
|
|
defer(() => {
|
|
voiceCommands.stop({ removeUi: true });
|
|
if (window.__gevVoiceCommands === voiceCommands)
|
|
delete window.__gevVoiceCommands;
|
|
});
|
|
debug.voiceCommands = voiceCommands;
|
|
defer(
|
|
installViews({
|
|
shell: styleManager,
|
|
viewer,
|
|
dataManager,
|
|
run: (name, args) => voiceCommands.runner(name, args, { signal }),
|
|
signal,
|
|
}),
|
|
);
|
|
return { sceneDirector, annotations, voiceCommands };
|
|
}
|