mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 14:52:19 +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
@@ -67,6 +67,8 @@ try {
|
|||||||
await page.waitForSelector('#globe-engine', { visible: true });
|
await page.waitForSelector('#globe-engine', { visible: true });
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.__switchDocument = document;
|
window.__switchDocument = document;
|
||||||
|
window.__quakeEntry = window.__godsEyeView.dataManager.layers.get('earthquakes');
|
||||||
|
window.__quakeTimer = window.__quakeEntry.intervalId;
|
||||||
window.__renderers ??= new Map();
|
window.__renderers ??= new Map();
|
||||||
window.__renderers.set(
|
window.__renderers.set(
|
||||||
window.__godsEyeView.engine.kind,
|
window.__godsEyeView.engine.kind,
|
||||||
@@ -93,6 +95,8 @@ try {
|
|||||||
const previous = window.__renderers.get(engine.kind);
|
const previous = window.__renderers.get(engine.kind);
|
||||||
return (
|
return (
|
||||||
!window.__switchSplash &&
|
!window.__switchSplash &&
|
||||||
|
window.__quakeEntry === window.__godsEyeView.dataManager.layers.get('earthquakes') &&
|
||||||
|
window.__quakeTimer === window.__quakeEntry.intervalId &&
|
||||||
(!previous || previous === engine) &&
|
(!previous || previous === engine) &&
|
||||||
!window.__outgoingEngine.isDestroyed() &&
|
!window.__outgoingEngine.isDestroyed() &&
|
||||||
!window.__outgoingEngine.host.canvas.isConnected &&
|
!window.__outgoingEngine.host.canvas.isConnected &&
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||||
|
|
||||||
|
const url = new URL(process.argv[2]);
|
||||||
|
if (
|
||||||
|
!['localhost', '127.0.0.1'].includes(url.hostname) ||
|
||||||
|
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||||
|
url.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw new Error('Supply an unreserved local keyless preview URL');
|
||||||
|
url.search = '?engine=navara&welcome=0';
|
||||||
|
url.hash =
|
||||||
|
'v=2&lat=40.2539&lon=-125.5779&alt=21130&heading=360&pitch=-35&map=sentinel2&l=f&lo=f.e.1_f.t.71be26';
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--use-angle=metal', '--enable-gpu'],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await blockPaidQaMaps(page);
|
||||||
|
const errors = [];
|
||||||
|
page.on('pageerror', (e) => errors.push(e.message));
|
||||||
|
await page.setViewport({ width: 1280, height: 720 });
|
||||||
|
await page.evaluateOnNewDocument(() => {
|
||||||
|
const fetch = window.fetch.bind(window);
|
||||||
|
window.__flightPolls = 0;
|
||||||
|
window.fetch = (input, options) => {
|
||||||
|
const path = new URL(input.url ?? input, document.baseURI).pathname;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
if (path === '/api/flights') {
|
||||||
|
window.__flightPolls++;
|
||||||
|
return Promise.resolve(
|
||||||
|
Response.json({
|
||||||
|
time: now,
|
||||||
|
states: [
|
||||||
|
[
|
||||||
|
'71be26',
|
||||||
|
'GEV123',
|
||||||
|
'QA',
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
-125.5779,
|
||||||
|
40.2539,
|
||||||
|
9700,
|
||||||
|
false,
|
||||||
|
200,
|
||||||
|
45,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
9700,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (path === '/api/military')
|
||||||
|
return Promise.resolve(Response.json({ now: Date.now(), ac: [] }));
|
||||||
|
if (path === '/api/flights/track')
|
||||||
|
return Promise.resolve(Response.json({ path: [] }));
|
||||||
|
if (path.startsWith('/api/flights/') || path.startsWith('/api/openai/'))
|
||||||
|
return Promise.resolve(Response.json({}));
|
||||||
|
return fetch(input, options);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||||
|
const ready = (kind) =>
|
||||||
|
page.waitForFunction(
|
||||||
|
(engine) =>
|
||||||
|
window.__godsEyeView?.engine.kind === engine &&
|
||||||
|
!document.querySelector('#globe-engine')?.disabled &&
|
||||||
|
document.querySelector('#loading-screen.hidden') &&
|
||||||
|
!document.querySelector('.engine-switch-frame'),
|
||||||
|
{ timeout: 90000 },
|
||||||
|
kind,
|
||||||
|
);
|
||||||
|
await ready('navara');
|
||||||
|
await page.evaluate(
|
||||||
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
|
);
|
||||||
|
await page.waitForSelector('#cockpit-entry', { visible: true });
|
||||||
|
await page.click('#cockpit-entry');
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => window.__godsEyeView.styleManager.getCockpitState().active,
|
||||||
|
);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const { dataManager, styleManager, tileset } = window.__godsEyeView;
|
||||||
|
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
|
||||||
|
throw Error('Keyless preview required');
|
||||||
|
styleManager.cockpitView.setVisionMode('thermal');
|
||||||
|
const entry = dataManager.layers.get('flights');
|
||||||
|
window.__handoff = {
|
||||||
|
dataManager,
|
||||||
|
entry,
|
||||||
|
timer: entry.intervalId,
|
||||||
|
model: entry.module,
|
||||||
|
pollCount: window.__flightPolls,
|
||||||
|
timestamp: entry.module.getStats().lastUpdate,
|
||||||
|
started: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
for (const kind of ['cesium', 'navara']) {
|
||||||
|
await page.select('#globe-engine', kind);
|
||||||
|
await ready(kind);
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const { dataManager, engine, styleManager } = window.__godsEyeView;
|
||||||
|
const before = window.__handoff;
|
||||||
|
const entry = dataManager.layers.get('flights');
|
||||||
|
const cockpit = styleManager.getCockpitState();
|
||||||
|
return {
|
||||||
|
sameOwner:
|
||||||
|
before.dataManager === dataManager &&
|
||||||
|
before.entry === entry &&
|
||||||
|
before.model === entry.module,
|
||||||
|
sameTimer:
|
||||||
|
before.timer === entry.intervalId && entry.intervalId != null,
|
||||||
|
sourcePolls: window.__flightPolls - before.pollCount,
|
||||||
|
elapsed: Date.now() - before.started,
|
||||||
|
stamp: entry.module.getStats().lastUpdate,
|
||||||
|
initialStamp: before.timestamp,
|
||||||
|
id: cockpit.subject?.id,
|
||||||
|
active: cockpit.active,
|
||||||
|
mode: cockpit.visionMode,
|
||||||
|
hud: !document.querySelector('#cockpit-hud').hidden,
|
||||||
|
drawn: entry.module.qaDrawn().some((row) => row.id === '71be26'),
|
||||||
|
height: engine.camera.height(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert.ok(
|
||||||
|
result.sameOwner &&
|
||||||
|
result.sameTimer &&
|
||||||
|
result.active &&
|
||||||
|
result.hud &&
|
||||||
|
result.drawn,
|
||||||
|
JSON.stringify(result),
|
||||||
|
);
|
||||||
|
assert.equal(result.id, '71be26');
|
||||||
|
assert.equal(result.mode, 'thermal');
|
||||||
|
if (result.elapsed < 30000) {
|
||||||
|
assert.equal(result.sourcePolls, 0);
|
||||||
|
assert.equal(result.stamp, result.initialStamp);
|
||||||
|
}
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => Math.abs(window.__godsEyeView.engine.camera.height() - 9700) < 1000,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`PASS ${kind} cockpit, aircraft, HUD, manager and timer (${result.sourcePolls} polls since entry)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await page.evaluate(() =>
|
||||||
|
window.__godsEyeView.styleManager.controlCockpit('exit'),
|
||||||
|
);
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => !window.__godsEyeView.styleManager.getCockpitState().active,
|
||||||
|
);
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
+33
-11
@@ -66,6 +66,7 @@ export function createApplicationCatalog({
|
|||||||
metadata = APPLICATION_LAYER_METADATA,
|
metadata = APPLICATION_LAYER_METADATA,
|
||||||
vesselOptions,
|
vesselOptions,
|
||||||
resolveAsset,
|
resolveAsset,
|
||||||
|
layerSession,
|
||||||
nepalBoundaryResolver,
|
nepalBoundaryResolver,
|
||||||
streetLevelProviders,
|
streetLevelProviders,
|
||||||
engineKind = 'cesium',
|
engineKind = 'cesium',
|
||||||
@@ -79,30 +80,48 @@ export function createApplicationCatalog({
|
|||||||
|
|
||||||
const sourceComposition = resolveCatalogSources(sources);
|
const sourceComposition = resolveCatalogSources(sources);
|
||||||
sources = sourceComposition.sources;
|
sources = sourceComposition.sources;
|
||||||
const militaryRegistry = createMilitaryRegistry();
|
const militaryRegistry =
|
||||||
|
layerSession?.militaryRegistry ?? createMilitaryRegistry();
|
||||||
const weatherClock = createWeatherClock();
|
const weatherClock = createWeatherClock();
|
||||||
const dispose = () => {
|
const dispose = () => {
|
||||||
signal.removeEventListener('abort', dispose);
|
signal.removeEventListener('abort', dispose);
|
||||||
militaryRegistry.dispose();
|
if (!layerSession) militaryRegistry.dispose();
|
||||||
weatherClock.destroy();
|
weatherClock.destroy();
|
||||||
};
|
};
|
||||||
signal.addEventListener('abort', dispose, { once: true });
|
signal.addEventListener('abort', dispose, { once: true });
|
||||||
try {
|
try {
|
||||||
if (sourceComposition.isConfigured('military'))
|
if (!layerSession?.militaryRegistry) {
|
||||||
militaryRegistry.configureSource(sources.military, { signal });
|
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,
|
// Shared instances (awareness and launches read others) are made once,
|
||||||
// when the first layer that needs them is built.
|
// when the first layer that needs them is built.
|
||||||
const once = (create) => {
|
const once = (create) => {
|
||||||
let value;
|
let value;
|
||||||
return () => (value ??= create());
|
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(() =>
|
const flights = once(() =>
|
||||||
createApplicationFlights({
|
sessionModel('flights', () =>
|
||||||
surface,
|
createApplicationFlights({
|
||||||
source: sources.flights,
|
surface,
|
||||||
militaryRegistry,
|
source: sources.flights,
|
||||||
resolveAsset,
|
militaryRegistry,
|
||||||
}),
|
resolveAsset,
|
||||||
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const military = once(() =>
|
const military = once(() =>
|
||||||
createApplicationMilitary({
|
createApplicationMilitary({
|
||||||
@@ -158,7 +177,10 @@ export function createApplicationCatalog({
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'earthquakes',
|
'earthquakes',
|
||||||
() => createApplicationEarthquakes({ source: sources.earthquakes }),
|
() =>
|
||||||
|
sessionModel('earthquakes', () =>
|
||||||
|
createApplicationEarthquakes({ source: sources.earthquakes }),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'air-quality',
|
'air-quality',
|
||||||
|
|||||||
+9
-4
@@ -8,12 +8,13 @@ export function createApplicationData({
|
|||||||
controls: { styleManager },
|
controls: { styleManager },
|
||||||
catalog,
|
catalog,
|
||||||
allowQaRegistration,
|
allowQaRegistration,
|
||||||
|
layerSession,
|
||||||
onData,
|
onData,
|
||||||
defer,
|
defer,
|
||||||
}) {
|
}) {
|
||||||
// Initialize data layer manager
|
// Initialize data layer manager
|
||||||
// The modules get { signal, engine } on every call (viewer null on Navara).
|
// 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,
|
engine,
|
||||||
allowQaRegistration,
|
allowQaRegistration,
|
||||||
// A panel's host may report it hidden while it is on screen; its layers
|
// 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),
|
visibilityTarget: panelAwareVisibility(document),
|
||||||
getSourceAvailability: catalog?.getSourceAvailability,
|
getSourceAvailability: catalog?.getSourceAvailability,
|
||||||
});
|
});
|
||||||
|
if (layerSession) layerSession.manager = dataManager;
|
||||||
defer(async () => {
|
defer(async () => {
|
||||||
|
if (layerSession && !layerSession.lifetime.signal.aborted) {
|
||||||
|
await dataManager.detachScene();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await dataManager.destroyAll();
|
await dataManager.destroyAll();
|
||||||
if (dataManager.layers.size)
|
if (dataManager.layers.size)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -35,12 +41,11 @@ 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');
|
||||||
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.attachDataManager?.(dataManager);
|
||||||
for (const layer of catalog.layers)
|
for (const layer of catalog.layers)
|
||||||
layer.attachMapStackController?.(mapStackController);
|
layer.attachMapStackController?.(mapStackController);
|
||||||
// Restoration starts only after the caller's complete registry is sealed.
|
|
||||||
dataManager.finalizeRegistrations(catalog.metadata);
|
|
||||||
if (allowQaRegistration) {
|
if (allowQaRegistration) {
|
||||||
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
||||||
if (targetManager !== dataManager)
|
if (targetManager !== dataManager)
|
||||||
|
|||||||
+12
-2
@@ -29,12 +29,14 @@ export async function createApplicationScene({
|
|||||||
signal,
|
signal,
|
||||||
defer,
|
defer,
|
||||||
rendererCache,
|
rendererCache,
|
||||||
|
layerSession,
|
||||||
...options
|
...options
|
||||||
}) {
|
}) {
|
||||||
const operations = createApplicationOperations({
|
const operations = layerSession?.operations ?? createApplicationOperations({
|
||||||
requests: requestServices,
|
requests: requestServices,
|
||||||
signal,
|
signal: layerSession?.lifetime.signal ?? signal,
|
||||||
});
|
});
|
||||||
|
if (layerSession) layerSession.operations = operations;
|
||||||
defer(initLogoGaze());
|
defer(initLogoGaze());
|
||||||
const previousKey = window.__GOOGLE_MAPS_API_KEY__;
|
const previousKey = window.__GOOGLE_MAPS_API_KEY__;
|
||||||
if (googleApiKey) {
|
if (googleApiKey) {
|
||||||
@@ -55,6 +57,14 @@ export async function createApplicationScene({
|
|||||||
const { viewer, engine, tileset, mapStackController } = rendererCache
|
const { viewer, engine, tileset, mapStackController } = rendererCache
|
||||||
? await rendererCache.acquire(options.engineKind, build, defer)
|
? await rendererCache.acquire(options.engineKind, build, defer)
|
||||||
: await 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 };
|
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.
|
* first-person flight directly from a single selected aircraft layer.
|
||||||
*
|
*
|
||||||
* @param {object} input Current context and dependency visibility.
|
* @param {object} input Current context and dependency visibility.
|
||||||
|
|||||||
@@ -470,6 +470,36 @@ export class LayerStateCoordinator {
|
|||||||
return null;
|
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) {
|
_passivelyClearTrackingSelection(selected) {
|
||||||
const current =
|
const current =
|
||||||
this._durableState.options?.[selected.optionOwner]?.[selected.optionKey];
|
this._durableState.options?.[selected.optionOwner]?.[selected.optionKey];
|
||||||
|
|||||||
@@ -121,6 +121,50 @@ export class LayerLifecycle {
|
|||||||
this._registerLayer(layerModule);
|
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. */
|
/** Register a synthetic layer after sealing in an explicitly dev-enabled manager. */
|
||||||
registerForQa(layerModule) {
|
registerForQa(layerModule) {
|
||||||
if (!this._allowQaRegistration || !this._registrationsFinalized) {
|
if (!this._allowQaRegistration || !this._registrationsFinalized) {
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ export { createUsgsEarthquakeSource } from './source.js';
|
|||||||
/**
|
/**
|
||||||
* Own one earthquake display and its refresh lifecycle.
|
* Own one earthquake display and its refresh lifecycle.
|
||||||
* renderer.create({viewer, engine}) owns drawing and returns:
|
* 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
|
* 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 } = {}) {
|
export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
||||||
if (typeof source?.getSnapshot !== 'function')
|
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 (!overlayHost) throw new TypeError('Earthquakes require an overlay host');
|
||||||
if (typeof renderer?.create !== 'function')
|
if (typeof renderer?.create !== 'function')
|
||||||
throw new TypeError('Earthquakes require a renderer factory');
|
throw new TypeError('Earthquakes require a renderer factory');
|
||||||
let _viewer = null;
|
let initialized = false;
|
||||||
|
let observations = [];
|
||||||
|
let discs = [];
|
||||||
|
let labels = [];
|
||||||
let _request = null;
|
let _request = null;
|
||||||
let _renderer = null;
|
let _renderer = null;
|
||||||
let _count = 0;
|
let _count = 0;
|
||||||
@@ -34,6 +37,14 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
|||||||
let _lastError = null;
|
let _lastError = null;
|
||||||
let _enabled = false;
|
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 = {
|
const layer = {
|
||||||
id: 'earthquakes',
|
id: 'earthquakes',
|
||||||
name: 'Earthquakes (24h)',
|
name: 'Earthquakes (24h)',
|
||||||
@@ -42,17 +53,32 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
|||||||
updateInterval: 60000,
|
updateInterval: 60000,
|
||||||
|
|
||||||
init(viewer, { engine = null } = {}) {
|
init(viewer, { engine = null } = {}) {
|
||||||
if (_renderer) throw new Error('Earthquake layer is already initialized');
|
if (initialized) throw new Error('Earthquake layer is already initialized');
|
||||||
_viewer = viewer;
|
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 });
|
_renderer = renderer.create({ viewer, engine });
|
||||||
if (!_renderer) throw new Error('Earthquake renderer unavailable');
|
if (!_renderer) throw new Error('Earthquake renderer unavailable');
|
||||||
_renderer.setVisible(false);
|
_renderer.setVisible(_enabled);
|
||||||
_count = 0;
|
if (discs.length) _renderer.replace(discs);
|
||||||
_lastUpdate = null;
|
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, _enabled);
|
||||||
_lastError = null;
|
if (_enabled) publishLabels();
|
||||||
_enabled = false;
|
},
|
||||||
|
|
||||||
|
detachRenderer() {
|
||||||
|
overlayHost.clearSource(EARTHQUAKE_OVERLAY_SOURCE_ID);
|
||||||
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, false);
|
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, false);
|
||||||
console.log('[Data:Earthquakes] Initialized');
|
_renderer?.destroy();
|
||||||
|
_renderer = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
enable(viewer) {
|
enable(viewer) {
|
||||||
@@ -60,7 +86,10 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
|||||||
// No continuous-render hold: the discs are static geometry now, so the
|
// 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.
|
// layer has no per-frame animator to keep the render loop alive for.
|
||||||
_renderer?.setVisible(true);
|
_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) {
|
disable(viewer) {
|
||||||
@@ -73,7 +102,7 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async update(viewer) {
|
async update(viewer) {
|
||||||
if (!_enabled || !_renderer) return false;
|
if (!_enabled) return false;
|
||||||
_request?.abort();
|
_request?.abort();
|
||||||
const request = new AbortController();
|
const request = new AbortController();
|
||||||
_request = request;
|
_request = request;
|
||||||
@@ -128,17 +157,12 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderer.replace(nextDiscs);
|
observations = rows;
|
||||||
if (_enabled) {
|
discs = nextDiscs;
|
||||||
overlayHost.setEntries(
|
labels = selectEarthquakeOverlayCohort(overlayEntries);
|
||||||
EARTHQUAKE_OVERLAY_SOURCE_ID,
|
if (_renderer) {
|
||||||
selectEarthquakeOverlayCohort(overlayEntries),
|
_renderer.replace(discs);
|
||||||
{
|
publishLabels();
|
||||||
cohortLimit: EARTHQUAKE_OVERLAY_COHORT_LIMIT,
|
|
||||||
collisionCapacity: EARTHQUAKE_OVERLAY_COLLISION_CAPACITY,
|
|
||||||
moving: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_count = count;
|
_count = count;
|
||||||
@@ -157,15 +181,15 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
destroy(viewer = _viewer) {
|
destroy() {
|
||||||
_request?.abort();
|
_request?.abort();
|
||||||
_request = null;
|
_request = null;
|
||||||
_viewer = null;
|
initialized = false;
|
||||||
_enabled = false;
|
_enabled = false;
|
||||||
overlayHost.clearSource(EARTHQUAKE_OVERLAY_SOURCE_ID);
|
this.detachRenderer();
|
||||||
overlayHost.setVisible(EARTHQUAKE_OVERLAY_SOURCE_ID, false);
|
observations = [];
|
||||||
_renderer?.destroy();
|
discs = [];
|
||||||
_renderer = null;
|
labels = [];
|
||||||
_count = 0;
|
_count = 0;
|
||||||
_lastUpdate = null;
|
_lastUpdate = null;
|
||||||
_lastError = null;
|
_lastError = null;
|
||||||
@@ -180,11 +204,13 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
|
|||||||
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
|
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
|
||||||
*/
|
*/
|
||||||
getAnalystRecords(maxCount = 2000) {
|
getAnalystRecords(maxCount = 2000) {
|
||||||
if (!_enabled || !_renderer) return [];
|
if (!_enabled) return [];
|
||||||
const limit = Number.isFinite(maxCount)
|
const limit = Number.isFinite(maxCount)
|
||||||
? Math.max(1, Math.floor(maxCount))
|
? Math.max(1, Math.floor(maxCount))
|
||||||
: 2000;
|
: 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() {
|
getStats() {
|
||||||
|
|||||||
@@ -92,3 +92,28 @@ test('USGS body completion honors cancellation even with an uncooperative transp
|
|||||||
name: 'AbortError',
|
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 application;
|
||||||
let busy = false;
|
let busy = false;
|
||||||
const renderers = createRendererCache();
|
const renderers = createRendererCache();
|
||||||
|
const layerSession = { lifetime: new AbortController() };
|
||||||
window.addEventListener('pagehide', (event) => {
|
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;
|
kind = next;
|
||||||
application = createStandaloneApplication({
|
application = createStandaloneApplication({
|
||||||
googleApiKey: import.meta.env.GOOGLE_MAPS_API_KEY,
|
googleApiKey: import.meta.env.GOOGLE_MAPS_API_KEY,
|
||||||
@@ -27,11 +30,25 @@ async function start(next, engineSwitch = false) {
|
|||||||
allowQaRegistration: import.meta.env.DEV,
|
allowQaRegistration: import.meta.env.DEV,
|
||||||
engineKind: next,
|
engineKind: next,
|
||||||
rendererCache: renderers,
|
rendererCache: renderers,
|
||||||
|
layerSession,
|
||||||
engineSwitch,
|
engineSwitch,
|
||||||
});
|
});
|
||||||
const { scene, controls } = await application.start();
|
const { scene, controls } = await application.start();
|
||||||
if (engineSwitch) {
|
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);
|
await waitForGlobeFrame(scene.engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,12 +123,16 @@ async function reconcile(remember = false) {
|
|||||||
present();
|
present();
|
||||||
const previous = kind;
|
const previous = kind;
|
||||||
let cover;
|
let cover;
|
||||||
|
let cockpit;
|
||||||
try {
|
try {
|
||||||
while (pageEngineKind() !== kind) {
|
while (pageEngineKind() !== kind) {
|
||||||
cover ??= keepGlobeFrame(application.getComponents().scene.engine);
|
cover ??= keepGlobeFrame(application.getComponents().scene.engine);
|
||||||
|
cockpit = application
|
||||||
|
.getComponents()
|
||||||
|
.controls.styleManager.getCockpitState();
|
||||||
renderers.retain(kind);
|
renderers.retain(kind);
|
||||||
await application.destroy();
|
await application.destroy();
|
||||||
await start(pageEngineKind(), true);
|
await start(pageEngineKind(), true, cockpit);
|
||||||
}
|
}
|
||||||
if (remember) {
|
if (remember) {
|
||||||
try {
|
try {
|
||||||
@@ -127,7 +148,7 @@ async function reconcile(remember = false) {
|
|||||||
history.replaceState(history.state, '', url);
|
history.replaceState(history.state, '', url);
|
||||||
try {
|
try {
|
||||||
await application.destroy();
|
await application.destroy();
|
||||||
await start(previous, true);
|
await start(previous, true, cockpit);
|
||||||
application
|
application
|
||||||
.getComponents()
|
.getComponents()
|
||||||
.controls.styleManager._showToast(describeError(error));
|
.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). */
|
/** Test hook: clears mesh cells (never called at runtime — cells are valid all session). */
|
||||||
function _clearMeshFloorCellsForTest() {
|
function clearMeshFloorCells() {
|
||||||
_meshCells.clear();
|
_meshCells.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,7 +663,8 @@ export function createGroundFloor({ terrain, signal }) {
|
|||||||
meshFloorSampleWithinPrior,
|
meshFloorSampleWithinPrior,
|
||||||
reportValidatedMeshFloorCell,
|
reportValidatedMeshFloorCell,
|
||||||
cachedMeshFloor,
|
cachedMeshFloor,
|
||||||
_clearMeshFloorCellsForTest,
|
clearMeshFloorCells,
|
||||||
|
_clearMeshFloorCellsForTest: clearMeshFloorCells,
|
||||||
cachedGroundFloor,
|
cachedGroundFloor,
|
||||||
FLOOR_RESOLVE_DEADLINE_MS,
|
FLOOR_RESOLVE_DEADLINE_MS,
|
||||||
resolveGroundFloorCellsBounded,
|
resolveGroundFloorCellsBounded,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function createStandaloneApplication({
|
|||||||
allowQaRegistration = false,
|
allowQaRegistration = false,
|
||||||
engineKind = pageEngineKind(),
|
engineKind = pageEngineKind(),
|
||||||
rendererCache,
|
rendererCache,
|
||||||
|
layerSession,
|
||||||
engineSwitch = false,
|
engineSwitch = false,
|
||||||
}) {
|
}) {
|
||||||
if (owner)
|
if (owner)
|
||||||
@@ -89,6 +90,7 @@ export function createStandaloneApplication({
|
|||||||
loaderStatus,
|
loaderStatus,
|
||||||
engineKind,
|
engineKind,
|
||||||
rendererCache,
|
rendererCache,
|
||||||
|
layerSession,
|
||||||
});
|
});
|
||||||
// The catalog of this engine: on Navara only the layers with a Navara
|
// The catalog of this engine: on Navara only the layers with a Navara
|
||||||
// renderer (src/app/layers/renderers.js; none yet), the others' state
|
// renderer (src/app/layers/renderers.js; none yet), the others' state
|
||||||
@@ -108,6 +110,7 @@ export function createStandaloneApplication({
|
|||||||
: {};
|
: {};
|
||||||
catalog = createStandaloneCatalog({
|
catalog = createStandaloneCatalog({
|
||||||
engineKind,
|
engineKind,
|
||||||
|
layerSession,
|
||||||
sources: { ...sources, ...qaSources },
|
sources: { ...sources, ...qaSources },
|
||||||
streetLevelProviders,
|
streetLevelProviders,
|
||||||
nepalBoundaryResolver: (signal) =>
|
nepalBoundaryResolver: (signal) =>
|
||||||
@@ -132,7 +135,7 @@ export function createStandaloneApplication({
|
|||||||
catalog,
|
catalog,
|
||||||
}),
|
}),
|
||||||
createData: (context) =>
|
createData: (context) =>
|
||||||
createStandaloneData({ ...context, allowQaRegistration, catalog }),
|
createStandaloneData({ ...context, allowQaRegistration, catalog, layerSession }),
|
||||||
createTools: async (context) => {
|
createTools: async (context) => {
|
||||||
const tools = await createStandaloneTools({
|
const tools = await createStandaloneTools({
|
||||||
...context,
|
...context,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export { createStandaloneReferenceSources } from './layerSources.js';
|
|||||||
export function createStandaloneCatalog({
|
export function createStandaloneCatalog({
|
||||||
nepalBoundaryResolver,
|
nepalBoundaryResolver,
|
||||||
engineKind = 'cesium',
|
engineKind = 'cesium',
|
||||||
|
layerSession,
|
||||||
sources = {},
|
sources = {},
|
||||||
streetLevelProviders,
|
streetLevelProviders,
|
||||||
signal = new AbortController().signal,
|
signal = new AbortController().signal,
|
||||||
@@ -18,6 +19,7 @@ export function createStandaloneCatalog({
|
|||||||
} = {}) {
|
} = {}) {
|
||||||
return createApplicationCatalog({
|
return createApplicationCatalog({
|
||||||
engineKind,
|
engineKind,
|
||||||
|
layerSession,
|
||||||
nepalBoundaryResolver,
|
nepalBoundaryResolver,
|
||||||
surface,
|
surface,
|
||||||
sources: createStandaloneLayerSources(sources),
|
sources: createStandaloneLayerSources(sources),
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class CockpitCoordinator {
|
|||||||
operations,
|
operations,
|
||||||
);
|
);
|
||||||
this._cockpitDisplayPortal = null;
|
this._cockpitDisplayPortal = null;
|
||||||
this.cockpitView = viewer || engine?.kind === 'navara'
|
this.cockpitView = viewer || (engine?.frames?.preUpdate && engine?.camera?.followChanged)
|
||||||
? this._createCockpitView(viewer, services)
|
? this._createCockpitView(viewer, services)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ export class CockpitCoordinator {
|
|||||||
this._stampNavigation({ cancelPendingSelection: false }),
|
this._stampNavigation({ cancelPendingSelection: false }),
|
||||||
isEntryAllowed: () =>
|
isEntryAllowed: () =>
|
||||||
cockpitEntryAllowed({
|
cockpitEntryAllowed({
|
||||||
standaloneAircraft: this.engine?.kind === 'navara',
|
standaloneAircraft: true,
|
||||||
contextMode: this._contextMode,
|
contextMode: this._contextMode,
|
||||||
contextModeChanging: this._contextModeChanging,
|
contextModeChanging: this._contextModeChanging,
|
||||||
flightsEnabled: !!this._dataManager?.isEnabled('flights'),
|
flightsEnabled: !!this._dataManager?.isEnabled('flights'),
|
||||||
@@ -279,7 +279,7 @@ export class CockpitCoordinator {
|
|||||||
action: 'control_cockpit',
|
action: 'control_cockpit',
|
||||||
error: this._contextModeChanging
|
error: this._contextModeChanging
|
||||||
? 'Contacts is still starting up — try Cockpit again in a moment'
|
? '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'
|
? 'Enable an aircraft layer and track a flight to enter Cockpit'
|
||||||
: 'Contacts must be active to enter Cockpit — say "open contacts" first',
|
: 'Contacts must be active to enter Cockpit — say "open contacts" first',
|
||||||
state: this.getCockpitState(),
|
state: this.getCockpitState(),
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export class NavigationController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_reassertNavigationHandoff(generation, { trackingOrigin = 'tool' } = {}) {
|
_reassertNavigationHandoff(generation, { trackingOrigin = 'tool', keepTracking = false } = {}) {
|
||||||
return reassertNavigationHandoff({
|
return reassertNavigationHandoff({
|
||||||
generation,
|
generation,
|
||||||
currentGeneration: this._navigationGeneration,
|
currentGeneration: this._navigationGeneration,
|
||||||
@@ -266,7 +266,7 @@ export class NavigationController {
|
|||||||
// was refused by the cockpit leaves the old readout standing.
|
// was refused by the cockpit leaves the old readout standing.
|
||||||
release: () => {
|
release: () => {
|
||||||
this.clearLocation();
|
this.clearLocation();
|
||||||
return this._releaseFollowCamera({ trackingOrigin });
|
if (!keepTracking) return this._releaseFollowCamera({ trackingOrigin });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,12 +174,17 @@ export class ShareRestoration {
|
|||||||
this._initialShareRestoreTimeout = null;
|
this._initialShareRestoreTimeout = null;
|
||||||
if (this._disposed) return;
|
if (this._disposed) return;
|
||||||
const generation = this._initialShareNavigationGeneration;
|
const generation = this._initialShareNavigationGeneration;
|
||||||
|
const retained = engineSwitch && this._layerStateCoordinator?.retainedTrackingTarget();
|
||||||
const applyCamera =
|
const applyCamera =
|
||||||
Number.isInteger(generation) &&
|
Number.isInteger(generation) &&
|
||||||
this.navigation._reassertNavigationHandoff(generation);
|
this.navigation._reassertNavigationHandoff(generation, {
|
||||||
|
trackingOrigin: engineSwitch ? 'share-restore' : 'tool',
|
||||||
|
keepTracking: Boolean(retained),
|
||||||
|
});
|
||||||
void this._runShareRestore(savedState, {
|
void this._runShareRestore(savedState, {
|
||||||
applyCamera,
|
applyCamera,
|
||||||
instant: engineSwitch,
|
instant: engineSwitch,
|
||||||
|
retained,
|
||||||
navigationToken: generation,
|
navigationToken: generation,
|
||||||
layersPromise: this._layerStateRestorePromise,
|
layersPromise: this._layerStateRestorePromise,
|
||||||
settle: (result) => this._settleInitialShareRestore(result),
|
settle: (result) => this._settleInitialShareRestore(result),
|
||||||
@@ -238,7 +243,7 @@ export class ShareRestoration {
|
|||||||
*/
|
*/
|
||||||
async _runShareRestore(
|
async _runShareRestore(
|
||||||
state,
|
state,
|
||||||
{ applyCamera, instant = false, navigationToken, layersPromise, settle },
|
{ applyCamera, instant = false, retained, navigationToken, layersPromise, settle },
|
||||||
) {
|
) {
|
||||||
const generation = this._shareRestoreGeneration;
|
const generation = this._shareRestoreGeneration;
|
||||||
const current = () =>
|
const current = () =>
|
||||||
@@ -253,7 +258,8 @@ export class ShareRestoration {
|
|||||||
if (!current()) return;
|
if (!current()) return;
|
||||||
const tracking =
|
const tracking =
|
||||||
share.camera === 'applied'
|
share.camera === 'applied'
|
||||||
? await this._layerStateCoordinator?.restoreShareTrackingSelection?.()
|
? (retained && this._layerStateCoordinator.resumeRetainedTracking(retained)) ||
|
||||||
|
await this._layerStateCoordinator?.restoreShareTrackingSelection?.()
|
||||||
: {
|
: {
|
||||||
status: 'superseded',
|
status: 'superseded',
|
||||||
cleared:
|
cleared:
|
||||||
|
|||||||
Reference in new issue
Block a user