mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Retain live flights and earthquake observations across globe switches
This commit is contained in:
1 parent
a0e5f04c30
commit
c0062c4d3d
17 files changed
+428
-66
No files matched your search
+33
-11
@@ -66,6 +66,7 @@ export function createApplicationCatalog({
|
||||
metadata = APPLICATION_LAYER_METADATA,
|
||||
vesselOptions,
|
||||
resolveAsset,
|
||||
layerSession,
|
||||
nepalBoundaryResolver,
|
||||
streetLevelProviders,
|
||||
engineKind = 'cesium',
|
||||
@@ -79,30 +80,48 @@ export function createApplicationCatalog({
|
||||
|
||||
const sourceComposition = resolveCatalogSources(sources);
|
||||
sources = sourceComposition.sources;
|
||||
const militaryRegistry = createMilitaryRegistry();
|
||||
const militaryRegistry =
|
||||
layerSession?.militaryRegistry ?? createMilitaryRegistry();
|
||||
const weatherClock = createWeatherClock();
|
||||
const dispose = () => {
|
||||
signal.removeEventListener('abort', dispose);
|
||||
militaryRegistry.dispose();
|
||||
if (!layerSession) militaryRegistry.dispose();
|
||||
weatherClock.destroy();
|
||||
};
|
||||
signal.addEventListener('abort', dispose, { once: true });
|
||||
try {
|
||||
if (sourceComposition.isConfigured('military'))
|
||||
militaryRegistry.configureSource(sources.military, { signal });
|
||||
if (!layerSession?.militaryRegistry) {
|
||||
const lifetime = layerSession?.lifetime.signal ?? signal;
|
||||
if (sourceComposition.isConfigured('military'))
|
||||
militaryRegistry.configureSource(sources.military, { signal: lifetime });
|
||||
if (layerSession) {
|
||||
layerSession.militaryRegistry = militaryRegistry;
|
||||
lifetime.addEventListener('abort', militaryRegistry.dispose, {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Shared instances (awareness and launches read others) are made once,
|
||||
// when the first layer that needs them is built.
|
||||
const once = (create) => {
|
||||
let value;
|
||||
return () => (value ??= create());
|
||||
};
|
||||
const sessionModel = (id, create) => {
|
||||
if (!layerSession) return create();
|
||||
const models = (layerSession.models ??= new Map());
|
||||
if (!models.has(id)) models.set(id, create());
|
||||
return models.get(id);
|
||||
};
|
||||
const flights = once(() =>
|
||||
createApplicationFlights({
|
||||
surface,
|
||||
source: sources.flights,
|
||||
militaryRegistry,
|
||||
resolveAsset,
|
||||
}),
|
||||
sessionModel('flights', () =>
|
||||
createApplicationFlights({
|
||||
surface,
|
||||
source: sources.flights,
|
||||
militaryRegistry,
|
||||
resolveAsset,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const military = once(() =>
|
||||
createApplicationMilitary({
|
||||
@@ -158,7 +177,10 @@ export function createApplicationCatalog({
|
||||
],
|
||||
[
|
||||
'earthquakes',
|
||||
() => createApplicationEarthquakes({ source: sources.earthquakes }),
|
||||
() =>
|
||||
sessionModel('earthquakes', () =>
|
||||
createApplicationEarthquakes({ source: sources.earthquakes }),
|
||||
),
|
||||
],
|
||||
[
|
||||
'air-quality',
|
||||
|
||||
+9
-4
@@ -8,12 +8,13 @@ export function createApplicationData({
|
||||
controls: { styleManager },
|
||||
catalog,
|
||||
allowQaRegistration,
|
||||
layerSession,
|
||||
onData,
|
||||
defer,
|
||||
}) {
|
||||
// Initialize data layer manager
|
||||
// The modules get { signal, engine } on every call (viewer null on Navara).
|
||||
const dataManager = new LayerLifecycle(viewer, {
|
||||
const dataManager = layerSession?.manager ?? new LayerLifecycle(viewer, {
|
||||
engine,
|
||||
allowQaRegistration,
|
||||
// A panel's host may report it hidden while it is on screen; its layers
|
||||
@@ -21,7 +22,12 @@ export function createApplicationData({
|
||||
visibilityTarget: panelAwareVisibility(document),
|
||||
getSourceAvailability: catalog?.getSourceAvailability,
|
||||
});
|
||||
if (layerSession) layerSession.manager = dataManager;
|
||||
defer(async () => {
|
||||
if (layerSession && !layerSession.lifetime.signal.aborted) {
|
||||
await dataManager.detachScene();
|
||||
return;
|
||||
}
|
||||
await dataManager.destroyAll();
|
||||
if (dataManager.layers.size)
|
||||
throw new Error(
|
||||
@@ -35,12 +41,11 @@ export function createApplicationData({
|
||||
onData?.(dataManager);
|
||||
if (!catalog?.layers || !catalog?.metadata)
|
||||
throw new TypeError('An application layer catalog is required');
|
||||
for (const layer of catalog.layers) dataManager.register(layer);
|
||||
dataManager.bindCatalog(viewer, engine, catalog);
|
||||
for (const layer of catalog.layers) layer.attachDataManager?.(dataManager);
|
||||
for (const layer of catalog.layers)
|
||||
layer.attachMapStackController?.(mapStackController);
|
||||
// Restoration starts only after the caller's complete registry is sealed.
|
||||
dataManager.finalizeRegistrations(catalog.metadata);
|
||||
|
||||
if (allowQaRegistration) {
|
||||
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
||||
if (targetManager !== dataManager)
|
||||
|
||||
+12
-2
@@ -29,12 +29,14 @@ export async function createApplicationScene({
|
||||
signal,
|
||||
defer,
|
||||
rendererCache,
|
||||
layerSession,
|
||||
...options
|
||||
}) {
|
||||
const operations = createApplicationOperations({
|
||||
const operations = layerSession?.operations ?? createApplicationOperations({
|
||||
requests: requestServices,
|
||||
signal,
|
||||
signal: layerSession?.lifetime.signal ?? signal,
|
||||
});
|
||||
if (layerSession) layerSession.operations = operations;
|
||||
defer(initLogoGaze());
|
||||
const previousKey = window.__GOOGLE_MAPS_API_KEY__;
|
||||
if (googleApiKey) {
|
||||
@@ -55,6 +57,14 @@ export async function createApplicationScene({
|
||||
const { viewer, engine, tileset, mapStackController } = rendererCache
|
||||
? await rendererCache.acquire(options.engineKind, build, defer)
|
||||
: await build({ defer });
|
||||
if (layerSession) {
|
||||
// DEM cells belong to the session. Mesh samples came from the outgoing
|
||||
// scene, including when the incoming canvas was warm in the cache.
|
||||
operations.surface.groundFloor.clearMeshFloorCells();
|
||||
operations.surface.groundFloor.setMeshFloorPreferred(
|
||||
mapStackController.getActiveId() === 'photoreal',
|
||||
);
|
||||
}
|
||||
return { viewer, engine, tileset, mapStackController, operations };
|
||||
}
|
||||
|
||||
|
||||
@@ -412,7 +412,7 @@ export function contextRestoreLayerIds(snapshot = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Contacts mode requires both observed-flight feeds. Navara also supports
|
||||
* Contacts mode requires both observed-flight feeds. Both globes support
|
||||
* first-person flight directly from a single selected aircraft layer.
|
||||
*
|
||||
* @param {object} input Current context and dependency visibility.
|
||||
|
||||
@@ -470,6 +470,36 @@ export class LayerStateCoordinator {
|
||||
return null;
|
||||
}
|
||||
|
||||
// On an in-page engine switch, an attached session model can already own
|
||||
// this exact contact. Ordinary shared links must still verify the source.
|
||||
retainedTrackingTarget() {
|
||||
const selected = this._selectedShareTrackingTarget();
|
||||
if (!selected || !this.dataManager.isEnabled(selected.layerId)) return null;
|
||||
const model = this.dataManager.layers.get(selected.layerId)?.module;
|
||||
if (
|
||||
model?.attachRenderer &&
|
||||
model.getParams?.()[selected.optionKey] === selected.targetId &&
|
||||
model.hasContact?.(selected.targetId) === true
|
||||
) return selected;
|
||||
return null;
|
||||
}
|
||||
|
||||
resumeRetainedTracking(previous) {
|
||||
const selected = this.retainedTrackingTarget();
|
||||
if (
|
||||
!selected || selected.layerId !== previous.layerId ||
|
||||
selected.targetId !== previous.targetId
|
||||
) return null;
|
||||
const model = this.dataManager.layers.get(selected.layerId).module;
|
||||
const followed = model.trackById(selected.targetId, {
|
||||
origin: LAYER_RESTORE_ORIGINS.share,
|
||||
});
|
||||
if (!followed) return null;
|
||||
const result = { ...selected, status: 'found', classification: 'followed', cleared: false };
|
||||
this._publishTrackingRestoreStatus(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
_passivelyClearTrackingSelection(selected) {
|
||||
const current =
|
||||
this._durableState.options?.[selected.optionOwner]?.[selected.optionKey];
|
||||
|
||||
@@ -121,6 +121,50 @@ export class LayerLifecycle {
|
||||
this._registerLayer(layerModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the scene only. Models with an explicit renderer lifetime keep
|
||||
* their entry, polling interval, visibility intents and in-flight source.
|
||||
* Legacy modules use their ordinary teardown until they gain that contract.
|
||||
*/
|
||||
async detachScene() {
|
||||
for (const [id, entry] of [...this.layers]) {
|
||||
await entry.toggleChain.catch(() => {});
|
||||
if (entry.module.attachRenderer && entry.module.detachRenderer) {
|
||||
if (entry.initialized) entry.module.detachRenderer(this.viewer);
|
||||
} else if (!(await this.destroyLayer(id))) {
|
||||
throw new Error(`Cannot detach layer ${id}`);
|
||||
}
|
||||
}
|
||||
this.viewer = null;
|
||||
this.engine = null;
|
||||
this._registrationsFinalized = false;
|
||||
this._registrationDispositions = null;
|
||||
}
|
||||
|
||||
/** Bind the next catalog only after the preceding scene released its entries. */
|
||||
bindCatalog(viewer, engine, catalog) {
|
||||
if (this._registrationsFinalized) throw new Error('Scene already attached');
|
||||
this.viewer = viewer;
|
||||
this.engine = engine ?? engineFromViewer(viewer) ?? null;
|
||||
this._catalogSourceAvailability = catalog.getSourceAvailability;
|
||||
for (const layer of catalog.layers) {
|
||||
const entry = this.layers.get(layer.id);
|
||||
if (!entry) this.register(layer);
|
||||
else {
|
||||
if (entry.module !== layer) throw new Error(`Layer owner changed: ${layer.id}`);
|
||||
if (entry.initialized) layer.attachRenderer(viewer, this._moduleContext());
|
||||
}
|
||||
}
|
||||
// Retained entries may appear anywhere in the directory. Preserve both
|
||||
// catalog order (panel/serialization) and each live entry's identity.
|
||||
for (const { id } of catalog.layers) {
|
||||
const entry = this.layers.get(id);
|
||||
this.layers.delete(id);
|
||||
this.layers.set(id, entry);
|
||||
}
|
||||
this.finalizeRegistrations(catalog.metadata);
|
||||
}
|
||||
|
||||
/** Register a synthetic layer after sealing in an explicitly dev-enabled manager. */
|
||||
registerForQa(layerModule) {
|
||||
if (!this._allowQaRegistration || !this._registrationsFinalized) {
|
||||
|
||||
@@ -16,9 +16,9 @@ export { createUsgsEarthquakeSource } from './source.js';
|
||||
/**
|
||||
* Own one earthquake display and its refresh lifecycle.
|
||||
* renderer.create({viewer, engine}) owns drawing and returns:
|
||||
* setVisible(boolean), replace(discs), records(limit), destroy().
|
||||
* setVisible(boolean), replace(discs), destroy().
|
||||
* Discs are plain ECEF anchors, metre radii and Rgba colours; renderers keep
|
||||
* source order and return plain raw analyst rows (model.mapAnalystRecord).
|
||||
* source order. The layer retains observations for queries while detached.
|
||||
*/
|
||||
export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||
if (typeof source?.getSnapshot !== 'function')
|
||||
@@ -26,7 +26,10 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||
if (!overlayHost) throw new TypeError('Earthquakes require an overlay host');
|
||||
if (typeof renderer?.create !== 'function')
|
||||
throw new TypeError('Earthquakes require a renderer factory');
|
||||
let _viewer = null;
|
||||
let initialized = false;
|
||||
let observations = [];
|
||||
let discs = [];
|
||||
let labels = [];
|
||||
let _request = null;
|
||||
let _renderer = null;
|
||||
let _count = 0;
|
||||
@@ -34,6 +37,14 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||
let _lastError = null;
|
||||
let _enabled = false;
|
||||
|
||||
function publishLabels() {
|
||||
overlayHost.setEntries(EARTHQUAKE_OVERLAY_SOURCE_ID, labels, {
|
||||
cohortLimit: EARTHQUAKE_OVERLAY_COHORT_LIMIT,
|
||||
collisionCapacity: EARTHQUAKE_OVERLAY_COLLISION_CAPACITY,
|
||||
moving: false,
|
||||
});
|
||||
}
|
||||
|
||||
const layer = {
|
||||
id: 'earthquakes',
|
||||
name: 'Earthquakes (24h)',
|
||||
@@ -42,17 +53,32 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||
updateInterval: 60000,
|
||||
|
||||
init(viewer, { engine = null } = {}) {
|
||||
if (_renderer) throw new Error('Earthquake layer is already initialized');
|
||||
_viewer = viewer;
|
||||
if (initialized) throw new Error('Earthquake layer is already initialized');
|
||||
initialized = true;
|
||||
try {
|
||||
if (viewer || engine) this.attachRenderer(viewer, { engine });
|
||||
} catch (error) {
|
||||
this.destroy();
|
||||
throw error;
|
||||
}
|
||||
console.log('[Data:Earthquakes] Initialized');
|
||||
},
|
||||
|
||||
attachRenderer(viewer, { engine = null } = {}) {
|
||||
if (!initialized || _renderer) throw new Error('Invalid earthquake scene attachment');
|
||||
_renderer = renderer.create({ viewer, engine });
|
||||
if (!_renderer) throw new Error('Earthquake renderer unavailable');
|
||||
_renderer.setVisible(false);
|
||||
_count = 0;
|
||||
_lastUpdate = null;
|
||||
_lastError = null;
|
||||
_enabled = false;
|
||||
_renderer.setVisible(_enabled);
|
||||
if (discs.length) _renderer.replace(discs);
|
||||
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, _enabled);
|
||||
if (_enabled) publishLabels();
|
||||
},
|
||||
|
||||
detachRenderer() {
|
||||
overlayHost.clearSource(EARTHQUAKE_OVERLAY_SOURCE_ID);
|
||||
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, false);
|
||||
console.log('[Data:Earthquakes] Initialized');
|
||||
_renderer?.destroy();
|
||||
_renderer = null;
|
||||
},
|
||||
|
||||
enable(viewer) {
|
||||
@@ -60,7 +86,10 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||
// No continuous-render hold: the discs are static geometry now, so the
|
||||
// layer has no per-frame animator to keep the render loop alive for.
|
||||
_renderer?.setVisible(true);
|
||||
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, true);
|
||||
if (_renderer) {
|
||||
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, true);
|
||||
if (labels.length) publishLabels();
|
||||
}
|
||||
},
|
||||
|
||||
disable(viewer) {
|
||||
@@ -73,7 +102,7 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||
},
|
||||
|
||||
async update(viewer) {
|
||||
if (!_enabled || !_renderer) return false;
|
||||
if (!_enabled) return false;
|
||||
_request?.abort();
|
||||
const request = new AbortController();
|
||||
_request = request;
|
||||
@@ -128,17 +157,12 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
_renderer.replace(nextDiscs);
|
||||
if (_enabled) {
|
||||
overlayHost.setEntries(
|
||||
EARTHQUAKE_OVERLAY_SOURCE_ID,
|
||||
selectEarthquakeOverlayCohort(overlayEntries),
|
||||
{
|
||||
cohortLimit: EARTHQUAKE_OVERLAY_COHORT_LIMIT,
|
||||
collisionCapacity: EARTHQUAKE_OVERLAY_COLLISION_CAPACITY,
|
||||
moving: false,
|
||||
},
|
||||
);
|
||||
observations = rows;
|
||||
discs = nextDiscs;
|
||||
labels = selectEarthquakeOverlayCohort(overlayEntries);
|
||||
if (_renderer) {
|
||||
_renderer.replace(discs);
|
||||
publishLabels();
|
||||
}
|
||||
|
||||
_count = count;
|
||||
@@ -157,15 +181,15 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
destroy(viewer = _viewer) {
|
||||
destroy() {
|
||||
_request?.abort();
|
||||
_request = null;
|
||||
_viewer = null;
|
||||
initialized = false;
|
||||
_enabled = false;
|
||||
overlayHost.clearSource(EARTHQUAKE_OVERLAY_SOURCE_ID);
|
||||
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, false);
|
||||
_renderer?.destroy();
|
||||
_renderer = null;
|
||||
this.detachRenderer();
|
||||
observations = [];
|
||||
discs = [];
|
||||
labels = [];
|
||||
_count = 0;
|
||||
_lastUpdate = null;
|
||||
_lastError = null;
|
||||
@@ -180,11 +204,13 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
|
||||
*/
|
||||
getAnalystRecords(maxCount = 2000) {
|
||||
if (!_enabled || !_renderer) return [];
|
||||
if (!_enabled) return [];
|
||||
const limit = Number.isFinite(maxCount)
|
||||
? Math.max(1, Math.floor(maxCount))
|
||||
: 2000;
|
||||
return _renderer.records(limit).map(mapAnalystRecord);
|
||||
return observations.slice(0, limit).map((row, index) =>
|
||||
mapAnalystRecord({ ...row, id: row.usgsId, depth: row.depthKm }, index),
|
||||
);
|
||||
},
|
||||
|
||||
getStats() {
|
||||
|
||||
@@ -92,3 +92,28 @@ test('USGS body completion honors cancellation even with an uncooperative transp
|
||||
name: 'AbortError',
|
||||
});
|
||||
});
|
||||
|
||||
test('an observation received between scenes is queryable and draws without a second fetch', async () => {
|
||||
let resolve, polls = 0;
|
||||
const h = harness({ getSnapshot() {
|
||||
polls++;
|
||||
return new Promise(done => { resolve = done; });
|
||||
} });
|
||||
try {
|
||||
const pending = h.layer.update();
|
||||
h.layer.detachRenderer();
|
||||
assert.equal(h.sources.length, 0);
|
||||
resolve([row]);
|
||||
assert.equal(await pending, true);
|
||||
const [record] = h.layer.getAnalystRecords();
|
||||
assert.equal(record.id, row.usgsId);
|
||||
assert.equal(record.lat, row.lat);
|
||||
assert.equal(h.events.length, 0);
|
||||
h.layer.attachRenderer(h.viewer);
|
||||
assert.equal(h.sources[0].entities.values.length, 1);
|
||||
assert.equal(h.events.length, 1);
|
||||
assert.equal(polls, 1);
|
||||
} finally {
|
||||
h.layer.destroy();
|
||||
}
|
||||
});
|
||||
+26
-5
@@ -15,11 +15,14 @@ let kind = pageEngineKind();
|
||||
let application;
|
||||
let busy = false;
|
||||
const renderers = createRendererCache();
|
||||
const layerSession = { lifetime: new AbortController() };
|
||||
window.addEventListener('pagehide', (event) => {
|
||||
if (!event.persisted) renderers.clear();
|
||||
if (event.persisted) return;
|
||||
layerSession.lifetime.abort();
|
||||
void application?.destroy().catch(report).finally(() => renderers.clear());
|
||||
});
|
||||
|
||||
async function start(next, engineSwitch = false) {
|
||||
async function start(next, engineSwitch = false, cockpit = null) {
|
||||
kind = next;
|
||||
application = createStandaloneApplication({
|
||||
googleApiKey: import.meta.env.GOOGLE_MAPS_API_KEY,
|
||||
@@ -27,11 +30,25 @@ async function start(next, engineSwitch = false) {
|
||||
allowQaRegistration: import.meta.env.DEV,
|
||||
engineKind: next,
|
||||
rendererCache: renderers,
|
||||
layerSession,
|
||||
engineSwitch,
|
||||
});
|
||||
const { scene, controls } = await application.start();
|
||||
if (engineSwitch) {
|
||||
await controls.styleManager.initialRestorePromise;
|
||||
const style = controls.styleManager;
|
||||
await style.initialRestorePromise;
|
||||
// Only resume the aircraft that actually survived the handoff. A history
|
||||
// entry may select a different subject, and a stale track may disappear.
|
||||
const target = style.getAircraftTrackingTarget();
|
||||
if (
|
||||
cockpit?.active &&
|
||||
target &&
|
||||
target?.id === cockpit.subject?.id &&
|
||||
target?.layerId === cockpit.subject?.layerId &&
|
||||
style.controlCockpit('enter').ok
|
||||
) {
|
||||
style.cockpitView.setVisionMode(cockpit.visionMode);
|
||||
}
|
||||
await waitForGlobeFrame(scene.engine);
|
||||
}
|
||||
}
|
||||
@@ -106,12 +123,16 @@ async function reconcile(remember = false) {
|
||||
present();
|
||||
const previous = kind;
|
||||
let cover;
|
||||
let cockpit;
|
||||
try {
|
||||
while (pageEngineKind() !== kind) {
|
||||
cover ??= keepGlobeFrame(application.getComponents().scene.engine);
|
||||
cockpit = application
|
||||
.getComponents()
|
||||
.controls.styleManager.getCockpitState();
|
||||
renderers.retain(kind);
|
||||
await application.destroy();
|
||||
await start(pageEngineKind(), true);
|
||||
await start(pageEngineKind(), true, cockpit);
|
||||
}
|
||||
if (remember) {
|
||||
try {
|
||||
@@ -127,7 +148,7 @@ async function reconcile(remember = false) {
|
||||
history.replaceState(history.state, '', url);
|
||||
try {
|
||||
await application.destroy();
|
||||
await start(previous, true);
|
||||
await start(previous, true, cockpit);
|
||||
application
|
||||
.getComponents()
|
||||
.controls.styleManager._showToast(describeError(error));
|
||||
|
||||
@@ -500,7 +500,7 @@ export function createGroundFloor({ terrain, signal }) {
|
||||
}
|
||||
|
||||
/** Test hook: clears mesh cells (never called at runtime — cells are valid all session). */
|
||||
function _clearMeshFloorCellsForTest() {
|
||||
function clearMeshFloorCells() {
|
||||
_meshCells.clear();
|
||||
}
|
||||
|
||||
@@ -663,7 +663,8 @@ export function createGroundFloor({ terrain, signal }) {
|
||||
meshFloorSampleWithinPrior,
|
||||
reportValidatedMeshFloorCell,
|
||||
cachedMeshFloor,
|
||||
_clearMeshFloorCellsForTest,
|
||||
clearMeshFloorCells,
|
||||
_clearMeshFloorCellsForTest: clearMeshFloorCells,
|
||||
cachedGroundFloor,
|
||||
FLOOR_RESOLVE_DEADLINE_MS,
|
||||
resolveGroundFloorCellsBounded,
|
||||
|
||||
@@ -42,6 +42,7 @@ export function createStandaloneApplication({
|
||||
allowQaRegistration = false,
|
||||
engineKind = pageEngineKind(),
|
||||
rendererCache,
|
||||
layerSession,
|
||||
engineSwitch = false,
|
||||
}) {
|
||||
if (owner)
|
||||
@@ -89,6 +90,7 @@ export function createStandaloneApplication({
|
||||
loaderStatus,
|
||||
engineKind,
|
||||
rendererCache,
|
||||
layerSession,
|
||||
});
|
||||
// The catalog of this engine: on Navara only the layers with a Navara
|
||||
// renderer (src/app/layers/renderers.js; none yet), the others' state
|
||||
@@ -108,6 +110,7 @@ export function createStandaloneApplication({
|
||||
: {};
|
||||
catalog = createStandaloneCatalog({
|
||||
engineKind,
|
||||
layerSession,
|
||||
sources: { ...sources, ...qaSources },
|
||||
streetLevelProviders,
|
||||
nepalBoundaryResolver: (signal) =>
|
||||
@@ -132,7 +135,7 @@ export function createStandaloneApplication({
|
||||
catalog,
|
||||
}),
|
||||
createData: (context) =>
|
||||
createStandaloneData({ ...context, allowQaRegistration, catalog }),
|
||||
createStandaloneData({ ...context, allowQaRegistration, catalog, layerSession }),
|
||||
createTools: async (context) => {
|
||||
const tools = await createStandaloneTools({
|
||||
...context,
|
||||
|
||||
@@ -8,6 +8,7 @@ export { createStandaloneReferenceSources } from './layerSources.js';
|
||||
export function createStandaloneCatalog({
|
||||
nepalBoundaryResolver,
|
||||
engineKind = 'cesium',
|
||||
layerSession,
|
||||
sources = {},
|
||||
streetLevelProviders,
|
||||
signal = new AbortController().signal,
|
||||
@@ -18,6 +19,7 @@ export function createStandaloneCatalog({
|
||||
} = {}) {
|
||||
return createApplicationCatalog({
|
||||
engineKind,
|
||||
layerSession,
|
||||
nepalBoundaryResolver,
|
||||
surface,
|
||||
sources: createStandaloneLayerSources(sources),
|
||||
|
||||
@@ -37,7 +37,7 @@ export class CockpitCoordinator {
|
||||
operations,
|
||||
);
|
||||
this._cockpitDisplayPortal = null;
|
||||
this.cockpitView = viewer || engine?.kind === 'navara'
|
||||
this.cockpitView = viewer || (engine?.frames?.preUpdate && engine?.camera?.followChanged)
|
||||
? this._createCockpitView(viewer, services)
|
||||
: null;
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export class CockpitCoordinator {
|
||||
this._stampNavigation({ cancelPendingSelection: false }),
|
||||
isEntryAllowed: () =>
|
||||
cockpitEntryAllowed({
|
||||
standaloneAircraft: this.engine?.kind === 'navara',
|
||||
standaloneAircraft: true,
|
||||
contextMode: this._contextMode,
|
||||
contextModeChanging: this._contextModeChanging,
|
||||
flightsEnabled: !!this._dataManager?.isEnabled('flights'),
|
||||
@@ -279,7 +279,7 @@ export class CockpitCoordinator {
|
||||
action: 'control_cockpit',
|
||||
error: this._contextModeChanging
|
||||
? 'Contacts is still starting up — try Cockpit again in a moment'
|
||||
: this.engine?.kind === 'navara'
|
||||
: !this._contextMode
|
||||
? 'Enable an aircraft layer and track a flight to enter Cockpit'
|
||||
: 'Contacts must be active to enter Cockpit — say "open contacts" first',
|
||||
state: this.getCockpitState(),
|
||||
|
||||
@@ -254,7 +254,7 @@ export class NavigationController {
|
||||
});
|
||||
}
|
||||
|
||||
_reassertNavigationHandoff(generation, { trackingOrigin = 'tool' } = {}) {
|
||||
_reassertNavigationHandoff(generation, { trackingOrigin = 'tool', keepTracking = false } = {}) {
|
||||
return reassertNavigationHandoff({
|
||||
generation,
|
||||
currentGeneration: this._navigationGeneration,
|
||||
@@ -266,7 +266,7 @@ export class NavigationController {
|
||||
// was refused by the cockpit leaves the old readout standing.
|
||||
release: () => {
|
||||
this.clearLocation();
|
||||
return this._releaseFollowCamera({ trackingOrigin });
|
||||
if (!keepTracking) return this._releaseFollowCamera({ trackingOrigin });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,12 +174,17 @@ export class ShareRestoration {
|
||||
this._initialShareRestoreTimeout = null;
|
||||
if (this._disposed) return;
|
||||
const generation = this._initialShareNavigationGeneration;
|
||||
const retained = engineSwitch && this._layerStateCoordinator?.retainedTrackingTarget();
|
||||
const applyCamera =
|
||||
Number.isInteger(generation) &&
|
||||
this.navigation._reassertNavigationHandoff(generation);
|
||||
this.navigation._reassertNavigationHandoff(generation, {
|
||||
trackingOrigin: engineSwitch ? 'share-restore' : 'tool',
|
||||
keepTracking: Boolean(retained),
|
||||
});
|
||||
void this._runShareRestore(savedState, {
|
||||
applyCamera,
|
||||
instant: engineSwitch,
|
||||
retained,
|
||||
navigationToken: generation,
|
||||
layersPromise: this._layerStateRestorePromise,
|
||||
settle: (result) => this._settleInitialShareRestore(result),
|
||||
@@ -238,7 +243,7 @@ export class ShareRestoration {
|
||||
*/
|
||||
async _runShareRestore(
|
||||
state,
|
||||
{ applyCamera, instant = false, navigationToken, layersPromise, settle },
|
||||
{ applyCamera, instant = false, retained, navigationToken, layersPromise, settle },
|
||||
) {
|
||||
const generation = this._shareRestoreGeneration;
|
||||
const current = () =>
|
||||
@@ -253,7 +258,8 @@ export class ShareRestoration {
|
||||
if (!current()) return;
|
||||
const tracking =
|
||||
share.camera === 'applied'
|
||||
? await this._layerStateCoordinator?.restoreShareTrackingSelection?.()
|
||||
? (retained && this._layerStateCoordinator.resumeRetainedTracking(retained)) ||
|
||||
await this._layerStateCoordinator?.restoreShareTrackingSelection?.()
|
||||
: {
|
||||
status: 'superseded',
|
||||
cleared:
|
||||
|
||||
Reference in new issue
Block a user