Format the files this branch wrote or broke with Prettier

This commit is contained in:
German Khokhlov committed 2026-11-03 23:02:04 +04:00
1 parent 227e3c2051
commit 8d7f055572
90 files changed
+1408 -640

No files matched your search

+66 -65
View File
@@ -38,71 +38,72 @@ export const stations = Array.from({ length: 750 }, (_, index) => {
}); });
export async function installRadioFixture(page) { export async function installRadioFixture(page) {
await page.evaluateOnNewDocument((stations) => { await page.evaluateOnNewDocument((stations) => {
localStorage.clear(); localStorage.clear();
localStorage.setItem('gev:first-run-mission:v1', 'suppressed'); localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
window.__qaRadio = { window.__qaRadio = {
stations, stations,
generation: 1, generation: 1,
plays: [], plays: [],
delay: 0, delay: 0,
response: 'normal', response: 'normal',
pending: null, pending: null,
}; };
HTMLMediaElement.prototype.play = function () { HTMLMediaElement.prototype.play = function () {
window.qaPlayer = this; window.qaPlayer = this;
window.__qaRadio.plays.push(this.src); window.__qaRadio.plays.push(this.src);
if (window.__qaRadio.delayPlay) { if (window.__qaRadio.delayPlay) {
window.__qaRadio.delayPlay = false; window.__qaRadio.delayPlay = false;
return new Promise((resolve) => { return new Promise((resolve) => {
window.__qaRadio.releasePlay = () => { window.__qaRadio.releasePlay = () => {
this.dispatchEvent(new Event('playing')); this.dispatchEvent(new Event('playing'));
resolve(); resolve();
}; };
});
}
queueMicrotask(() => this.dispatchEvent(new Event('playing')));
return Promise.resolve();
};
const fetch = window.fetch.bind(window);
window.fetch = async (input, init) => {
const address = String(
typeof input === 'string' || input instanceof URL ? input : input.url,
);
if (address.includes('/api/radio/stations')) {
const qa = window.__qaRadio;
qa.reads = (qa.reads || 0) + 1;
const response = {
stations: qa.response === 'empty' ? [] : qa.stations,
updatedAt: new Date().toISOString(),
stale: false,
degraded: false,
acceptedGeneration: qa.generation,
catalogInstance: 'w2-radio-fixture',
};
const mode = qa.response;
const delay = qa.delay;
qa.delay = 0;
if (delay)
await new Promise((resolve) => {
qa.pending = resolve;
}); });
init?.signal?.throwIfAborted(); }
return new Response( queueMicrotask(() => this.dispatchEvent(new Event('playing')));
mode === 'failure' ? '{}' : JSON.stringify(response), return Promise.resolve();
{ };
status: mode === 'failure' ? 503 : 200, const fetch = window.fetch.bind(window);
headers: { 'Content-Type': 'application/json' }, window.fetch = async (input, init) => {
}, const address = String(
typeof input === 'string' || input instanceof URL ? input : input.url,
); );
} if (address.includes('/api/radio/stations')) {
if (address.includes('/api/radio/click/')) const qa = window.__qaRadio;
return new Response(null, { status: 204 }); qa.reads = (qa.reads || 0) + 1;
if (address.includes('/api/flights')) return new Response('{"states":[]}'); const response = {
if (address.includes('/api/military')) return new Response('{"ac":[]}'); stations: qa.response === 'empty' ? [] : qa.stations,
if (address.includes('/api/vessels')) updatedAt: new Date().toISOString(),
return new Response('{"connected":false,"vessels":[]}'); stale: false,
return fetch(input, init); degraded: false,
}; acceptedGeneration: qa.generation,
}, stations); catalogInstance: 'w2-radio-fixture',
};
const mode = qa.response;
const delay = qa.delay;
qa.delay = 0;
if (delay)
await new Promise((resolve) => {
qa.pending = resolve;
});
init?.signal?.throwIfAborted();
return new Response(
mode === 'failure' ? '{}' : JSON.stringify(response),
{
status: mode === 'failure' ? 503 : 200,
headers: { 'Content-Type': 'application/json' },
},
);
}
if (address.includes('/api/radio/click/'))
return new Response(null, { status: 204 });
if (address.includes('/api/flights'))
return new Response('{"states":[]}');
if (address.includes('/api/military')) return new Response('{"ac":[]}');
if (address.includes('/api/vessels'))
return new Response('{"connected":false,"vessels":[]}');
return fetch(input, init);
};
}, stations);
} }
+191 -98
View File
@@ -86,7 +86,11 @@ export function createApplicationCatalog({
const weatherClock = layerSession?.weatherClock ?? createWeatherClock(); const weatherClock = layerSession?.weatherClock ?? createWeatherClock();
if (layerSession && !layerSession.weatherClock) { if (layerSession && !layerSession.weatherClock) {
layerSession.weatherClock = weatherClock; layerSession.weatherClock = weatherClock;
layerSession.lifetime.signal.addEventListener('abort', weatherClock.destroy, { once: true }); layerSession.lifetime.signal.addEventListener(
'abort',
weatherClock.destroy,
{ once: true },
);
} }
const dispose = () => { const dispose = () => {
signal.removeEventListener('abort', dispose); signal.removeEventListener('abort', dispose);
@@ -98,7 +102,9 @@ export function createApplicationCatalog({
if (!layerSession?.militaryRegistry) { if (!layerSession?.militaryRegistry) {
const lifetime = layerSession?.lifetime.signal ?? signal; const lifetime = layerSession?.lifetime.signal ?? signal;
if (sourceComposition.isConfigured('military')) if (sourceComposition.isConfigured('military'))
militaryRegistry.configureSource(sources.military, { signal: lifetime }); militaryRegistry.configureSource(sources.military, {
signal: lifetime,
});
if (layerSession) { if (layerSession) {
layerSession.militaryRegistry = militaryRegistry; layerSession.militaryRegistry = militaryRegistry;
lifetime.addEventListener('abort', militaryRegistry.dispose, { lifetime.addEventListener('abort', militaryRegistry.dispose, {
@@ -129,12 +135,14 @@ export function createApplicationCatalog({
), ),
); );
const military = once(() => const military = once(() =>
sessionModel('military', () => createApplicationMilitary({ sessionModel('military', () =>
surface, createApplicationMilitary({
source: sources.military, surface,
militaryRegistry, source: sources.military,
resolveAsset, militaryRegistry,
})), resolveAsset,
}),
),
); );
const vessels = once(() => const vessels = once(() =>
sessionModel('ais-live-vessels', () => sessionModel('ais-live-vessels', () =>
@@ -153,39 +161,53 @@ export function createApplicationCatalog({
), ),
); );
const satellites = once(() => const satellites = once(() =>
sessionModel('satellites', () => createApplicationSatellites({ sessionModel('satellites', () =>
source: sources.satellites, createApplicationSatellites({
})), source: sources.satellites,
}),
),
); );
// Catalog order: [layer id(s), construction]. Only the layers the // Catalog order: [layer id(s), construction]. Only the layers the
// engine can show are constructed (renderers.js). // engine can show are constructed (renderers.js).
const entries = [ const entries = [
['bhote-koshi-2026', () => sessionModel('bhote-koshi-2026', () => createBhoteKoshiEventLayer({ [
createNativeRenderer: (engine) => rendererFor('bhote-koshi-2026', engine), 'bhote-koshi-2026',
}))], () =>
sessionModel('bhote-koshi-2026', () =>
createBhoteKoshiEventLayer({
createNativeRenderer: (engine) =>
rendererFor('bhote-koshi-2026', engine),
}),
),
],
[ [
'bhote-koshi-locator', 'bhote-koshi-locator',
() => () =>
sessionModel('bhote-koshi-locator', () => createBhoteKoshiLocatorLayer({ sessionModel('bhote-koshi-locator', () =>
boundaryResolver: nepalBoundaryResolver, createBhoteKoshiLocatorLayer({
createNativeRenderer: (engine, data) => rendererFor('bhote-koshi-locator', engine, { data }), boundaryResolver: nepalBoundaryResolver,
})), createNativeRenderer: (engine, data) =>
rendererFor('bhote-koshi-locator', engine, { data }),
}),
),
], ],
['flights', flights], ['flights', flights],
['military', military], ['military', military],
[ [
'local-adsb', 'local-adsb',
() => () =>
sessionModel('local-adsb', () => createApplicationLocalAdsb({ sessionModel('local-adsb', () =>
surface, createApplicationLocalAdsb({
// QA fixture receivers (?qaFeed=local-adsb:fixture, dev only). surface,
...(sources.localAdsbFeeds // QA fixture receivers (?qaFeed=local-adsb:fixture, dev only).
? { feeds: sources.localAdsbFeeds } ...(sources.localAdsbFeeds
: {}), ? { feeds: sources.localAdsbFeeds }
enrichment: sources.flights, : {}),
displayParams: () => flights().getParams(), enrichment: sources.flights,
...(resolveAsset ? { resolveAsset } : {}), displayParams: () => flights().getParams(),
})), ...(resolveAsset ? { resolveAsset } : {}),
}),
),
], ],
[ [
'earthquakes', 'earthquakes',
@@ -196,144 +218,215 @@ export function createApplicationCatalog({
], ],
[ [
'air-quality', 'air-quality',
() => sessionModel('air-quality', () => createApplicationAirQuality({ source: sources['air-quality'] })), () =>
sessionModel('air-quality', () =>
createApplicationAirQuality({ source: sources['air-quality'] }),
),
], ],
[ [
'fire-perimeters', 'fire-perimeters',
() => () =>
sessionModel('fire-perimeters', () => createApplicationFirePerimeters({ sessionModel('fire-perimeters', () =>
source: sources['fire-perimeters'], createApplicationFirePerimeters({
})), source: sources['fire-perimeters'],
}),
),
], ],
[ [
'gnss-integrity', 'gnss-integrity',
() => () =>
sessionModel('gnss-integrity', () => createApplicationGnssIntegrity({ sessionModel('gnss-integrity', () =>
source: sources['gnss-integrity'], createApplicationGnssIntegrity({
windowMs: sources['gnss-integrity'].windowMs, source: sources['gnss-integrity'],
})), windowMs: sources['gnss-integrity'].windowMs,
}),
),
], ],
[ [
's2-ghostmaps', 's2-ghostmaps',
() => sessionModel('s2-ghostmaps', () => createApplicationGhostMaps({ source: sources['s2-ghostmaps'] })), () =>
sessionModel('s2-ghostmaps', () =>
createApplicationGhostMaps({ source: sources['s2-ghostmaps'] }),
),
], ],
[ [
'alpr-cameras', 'alpr-cameras',
() => sessionModel('alpr-cameras', () => createApplicationAlpr({ surface, source: sources.alpr })), () =>
sessionModel('alpr-cameras', () =>
createApplicationAlpr({ surface, source: sources.alpr }),
),
], ],
[ [
'street-level', 'street-level',
() => () =>
sessionModel('street-level', () => createApplicationStreetLevel({ sessionModel('street-level', () =>
surface, createApplicationStreetLevel({
nativeRenderer:engine=>rendererFor('street-level',engine), surface,
sources: { mapillary: sources.mapillary }, nativeRenderer: (engine) => rendererFor('street-level', engine),
providers: streetLevelProviders, sources: { mapillary: sources.mapillary },
})), providers: streetLevelProviders,
}),
),
], ],
['satellites', satellites], ['satellites', satellites],
[ [
'rocket-launches', 'rocket-launches',
() => () =>
sessionModel('rocket-launches', () => createApplicationLaunches({ sessionModel('rocket-launches', () =>
source: sources.launches, createApplicationLaunches({
satellites: satellites(), source: sources.launches,
})), satellites: satellites(),
}),
),
], ],
[ [
'traffic', 'traffic',
() => sessionModel('traffic', () => createApplicationTraffic({ () =>
source: sources.traffic, surface, nativeRenderer: engine => rendererFor('traffic',engine), sessionModel('traffic', () =>
})), createApplicationTraffic({
source: sources.traffic,
surface,
nativeRenderer: (engine) => rendererFor('traffic', engine),
}),
),
],
[
'cctv',
() =>
sessionModel('cctv', () =>
createApplicationCctv({ surface, source: sources.cctv }),
),
], ],
['cctv', () => sessionModel('cctv', () => createApplicationCctv({ surface, source: sources.cctv }))],
[ [
'radio', 'radio',
() => sessionModel('radio', () => createApplicationRadio({ surface, source: sources.radio })), () =>
sessionModel('radio', () =>
createApplicationRadio({ surface, source: sources.radio }),
),
], ],
[ [
'transit', 'transit',
() => sessionModel('transit', () => createApplicationTransit({ surface, source: sources.transit })), () =>
sessionModel('transit', () =>
createApplicationTransit({ surface, source: sources.transit }),
),
], ],
[ [
'bikeshare', 'bikeshare',
() => sessionModel('bikeshare', () => createApplicationBikeshare({ source: sources.bikeshare })), () =>
sessionModel('bikeshare', () =>
createApplicationBikeshare({ source: sources.bikeshare }),
),
], ],
[ [
'directions', 'directions',
() => sessionModel('directions', () => createApplicationDirections({ source: sources.directions, surface })), () =>
sessionModel('directions', () =>
createApplicationDirections({
source: sources.directions,
surface,
}),
),
], ],
[ [
'recent-imagery', 'recent-imagery',
() => () =>
sessionModel('recent-imagery', () => createApplicationRecentImagery({ sessionModel('recent-imagery', () =>
source: sources['recent-imagery'], createApplicationRecentImagery({
})), source: sources['recent-imagery'],
}),
),
], ],
['ais-live-vessels', vessels], ['ais-live-vessels', vessels],
['military-installations', installations], ['military-installations', installations],
[ [
'military-awareness', 'military-awareness',
() => sessionModel('military-awareness', () => () =>
createApplicationAwareness({ sessionModel('military-awareness', () =>
flights: flights(), createApplicationAwareness({
military: military(), flights: flights(),
vessels: vessels(), military: military(),
installations: installations(), vessels: vessels(),
}), installations: installations(),
), }),
),
], ],
[ [
'wind', 'wind',
() => sessionModel('wind', () => createWindLayer({ () =>
feed: sources.wind, sessionModel('wind', () =>
clock: weatherClock, createWindLayer({
createNativeRendering: (engine) => rendererFor('wind', engine), feed: sources.wind,
})), clock: weatherClock,
createNativeRendering: (engine) => rendererFor('wind', engine),
}),
),
], ],
...['weather-radar', 'weather-satellite', 'weather-lightning'].map( ...['weather-radar', 'weather-satellite', 'weather-lightning'].map(
(id) => [ (id) => [
id, id,
() => sessionModel(id, () => () =>
createWeatherLayer({ sessionModel(id, () =>
feed: sources.weather, createWeatherLayer({
id, feed: sources.weather,
clock: weatherClock, id,
createNativeRendering: (engine, options) => rendererFor(id, engine, options), clock: weatherClock,
}), createNativeRendering: (engine, options) =>
), rendererFor(id, engine, options),
}),
),
], ],
), ),
[ [
'weather-cyclones', 'weather-cyclones',
() => sessionModel('weather-cyclones', () => createCyclonesLayer({ () =>
feed: sources.cyclones, sessionModel('weather-cyclones', () =>
createNativeRendering: (engine, options) => rendererFor('weather-cyclones', engine, options), createCyclonesLayer({
})), feed: sources.cyclones,
createNativeRendering: (engine, options) =>
rendererFor('weather-cyclones', engine, options),
}),
),
], ],
[ [
['local-datacenters', 'local-dams'], ['local-datacenters', 'local-dams'],
() => sessionModel('local-infrastructure', () => () =>
createInfrastructureLayers(localGeoJsonServices, (config, services) => sessionModel('local-infrastructure', () =>
createInfrastructureSessionLayer(config, services, (engine, descriptor, ops) => createInfrastructureLayers(
rendererFor(config.id,engine,{config:descriptor,services:ops}))), localGeoJsonServices,
), (config, services) =>
createInfrastructureSessionLayer(
config,
services,
(engine, descriptor, ops) =>
rendererFor(config.id, engine, {
config: descriptor,
services: ops,
}),
),
),
),
], ],
[ [
'telegeography-submarine-cables', 'telegeography-submarine-cables',
() => sessionModel('telegeography-submarine-cables', () => createApplicationCables({ source: sources.cables, session: true })), () =>
sessionModel('telegeography-submarine-cables', () =>
createApplicationCables({ source: sources.cables, session: true }),
),
], ],
[ [
'local-firms', 'local-firms',
() => () =>
sessionModel('local-firms', () => createApplicationFirms({ sessionModel('local-firms', () =>
surface, createApplicationFirms({
id: 'local-firms', surface,
name: 'FIRMS Active Fires', id: 'local-firms',
icon: '▲', name: 'FIRMS Active Fires',
source: 'NASA FIRMS · LIVE', icon: '▲',
feed: sources.firms, source: 'NASA FIRMS · LIVE',
})), feed: sources.firms,
}),
),
], ],
]; ];
const available = (id) => availability.isAvailable(id); const available = (id) => availability.isAvailable(id);
+5 -1
View File
@@ -4,5 +4,9 @@ import { rendererFor } from './renderers.js';
/** Construct Directions with application scene owners and an explicit route source. */ /** Construct Directions with application scene owners and an explicit route source. */
export function createApplicationDirections({ source, surface }) { export function createApplicationDirections({ source, surface }) {
return createApplicationDirectionsLayer({ source, groundFloor: surface?.groundFloor, createNativeRenderer: (engine) => rendererFor('directions', engine) }); return createApplicationDirectionsLayer({
source,
groundFloor: surface?.groundFloor,
createNativeRenderer: (engine) => rendererFor('directions', engine),
});
} }
+4 -3
View File
@@ -5,9 +5,10 @@ import { createGnssIntegrityLayer } from '../../layers/gnss/index.js';
/** Wire the adsb.lol GNSS navigation-integrity layer into the application catalog. */ /** Wire the adsb.lol GNSS navigation-integrity layer into the application catalog. */
export function createApplicationGnssIntegrity(options) { export function createApplicationGnssIntegrity(options) {
return createGnssIntegrityLayer({ return createGnssIntegrityLayer({
createRenderer: ({ viewer, engine, viewAnchor }) => engine createRenderer: ({ viewer, engine, viewAnchor }) =>
? rendererFor('gnss-integrity', engine, { viewer, viewAnchor }) engine
: createGnssCesiumRenderer({ viewer, viewAnchor }), ? rendererFor('gnss-integrity', engine, { viewer, viewAnchor })
: createGnssCesiumRenderer({ viewer, viewAnchor }),
...options, ...options,
}); });
} }
+10 -3
View File
@@ -24,9 +24,16 @@ export function createApplicationFirePerimeters(options) {
openExternal: (url) => window.open(url, '_blank', 'noopener,noreferrer'), openExternal: (url) => window.open(url, '_blank', 'noopener,noreferrer'),
screenSpaceEventHandlerFactory: (viewer) => screenSpaceEventHandlerFactory: (viewer) =>
new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas), new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas),
createRenderer: ({viewer, engine, screenSpaceEventHandlerFactory}) => engine createRenderer: ({ viewer, engine, screenSpaceEventHandlerFactory }) =>
? rendererFor('fire-perimeters', engine, {viewer, screenSpaceEventHandlerFactory}) engine
: createPerimetersCesiumRenderer({viewer, screenSpaceEventHandlerFactory}), ? rendererFor('fire-perimeters', engine, {
viewer,
screenSpaceEventHandlerFactory,
})
: createPerimetersCesiumRenderer({
viewer,
screenSpaceEventHandlerFactory,
}),
picking, picking,
pointer: { isPointerFree }, pointer: { isPointerFree },
...options, ...options,
+2 -1
View File
@@ -97,7 +97,8 @@ export const CESIUM_LAYER_RENDERERS = Object.freeze({
earthquakes: createEarthquakesCesiumRenderer, earthquakes: createEarthquakesCesiumRenderer,
'air-quality': (_engine, options) => createAirQualityCesiumRenderer(options), 'air-quality': (_engine, options) => createAirQualityCesiumRenderer(options),
'gnss-integrity': (_engine, options) => createGnssCesiumRenderer(options), 'gnss-integrity': (_engine, options) => createGnssCesiumRenderer(options),
'fire-perimeters': (_engine, options) => createPerimetersCesiumRenderer(options), 'fire-perimeters': (_engine, options) =>
createPerimetersCesiumRenderer(options),
'local-firms': createFirmsCesiumRenderer, 'local-firms': createFirmsCesiumRenderer,
'rocket-launches': createLaunchesCesiumRenderer, 'rocket-launches': createLaunchesCesiumRenderer,
satellites: createSatellitesCesiumRenderer, satellites: createSatellitesCesiumRenderer,
+11 -9
View File
@@ -5,18 +5,20 @@ import { rendererFor } from './renderers.js';
import { overlayHost } from './overlayHost.js'; import { overlayHost } from './overlayHost.js';
/** Wire cable source geometry to the application overlay host. */ /** Wire cable source geometry to the application overlay host. */
export function createApplicationCables({ session = false, ...options }) { export function createApplicationCables({ session = false, ...options }) {
const createCesium = (source) => createSubmarineCableLayer({ const createCesium = (source) =>
overlayHost, createSubmarineCableLayer({
screenSpaceEventHandlerFactory: (canvas) => overlayHost,
new Cesium.ScreenSpaceEventHandler(canvas), screenSpaceEventHandlerFactory: (canvas) =>
mapStackEventTarget: typeof window !== 'undefined' ? window : null, new Cesium.ScreenSpaceEventHandler(canvas),
...options, mapStackEventTarget: typeof window !== 'undefined' ? window : null,
source, ...options,
}); source,
});
if (!session) return createCesium(options.source); if (!session) return createCesium(options.source);
return createCableSessionLayer({ return createCableSessionLayer({
source: options.source, source: options.source,
createCesium, createCesium,
createNavara: (engine) => rendererFor('telegeography-submarine-cables', engine, { overlayHost }), createNavara: (engine) =>
rendererFor('telegeography-submarine-cables', engine, { overlayHost }),
}); });
} }
+6 -1
View File
@@ -7,6 +7,11 @@ import * as render from '../../renderGovernor.js';
export function createApplicationTraffic({ source, surface, nativeRenderer }) { export function createApplicationTraffic({ source, surface, nativeRenderer }) {
return createTrafficLayer({ return createTrafficLayer({
source, source,
services: { credits, render, nativeRenderer, ground: surface?.groundFloor || ground }, services: {
credits,
render,
nativeRenderer,
ground: surface?.groundFloor || ground,
},
}); });
} }
+21 -3
View File
@@ -17,15 +17,33 @@ import {
// The tools without a viewer (the experimental Navara engine); they load // The tools without a viewer (the experimental Navara engine); they load
// from src/app/navaraMode.js. // from src/app/navaraMode.js.
test('without a viewer the tools run the governor on the engine, show the notice, create no viewer tools and run voice on the engine', async (t) => { test('without a viewer the tools run the governor on the engine, show the notice, create no viewer tools and run voice on the engine', async (t) => {
const saved = { window: globalThis.window, document: globalThis.document, localStorage:globalThis.localStorage }; const saved = {
window: globalThis.window,
document: globalThis.document,
localStorage: globalThis.localStorage,
};
const listeners = new Map(); const listeners = new Map();
globalThis.window = {}; globalThis.window = {};
globalThis.localStorage = { getItem:()=>null,setItem(){},removeItem(){} }; globalThis.localStorage = {
getItem: () => null,
setItem() {},
removeItem() {},
};
globalThis.document = { globalThis.document = {
hidden: false, hidden: false,
getElementById: () => null, getElementById: () => null,
querySelector: () => null, querySelector: () => null,
body: {classList:{add(){},remove(){},toggle(){},contains(){return false;}},appendChild(){}}, body: {
classList: {
add() {},
remove() {},
toggle() {},
contains() {
return false;
},
},
appendChild() {},
},
addEventListener: (type, fn) => listeners.set(type, fn), addEventListener: (type, fn) => listeners.set(type, fn),
removeEventListener: (type, fn) => { removeEventListener: (type, fn) => {
if (listeners.get(type) === fn) listeners.delete(type); if (listeners.get(type) === fn) listeners.delete(type);
+1 -1
View File
@@ -483,7 +483,7 @@ test('GERMAN: an existing untracked military billboard repaints each poll withou
}); });
flightState.records = records; flightState.records = records;
flightState._renderer = { view: {}, surfaceExclusions: () => [] }; flightState._renderer = { view: {}, surfaceExclusions: () => [] };
const {applySnapshot: apply} = createMilitarySnapshotRenderer({ const { applySnapshot: apply } = createMilitarySnapshotRenderer({
flightState, flightState,
records, records,
groundFloor: { warmGroundFloor() {}, cachedGroundFloor: () => null }, groundFloor: { warmGroundFloor() {}, cachedGroundFloor: () => null },
+11 -6
View File
@@ -550,12 +550,13 @@ function anchorOnScreen(viewer, engine, anchor, root) {
); );
if (Cesium.Cartesian3.dot(delta, camera.directionWC) <= 0) return null; if (Cesium.Cartesian3.dot(delta, camera.directionWC) <= 0) return null;
} }
const pixel = engine ? engine.projector.worldToWindow(anchor, {}) : Cesium.SceneTransforms.worldToWindowCoordinates( const pixel = engine
viewer.scene, ? engine.projector.worldToWindow(anchor, {})
anchor, : Cesium.SceneTransforms.worldToWindowCoordinates(viewer.scene, anchor);
);
if (!pixel) return null; if (!pixel) return null;
const canvasBounds = (engine?.host.canvas ?? viewer?.canvas)?.getBoundingClientRect?.(); const canvasBounds = (
engine?.host.canvas ?? viewer?.canvas
)?.getBoundingClientRect?.();
const rootBounds = root.getBoundingClientRect?.(); const rootBounds = root.getBoundingClientRect?.();
if (!canvasBounds || !rootBounds) return null; if (!canvasBounds || !rootBounds) return null;
const x = pixel.x + canvasBounds.left - rootBounds.left; const x = pixel.x + canvasBounds.left - rootBounds.left;
@@ -638,7 +639,11 @@ function createRoot(documentRef, viewer) {
const root = documentRef.createElement('div'); const root = documentRef.createElement('div');
root.id = 'bhote-koshi-embedded-media-root'; root.id = 'bhote-koshi-embedded-media-root';
root.setAttribute('aria-live', 'polite'); root.setAttribute('aria-live', 'polite');
(documentRef.getElementById?.('cesiumContainer') || viewer?.container || documentRef.body)?.append(root); (
documentRef.getElementById?.('cesiumContainer') ||
viewer?.container ||
documentRef.body
)?.append(root);
return root; return root;
} }
+2 -1
View File
@@ -47,7 +47,8 @@ const SCENE_PATH_RENDER_HOLD = 'bhote-koshi-scene-path-travel';
const SCENE_MEDIA_MIN_PLAYBACK_SECONDS = 6; const SCENE_MEDIA_MIN_PLAYBACK_SECONDS = 6;
const SCENE_MEDIA_EXIT_SECONDS = 0.65; const SCENE_MEDIA_EXIT_SECONDS = 0.65;
const BHOTE_EVENT_ACCENT = WORLD_OVERLAY_STYLE.accent; const BHOTE_EVENT_ACCENT = WORLD_OVERLAY_STYLE.accent;
const FLOOD_COLOR = () => Cesium.Color.fromCssColorString('#d8954e').withAlpha(0.68); const FLOOD_COLOR = () =>
Cesium.Color.fromCssColorString('#d8954e').withAlpha(0.68);
const SURGE_COLOR = () => Cesium.Color.fromCssColorString('#e8b47c'); const SURGE_COLOR = () => Cesium.Color.fromCssColorString('#e8b47c');
const CINEMATIC_INPUT_EVENTS = Object.freeze(['pointerdown', 'wheel']); const CINEMATIC_INPUT_EVENTS = Object.freeze(['pointerdown', 'wheel']);
const CINEMATIC_VIEW_SPECS = Object.freeze([ const CINEMATIC_VIEW_SPECS = Object.freeze([
+21 -8
View File
@@ -291,7 +291,10 @@ export function createBhoteKoshiLocatorLayer({
let _pausedApproach = null; let _pausedApproach = null;
const cameraForScene = () => _engine?.camera ?? _viewer?.camera; const cameraForScene = () => _engine?.camera ?? _viewer?.camera;
const creditTarget = () => _engine ?? _viewer; const creditTarget = () => _engine ?? _viewer;
const requestRender = reason => _engine?.kind === 'navara' ? _engine.render.request(reason) : requestSceneRender(reason); const requestRender = (reason) =>
_engine?.kind === 'navara'
? _engine.render.request(reason)
: requestSceneRender(reason);
let _dataSource = null; let _dataSource = null;
let _enabled = false; let _enabled = false;
let _boundaryReady = false; let _boundaryReady = false;
@@ -651,10 +654,17 @@ export function createBhoteKoshiLocatorLayer({
roll: Cesium.Math.toRadians(pose.roll), roll: Cesium.Math.toRadians(pose.roll),
}, },
duration, duration,
easingFunction: orbit ? Cesium.EasingFunction.SINUSOIDAL_IN_OUT : Cesium.EasingFunction.CUBIC_IN_OUT, easingFunction: orbit
? Cesium.EasingFunction.SINUSOIDAL_IN_OUT
: Cesium.EasingFunction.CUBIC_IN_OUT,
complete: () => { complete: () => {
if (_triggerApproachAttempt !== attempt) return; if (_triggerApproachAttempt !== attempt) return;
if (orbit || !_enabled || _presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION) finish(); if (
orbit ||
!_enabled ||
_presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
)
finish();
else flyTriggerStage(attempt, 'orbiting', TRIGGER_ORBIT_DURATION_SEC); else flyTriggerStage(attempt, 'orbiting', TRIGGER_ORBIT_DURATION_SEC);
}, },
cancel: finish, cancel: finish,
@@ -1042,14 +1052,14 @@ export function createBhoteKoshiLocatorLayer({
showOsmCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_LAYER_ID); showOsmCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_LAYER_ID);
} }
function attachRenderer(viewer, {engine = null} = {}) { function attachRenderer(viewer, { engine = null } = {}) {
_viewer = viewer; _viewer = viewer;
_engine = engine; _engine = engine;
if (!_enabled) return true; if (!_enabled) return true;
connectDisplay(); connectDisplay();
publishOverlayEntries({moving: _placeRevealMoving}); publishOverlayEntries({ moving: _placeRevealMoving });
if (_pausedApproach) { if (_pausedApproach) {
const {phase, remaining} = _pausedApproach; const { phase, remaining } = _pausedApproach;
_pausedApproach = null; _pausedApproach = null;
if (phase === 'armed') armTriggerApproach(); if (phase === 'armed') armTriggerApproach();
else flyTriggerStage((_triggerApproachAttempt = {}), phase, remaining); else flyTriggerStage((_triggerApproachAttempt = {}), phase, remaining);
@@ -1060,8 +1070,11 @@ export function createBhoteKoshiLocatorLayer({
function detachRenderer() { function detachRenderer() {
const phase = _triggerApproachAttempt?.phase; const phase = _triggerApproachAttempt?.phase;
if (phase && phase !== 'finished') { if (phase && phase !== 'finished') {
_pausedApproach = {phase, remaining: Math.max(0, (_triggerApproachAttempt.endsAt - now()) / 1000)}; _pausedApproach = {
clearTriggerApproach({preserveCallout: true}); phase,
remaining: Math.max(0, (_triggerApproachAttempt.endsAt - now()) / 1000),
};
clearTriggerApproach({ preserveCallout: true });
} }
_native?.destroy(); _native?.destroy();
_native = null; _native = null;
+5 -1
View File
@@ -43,7 +43,11 @@ export function createApplicationDirectionsLayer({
createNativeRenderer, createNativeRenderer,
groundFloor = ground, groundFloor = ground,
} = {}) { } = {}) {
return createDirectionsLayer({ services: { ...directionsServices, ground: groundFloor }, source, createNativeRenderer }); return createDirectionsLayer({
services: { ...directionsServices, ground: groundFloor },
source,
createNativeRenderer,
});
} }
export { export {
+4 -1
View File
@@ -6,7 +6,10 @@ import { INFRASTRUCTURE_DATA_URLS } from '../sources/infrastructureData.js';
* @param {object} services Caller-owned context, overlay and render operations. * @param {object} services Caller-owned context, overlay and render operations.
* @returns {object[]} Datacenters then dams, with stable standalone identities. * @returns {object[]} Datacenters then dams, with stable standalone identities.
*/ */
export function createInfrastructureLayers(services, createLayer = createLocalGeoJsonLayer) { export function createInfrastructureLayers(
services,
createLayer = createLocalGeoJsonLayer,
) {
const datacentersUrl = INFRASTRUCTURE_DATA_URLS['local-datacenters']; const datacentersUrl = INFRASTRUCTURE_DATA_URLS['local-datacenters'];
const damsUrl = INFRASTRUCTURE_DATA_URLS['local-dams']; const damsUrl = INFRASTRUCTURE_DATA_URLS['local-dams'];
const datacenters = createLayer( const datacenters = createLayer(
+27 -9
View File
@@ -406,12 +406,21 @@ export class LayerStateCoordinator {
); );
// A same-document engine choice retains operational layer intents. // A same-document engine choice retains operational layer intents.
// The share still owns durable preferences and later history visits. // The share still owns durable preferences and later history visits.
if (live && this.dataManager.layers.get(entry.id) === live.entry && if (
!live.entry.lifecycleUncertain && live.entry.enabled === targetEnabled) { live &&
this.dataManager.layers.get(entry.id) === live.entry &&
!live.entry.lifecycleUncertain &&
live.entry.enabled === targetEnabled
) {
return { return {
layerId: entry.id, targetEnabled, origin, phase: 'retained', layerId: entry.id,
targetEnabled,
origin,
phase: 'retained',
...currentLayerOutcome(this.dataManager, entry.id), ...currentLayerOutcome(this.dataManager, entry.id),
appliedOptions: {}, persistenceWrite: false, succeeded: true, appliedOptions: {},
persistenceWrite: false,
succeeded: true,
}; };
} }
const options = layerOptionsForRestore(this._durableState, entry.id); const options = layerOptionsForRestore(this._durableState, entry.id);
@@ -512,24 +521,33 @@ export class LayerStateCoordinator {
const model = this.dataManager.layers.get(selected.layerId)?.module; const model = this.dataManager.layers.get(selected.layerId)?.module;
if ( if (
model?.attachRenderer && model?.attachRenderer &&
String(model.getParams?.()[selected.optionKey]) === String(selected.targetId) && String(model.getParams?.()[selected.optionKey]) ===
String(selected.targetId) &&
model.hasContact?.(selected.targetId) === true model.hasContact?.(selected.targetId) === true
) return selected; )
return selected;
return null; return null;
} }
resumeRetainedTracking(previous) { resumeRetainedTracking(previous) {
const selected = this.retainedTrackingTarget(); const selected = this.retainedTrackingTarget();
if ( if (
!selected || selected.layerId !== previous.layerId || !selected ||
selected.layerId !== previous.layerId ||
selected.targetId !== previous.targetId selected.targetId !== previous.targetId
) return null; )
return null;
const model = this.dataManager.layers.get(selected.layerId).module; const model = this.dataManager.layers.get(selected.layerId).module;
const followed = model.trackById(selected.targetId, { const followed = model.trackById(selected.targetId, {
origin: LAYER_RESTORE_ORIGINS.share, origin: LAYER_RESTORE_ORIGINS.share,
}); });
if (!followed) return null; if (!followed) return null;
const result = { ...selected, status: 'found', classification: 'followed', cleared: false }; const result = {
...selected,
status: 'found',
classification: 'followed',
cleared: false,
};
this._publishTrackingRestoreStatus(result); this._publishTrackingRestoreStatus(result);
return result; return result;
} }
+5 -2
View File
@@ -585,7 +585,8 @@ export function createLocalGeoJsonLayer(
// windows (before vs after the add settles) need different cleanup. // windows (before vs after the add settles) need different cleanup.
let addedToScene = false; let addedToScene = false;
try { try {
let features = _cachedFeatures ?? (readFeatures ? await readFeatures() : null); let features =
_cachedFeatures ?? (readFeatures ? await readFeatures() : null);
if (_destroyed) return; if (_destroyed) return;
if (!features) { if (!features) {
const response = await fetch(url, { const response = await fetch(url, {
@@ -1065,7 +1066,9 @@ export function createLocalGeoJsonLayer(
}, },
restoreFeatureSelection(viewer, featureId) { restoreFeatureSelection(viewer, featureId) {
const entity = _dataSource?.entities.values.find((item) => String(item.id) === featureId); const entity = _dataSource?.entities.values.find(
(item) => String(item.id) === featureId,
);
if (!entity) return false; if (!entity) return false;
viewer.selectedEntity = entity; viewer.selectedEntity = entity;
selectEntityContext(entity); selectEntityContext(entity);
+9 -3
View File
@@ -25,7 +25,11 @@ const STATION_MARKER_RADIUS_M = 6_000;
const READOUT_LIMIT = 5; const READOUT_LIMIT = 5;
/** Own one air-quality display, its refresh lifecycle, and its readout card. */ /** Own one air-quality display, its refresh lifecycle, and its readout card. */
export function createAirQualityLayer({ source, overlayHost, createRenderer = createAirQualityCesiumRenderer } = {}) { export function createAirQualityLayer({
source,
overlayHost,
createRenderer = createAirQualityCesiumRenderer,
} = {}) {
if (typeof source?.getSnapshot !== 'function') if (typeof source?.getSnapshot !== 'function')
throw new TypeError('Air quality requires a snapshot source'); throw new TypeError('Air quality requires a snapshot source');
if (!overlayHost) throw new TypeError('Air quality requires an overlay host'); if (!overlayHost) throw new TypeError('Air quality requires an overlay host');
@@ -61,7 +65,8 @@ export function createAirQualityLayer({ source, overlayHost, createRenderer = cr
updateInterval: 1_200_000, updateInterval: 1_200_000,
init(viewer, { engine = null } = {}) { init(viewer, { engine = null } = {}) {
if (initialized) throw new Error('Air quality layer is already initialized'); if (initialized)
throw new Error('Air quality layer is already initialized');
initialized = true; initialized = true;
try { try {
if (viewer || engine) this.attachRenderer(viewer, { engine }); if (viewer || engine) this.attachRenderer(viewer, { engine });
@@ -72,7 +77,8 @@ export function createAirQualityLayer({ source, overlayHost, createRenderer = cr
}, },
attachRenderer(viewer, { engine = null } = {}) { attachRenderer(viewer, { engine = null } = {}) {
if (!initialized || renderer) throw new Error('Invalid air quality scene attachment'); if (!initialized || renderer)
throw new Error('Invalid air quality scene attachment');
renderer = createRenderer({ viewer, engine }); renderer = createRenderer({ viewer, engine });
renderer.setVisible(_enabled); renderer.setVisible(_enabled);
overlayHost.setVisible(AIR_QUALITY_OVERLAY_SOURCE_ID, _enabled); overlayHost.setVisible(AIR_QUALITY_OVERLAY_SOURCE_ID, _enabled);
+6 -4
View File
@@ -276,10 +276,12 @@ test('a failed refresh over good readings reports degraded, not nominal', async
test('station data stays queryable and attaches to a fresh scene without polling', async () => { test('station data stays queryable and attaches to a fresh scene without polling', async () => {
let polls = 0; let polls = 0;
const h = harness({ getSnapshot: async () => { const h = harness({
polls++; getSnapshot: async () => {
return [reading('Toronto', 7)]; polls++;
} }); return [reading('Toronto', 7)];
},
});
try { try {
h.layer.detachRenderer(); h.layer.detachRenderer();
assert.equal(h.sources.length, 0); assert.equal(h.sources.length, 0);
+11 -2
View File
@@ -35,8 +35,17 @@ function stationFeature(reading, radius) {
/** One native GeoJSON layer per AQHI colour, independent of station count. */ /** One native GeoJSON layer per AQHI colour, independent of station count. */
export function createAirQualityNavaraRenderer(engine, { view }) { export function createAirQualityNavaraRenderer(engine, { view }) {
const display = createNavaraPolygonDisplay(engine, view, (color, show) => ({ const display = createNavaraPolygonDisplay(engine, view, (color, show) => ({
polygon: {color, show, transparent: true, opacity: 0.35, clampToGround: true, polygon: {
outline: true, outlineShow: true, outlineColor: color, outlineWidth: 2}, color,
show,
transparent: true,
opacity: 0.35,
clampToGround: true,
outline: true,
outlineShow: true,
outlineColor: color,
outlineWidth: 2,
},
})); }));
return { return {
viewRectangle: () => engine.camera.viewRectangle(), viewRectangle: () => engine.camera.viewRectangle(),
+7 -1
View File
@@ -232,7 +232,13 @@ export function createAlprCamerasLayer({
// Stale and saturated are independent facts; a cached response that also // Stale and saturated are independent facts; a cached response that also
// hit the cap must still warn about coverage, not just about freshness. // hit the cap must still warn about coverage, not just about freshness.
if (!restoringView) renderer?.renderRecords(); if (!restoringView) renderer?.renderRecords();
setAlprStatus((renderer?.count() ?? records.length) ? (stale ? 'stale' : 'ready') : 'empty'); setAlprStatus(
(renderer?.count() ?? records.length)
? stale
? 'stale'
: 'ready'
: 'empty',
);
} catch (error) { } catch (error) {
if ( if (
error?.name === 'AbortError' || error?.name === 'AbortError' ||
+10 -2
View File
@@ -45,7 +45,11 @@ export function createFocus({ state: layerState, services, parts, source }) {
) )
return true; return true;
announceNavigationAuthority('context-vessel-focus'); announceNavigationAuthority('context-vessel-focus');
layerState.display.frameContact(vessel.position, VESSEL_FOCUS_RADIUS_M, 1.4); layerState.display.frameContact(
vessel.position,
VESSEL_FOCUS_RADIUS_M,
1.4,
);
return true; return true;
} }
@@ -177,7 +181,11 @@ export function createFocus({ state: layerState, services, parts, source }) {
announceNavigationAuthority('context-vessel-autofocus', { announceNavigationAuthority('context-vessel-autofocus', {
cancelPendingSelection: false, cancelPendingSelection: false,
}); });
layerState.display.frameContact(vessel.position, VESSEL_FOCUS_RADIUS_M, 1.6); layerState.display.frameContact(
vessel.position,
VESSEL_FOCUS_RADIUS_M,
1.6,
);
return true; return true;
} }
return { return {
+38 -38
View File
@@ -51,43 +51,42 @@ export function createLifecycle({
'gev:awareness-subject-cleared', 'gev:awareness-subject-cleared',
layerState.subjectClearListener, layerState.subjectClearListener,
); );
layerState.preRenderRemover = layerState.preRenderRemover = layerState.display.onFrame(() => {
layerState.display.onFrame(() => { if (!layerState.enabled) return;
if (!layerState.enabled) return; const now = Date.now();
const now = Date.now(); // "Did the camera move" uses the SAME quantized pose signature the fleet
// "Did the camera move" uses the SAME quantized pose signature the fleet // rotation pass gates on (iconOrientation.cameraPoseSignature) rather than
// rotation pass gates on (iconOrientation.cameraPoseSignature) rather than // camera.changed, whose granularity is globally degraded by other layers
// camera.changed, whose granularity is globally degraded by other layers // mutating camera.percentageChanged.
// mutating camera.percentageChanged. const poseSig = cameraMotionSignature(layerState.display.camera);
const poseSig = cameraMotionSignature(layerState.display.camera); if (poseSig !== layerState.lastCameraPoseSig) {
if (poseSig !== layerState.lastCameraPoseSig) { layerState.lastCameraPoseSig = poseSig;
layerState.lastCameraPoseSig = poseSig; layerState.lastCameraPoseChangeMs = now;
layerState.lastCameraPoseChangeMs = now; }
} const decision = parts.model.awarenessRefreshDecision({
const decision = parts.model.awarenessRefreshDecision({ nowMs: now,
nowMs: now, lastRefreshMs: layerState.lastSubjectRefreshMs,
lastRefreshMs: layerState.lastSubjectRefreshMs, lastPoseChangeMs: layerState.lastCameraPoseChangeMs,
lastPoseChangeMs: layerState.lastCameraPoseChangeMs, wasMoving: layerState.cameraMoving,
wasMoving: layerState.cameraMoving,
});
layerState.cameraMoving = decision.moving;
syncAwarenessRenderHold();
if (!decision.refresh) return;
layerState.lastSubjectRefreshMs = now;
if (
layerState.subject &&
['flights', 'military', 'ais-live-vessels'].includes(
layerState.subject.layerId,
)
) {
parts.subject.refreshSelectedSubject();
} else if (!layerState.passive && layerState.autoFocusRetryPending) {
layerState.autoFocusRetryPending = false;
parts.focus.focusAttentionTarget();
} else {
parts.rendering.scheduleDirectionOverlayUpdate();
}
}); });
layerState.cameraMoving = decision.moving;
syncAwarenessRenderHold();
if (!decision.refresh) return;
layerState.lastSubjectRefreshMs = now;
if (
layerState.subject &&
['flights', 'military', 'ais-live-vessels'].includes(
layerState.subject.layerId,
)
) {
parts.subject.refreshSelectedSubject();
} else if (!layerState.passive && layerState.autoFocusRetryPending) {
layerState.autoFocusRetryPending = false;
parts.focus.focusAttentionTarget();
} else {
parts.rendering.scheduleDirectionOverlayUpdate();
}
});
layerState.runtimeListenersAttached = true; layerState.runtimeListenersAttached = true;
} }
@@ -184,10 +183,11 @@ export function createLifecycle({
layerState.display?.destroy?.(); layerState.display?.destroy?.();
layerState.display = viewer layerState.display = viewer
? createAwarenessCesiumRenderer(viewer) ? createAwarenessCesiumRenderer(viewer)
: services.renderer?.(engine) ?? null; : (services.renderer?.(engine) ?? null);
if (layerState.enabled) { if (layerState.enabled) {
attachRuntimeListeners(); attachRuntimeListeners();
if (layerState.subject) parts.rendering.renderVisual(layerState.subject); if (layerState.subject)
parts.rendering.renderVisual(layerState.subject);
parts.rendering.scheduleDirectionOverlayUpdate(true); parts.rendering.scheduleDirectionOverlayUpdate(true);
} }
}, },
+2 -1
View File
@@ -75,7 +75,8 @@ export function createBikeshareLayer({ services, source, rendererFactory }) {
state._viewer = state._engine = null; state._viewer = state._engine = null;
renderer = null; renderer = null;
initialized = false; initialized = false;
for (const key of ['rendering', 'selection', 'viewport', 'lifecycle']) delete parts[key]; for (const key of ['rendering', 'selection', 'viewport', 'lifecycle'])
delete parts[key];
}, },
enable(...args) { enable(...args) {
return renderer?.lifecycle.methods.enable(...args); return renderer?.lifecycle.methods.enable(...args);
@@ -137,7 +137,9 @@ export function createLifecycle({
document.removeEventListener('keydown', parts.selection._onKeyDown); document.removeEventListener('keydown', parts.selection._onKeyDown);
unregisterPickOwner('bikeshare'); unregisterPickOwner('bikeshare');
if (layerState._cameraChangedAttached) { if (layerState._cameraChangedAttached) {
viewer.camera.changed.removeEventListener(parts.viewport.onCameraChanged); viewer.camera.changed.removeEventListener(
parts.viewport.onCameraChanged,
);
releaseCameraSensitivity(viewer.camera, 'bikeshare'); releaseCameraSensitivity(viewer.camera, 'bikeshare');
layerState._cameraChangedAttached = false; layerState._cameraChangedAttached = false;
} }
+18 -9
View File
@@ -133,8 +133,7 @@ export function createLifecycle({
// swaps (bing→osm) no-op. // swaps (bing→osm) no-op.
layerState._lastAppliedRegime = parts.ground.currentSurfaceRegime(); layerState._lastAppliedRegime = parts.ground.currentSurfaceRegime();
if (!layerState._mapStackListener && typeof window !== 'undefined') { if (!layerState._mapStackListener && typeof window !== 'undefined') {
layerState._mapStackListener = () => layerState._mapStackListener = () => parts.ground.handleMapStackChanged();
parts.ground.handleMapStackChanged();
window.addEventListener( window.addEventListener(
'gev:map-stack-changed', 'gev:map-stack-changed',
layerState._mapStackListener, layerState._mapStackListener,
@@ -222,7 +221,6 @@ export function createLifecycle({
parts.hover.handleHoverMove(movement?.endPosition), parts.hover.handleHoverMove(movement?.endPosition),
}, },
); );
} }
function createRenderer(viewer, options) { function createRenderer(viewer, options) {
if (parts.renderer) throw new Error('CCTV layer is already initialized'); if (parts.renderer) throw new Error('CCTV layer is already initialized');
@@ -431,13 +429,17 @@ export function createLifecycle({
layerState._restoringViewport = Boolean(options.waitForCameraRestore); layerState._restoringViewport = Boolean(options.waitForCameraRestore);
renderer.createMarkers(); renderer.createMarkers();
for (const record of layerState._records) { for (const record of layerState._records) {
record.billboard = renderer.createMarker(record.camera, record.position); record.billboard = renderer.createMarker(
record.camera,
record.position,
);
record.groundResolved = {}; record.groundResolved = {};
record.groundSamples = {}; record.groundSamples = {};
record.probeClampRangeM = null; record.probeClampRangeM = null;
} }
bindScene(viewer, options); bindScene(viewer, options);
if (!layerState._restoringViewport && layerState._enabled) this.resumeDisplay(); if (!layerState._restoringViewport && layerState._enabled)
this.resumeDisplay();
}, },
resumeViewport() { resumeViewport() {
@@ -462,7 +464,10 @@ export function createLifecycle({
parts.renderer.hideCredit(); parts.renderer.hideCredit();
unregisterPickOwner('cctv'); unregisterPickOwner('cctv');
if (layerState._mapStackListener) { if (layerState._mapStackListener) {
window.removeEventListener('gev:map-stack-changed', layerState._mapStackListener); window.removeEventListener(
'gev:map-stack-changed',
layerState._mapStackListener,
);
layerState._mapStackListener = null; layerState._mapStackListener = null;
} }
layerState._viewResizeObserver?.disconnect(); layerState._viewResizeObserver?.disconnect();
@@ -470,7 +475,10 @@ export function createLifecycle({
layerState._horizonCullListener?.(); layerState._horizonCullListener?.();
layerState._moveStartListener?.(); layerState._moveStartListener?.();
layerState._removeFocusAppearListener?.(); layerState._removeFocusAppearListener?.();
layerState._horizonCullListener = layerState._moveStartListener = layerState._removeFocusAppearListener = null; layerState._horizonCullListener =
layerState._moveStartListener =
layerState._removeFocusAppearListener =
null;
layerState._gizmo?.destroy(); layerState._gizmo?.destroy();
layerState._gizmo = null; layerState._gizmo = null;
releaseContinuousRender('cctv-adjust'); releaseContinuousRender('cctv-adjust');
@@ -483,7 +491,7 @@ export function createLifecycle({
parts.hover.clearHoverCard(); parts.hover.clearHoverCard();
parts.projection.clearProjectionOverlay(); parts.projection.clearProjectionOverlay();
layerState._cctvOverlayHost.clearSource(CCTV_OVERLAY_SOURCE_ID); layerState._cctvOverlayHost.clearSource(CCTV_OVERLAY_SOURCE_ID);
parts.geometry.destroyCoverageEntities({retainMedia:true}); parts.geometry.destroyCoverageEntities({ retainMedia: true });
for (const record of layerState._records) { for (const record of layerState._records) {
record.billboard = null; record.billboard = null;
record.coverageEntities = []; record.coverageEntities = [];
@@ -536,7 +544,8 @@ export function createLifecycle({
const activeRecord = parts.selection.getActiveRecord(); const activeRecord = parts.selection.getActiveRecord();
if (activeRecord) { if (activeRecord) {
parts.projection.ensureProjectionRuntime(activeRecord); parts.projection.ensureProjectionRuntime(activeRecord);
if (refreshImage) parts.frames.refreshProjectionImage(activeRecord, true); if (refreshImage)
parts.frames.refreshProjectionImage(activeRecord, true);
} }
parts.rendering.refreshHorizonCulling(); parts.rendering.refreshHorizonCulling();
// Re-enabled mid-flight: moveStart has already fired. // Re-enabled mid-flight: moveStart has already fired.
+18 -6
View File
@@ -103,7 +103,8 @@ export function createProjection({
function setPlaneVisible(runtime, visible) { function setPlaneVisible(runtime, visible) {
if (!runtime) return; if (!runtime) return;
runtime.planeVisible = !!visible; runtime.planeVisible = !!visible;
if (runtime.planeEntity) parts.renderer?.setPlaneVisible(runtime, !!visible); if (runtime.planeEntity)
parts.renderer?.setPlaneVisible(runtime, !!visible);
if (visible && runtime.overlayEntry && runtime.cameraId) { if (visible && runtime.overlayEntry && runtime.cameraId) {
if (layerState._projectionOverlayOwnerId !== runtime.cameraId) { if (layerState._projectionOverlayOwnerId !== runtime.cameraId) {
layerState._cctvOverlayHost.setEntries( layerState._cctvOverlayHost.setEntries(
@@ -256,7 +257,8 @@ export function createProjection({
runtime.imageLoading = false; runtime.imageLoading = false;
runtime.imageReady = false; runtime.imageReady = false;
}; };
if (runtime.planeMaterial) parts.renderer?.setPlaneImage(runtime, runtime.canvas); if (runtime.planeMaterial)
parts.renderer?.setPlaneImage(runtime, runtime.canvas);
parts.frames.refreshProjectionImage(record, true); parts.frames.refreshProjectionImage(record, true);
parts.presentation.notifyListeners(); parts.presentation.notifyListeners();
}, },
@@ -314,11 +316,21 @@ export function createProjection({
if (record.projection && !record.projection.disposed) { if (record.projection && !record.projection.disposed) {
const runtime = record.projection; const runtime = record.projection;
if (runtime.needsPlane && parts.renderer?.ready) { if (runtime.needsPlane && parts.renderer?.ready) {
const geometry = record.frustumGeometry || parts.geometry.computeFrustumGeometry(record.camera, parts.ground.groundAltFor(record), record.probeClampRangeM); const geometry =
const positions = record.frustumPositions || parts.geometry.frustumCartesians(geometry); record.frustumGeometry ||
runtime.planeMaterial = parts.renderer.createPlaneMaterial(runtime.canvas); parts.geometry.computeFrustumGeometry(
record.camera,
parts.ground.groundAltFor(record),
record.probeClampRangeM,
);
const positions =
record.frustumPositions || parts.geometry.frustumCartesians(geometry);
runtime.planeMaterial = parts.renderer.createPlaneMaterial(
runtime.canvas,
);
createProjectionPlane(record, runtime, geometry, positions); createProjectionPlane(record, runtime, geometry, positions);
if (runtime.video?.videoWidth) parts.renderer.setPlaneImage(runtime, runtime.video); if (runtime.video?.videoWidth)
parts.renderer.setPlaneImage(runtime, runtime.video);
runtime.needsPlane = false; runtime.needsPlane = false;
runtime.lastSwappedCanvasStamp = -1; runtime.lastSwappedCanvasStamp = -1;
} }
+26 -10
View File
@@ -110,7 +110,12 @@ export function createCyclonesLayer({
function installSelection() { function installSelection() {
const canvas = engine?.host.canvas ?? viewer?.scene?.canvas; const canvas = engine?.host.canvas ?? viewer?.scene?.canvas;
const native = engine?.kind === 'navara'; const native = engine?.kind === 'navara';
if (clickHandler || !canvas || (!native && typeof cesium.ScreenSpaceEventHandler !== 'function')) return; if (
clickHandler ||
!canvas ||
(!native && typeof cesium.ScreenSpaceEventHandler !== 'function')
)
return;
const owner = native ? {} : new cesium.ScreenSpaceEventHandler(canvas); const owner = native ? {} : new cesium.ScreenSpaceEventHandler(canvas);
clickHandler = owner; clickHandler = owner;
let capturedHit = null; let capturedHit = null;
@@ -185,7 +190,8 @@ export function createCyclonesLayer({
if (id) layer.setParams({ stormId: id }); if (id) layer.setParams({ stormId: id });
else if (isSurfacePick(picked)) layer.setParams({ clear: true }); else if (isSurfacePick(picked)) layer.setParams({ clear: true });
}; };
if (native) owner.destroy = engine.input.on('click', onClick, { pick: true }); if (native)
owner.destroy = engine.input.on('click', onClick, { pick: true });
else owner.setInputAction(onClick, cesium.ScreenSpaceEventType.LEFT_CLICK); else owner.setInputAction(onClick, cesium.ScreenSpaceEventType.LEFT_CLICK);
} }
function removeSelection() { function removeSelection() {
@@ -202,14 +208,16 @@ export function createCyclonesLayer({
source: 'NOAA NHC / CPHC', source: 'NOAA NHC / CPHC',
updateInterval: 300_000, updateInterval: 300_000,
init(nextViewer, options = {}) { init(nextViewer, options = {}) {
if (nextViewer || options.engine) return layer.attachRenderer(nextViewer, options); if (nextViewer || options.engine)
return layer.attachRenderer(nextViewer, options);
}, },
attachRenderer(nextViewer, options = {}) { attachRenderer(nextViewer, options = {}) {
viewer = nextViewer; viewer = nextViewer;
engine = options.engine ?? null; engine = options.engine ?? null;
rendering = engine?.kind === 'navara' rendering =
? createNativeRendering(engine, { overlayHost }) engine?.kind === 'navara'
: createRendering({ viewer, cesium, overlayHost }); ? createNativeRendering(engine, { overlayHost })
: createRendering({ viewer, cesium, overlayHost });
if (!enabled) return; if (!enabled) return;
installSelection(); installSelection();
if (snapshot && !snapshot.unavailable) { if (snapshot && !snapshot.unavailable) {
@@ -287,8 +295,15 @@ export function createCyclonesLayer({
let display; let display;
do { do {
display = rendering; display = rendering;
applied = display ? await display.setSnapshot(next, { signal: controller.signal }) : true; applied = display
} while (display !== rendering && enabled && !controller.signal.aborted && request === controller); ? await display.setSnapshot(next, { signal: controller.signal })
: true;
} while (
display !== rendering &&
enabled &&
!controller.signal.aborted &&
request === controller
);
if ( if (
!applied || !applied ||
!enabled || !enabled ||
@@ -351,8 +366,9 @@ export function createCyclonesLayer({
selectedId !== storm.id selectedId !== storm.id
) )
return; return;
const fly = engine ? engine.camera.flyToSphere.bind(engine.camera) : const fly = engine
viewer.camera.flyToBoundingSphere.bind(viewer.camera); ? engine.camera.flyToSphere.bind(engine.camera)
: viewer.camera.flyToBoundingSphere.bind(viewer.camera);
return fly(sphere, { return fly(sphere, {
duration: matchMedia?.('(prefers-reduced-motion: reduce)') duration: matchMedia?.('(prefers-reduced-motion: reduce)')
?.matches ?.matches
+13 -5
View File
@@ -680,10 +680,18 @@ test('advisories delivered between scenes keep a deliberate selection without an
let reads = 0; let reads = 0;
const drawn = []; const drawn = [];
const layer = createCyclonesLayer({ const layer = createCyclonesLayer({
feed: { getSnapshot: () => ++reads === 1 ? snapshot([storm()]) : pending.promise }, feed: {
getSnapshot: () =>
++reads === 1 ? snapshot([storm()]) : pending.promise,
},
createRendering: () => ({ createRendering: () => ({
setSnapshot(next) { drawn.push(next.storms.map(s=>s.advisoryNumber)); return true; }, setSnapshot(next) {
setSelection() {}, clear() {}, destroy() {}, drawn.push(next.storms.map((s) => s.advisoryNumber));
return true;
},
setSelection() {},
clear() {},
destroy() {},
}), }),
}); });
layer.init({}); layer.init({});
@@ -692,11 +700,11 @@ test('advisories delivered between scenes keep a deliberate selection without an
layer.setParams({ stormId: 'ep152026' }); layer.setParams({ stormId: 'ep152026' });
const refresh = layer.update(); const refresh = layer.update();
layer.detachRenderer(); layer.detachRenderer();
pending.resolve(snapshot([{...storm(), advisoryNumber: '11'}])); pending.resolve(snapshot([{ ...storm(), advisoryNumber: '11' }]));
await refresh; await refresh;
assert.equal(layer.getDiagnostics().selectedId, 'ep152026'); assert.equal(layer.getDiagnostics().selectedId, 'ep152026');
await layer.attachRenderer({}); await layer.attachRenderer({});
assert.deepEqual(drawn, [['10'],['11']]); assert.deepEqual(drawn, [['10'], ['11']]);
assert.equal(reads, 2); assert.equal(reads, 2);
layer.destroy(); layer.destroy();
}); });
+34 -13
View File
@@ -71,7 +71,8 @@ export const DIRECTIONS_ROUTE_COLOR = '#39d0ff';
const MARKER_A_COLOR = () => Cesium.Color.fromCssColorString('#5dff9f'); const MARKER_A_COLOR = () => Cesium.Color.fromCssColorString('#5dff9f');
const MARKER_B_COLOR = () => Cesium.Color.fromCssColorString('#ff6b6b'); const MARKER_B_COLOR = () => Cesium.Color.fromCssColorString('#ff6b6b');
const STEP_COLOR = () => Cesium.Color.WHITE.withAlpha(0.95); const STEP_COLOR = () => Cesium.Color.WHITE.withAlpha(0.95);
const STEP_OUTLINE = () => Cesium.Color.fromCssColorString(DIRECTIONS_ROUTE_COLOR); const STEP_OUTLINE = () =>
Cesium.Color.fromCssColorString(DIRECTIONS_ROUTE_COLOR);
const STEP_PIXEL_SIZE = 8; const STEP_PIXEL_SIZE = 8;
const STEP_SELECTED_PIXEL_SIZE = 13; const STEP_SELECTED_PIXEL_SIZE = 13;
/** Route request timeout (ms) — the proxy itself gives OSRM 12 s. */ /** Route request timeout (ms) — the proxy itself gives OSRM 12 s. */
@@ -859,7 +860,11 @@ export function normalizeRoutePayload(payload, mode) {
* @param {{services: object, source: {getRoute: Function}}} options * @param {{services: object, source: {getRoute: Function}}} options
* @returns {object} The layer module the data manager registers. * @returns {object} The layer module the data manager registers.
*/ */
export function createDirectionsLayer({ services, source, createNativeRenderer }) { export function createDirectionsLayer({
services,
source,
createNativeRenderer,
}) {
if (typeof source?.getRoute !== 'function') if (typeof source?.getRoute !== 'function')
throw new TypeError('Directions require a route source'); throw new TypeError('Directions require a route source');
/** /**
@@ -970,10 +975,9 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
} }
function pickGround(screenPosition) { function pickGround(screenPosition) {
const cartesian = _native ? _engine.picker.pickGlobe(screenPosition) : services.scenePick.pickGroundPosition( const cartesian = _native
_viewer, ? _engine.picker.pickGlobe(screenPosition)
screenPosition, : services.scenePick.pickGroundPosition(_viewer, screenPosition);
);
if (!cartesian) return null; if (!cartesian) return null;
const carto = Cesium.Cartographic.fromCartesian(cartesian); const carto = Cesium.Cartographic.fromCartesian(cartesian);
if (!carto) return null; if (!carto) return null;
@@ -1011,7 +1015,10 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
} }
function removeEntity(entity) { function removeEntity(entity) {
if (_native) { entity?.destroy(); return; } if (_native) {
entity?.destroy();
return;
}
if (entity && _viewer && !_viewer.isDestroyed?.()) if (entity && _viewer && !_viewer.isDestroyed?.())
_viewer.entities.remove(entity); _viewer.entities.remove(entity);
} }
@@ -1099,10 +1106,13 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
function drawRoute(route) { function drawRoute(route) {
clearRouteGraphics(); clearRouteGraphics();
if (!_viewer && !_native) return; if (!_viewer && !_native) return;
if (_native) _routeEntity = _native.route(route.geometry, DIRECTIONS_ROUTE_COLOR); if (_native)
_routeEntity = _native.route(route.geometry, DIRECTIONS_ROUTE_COLOR);
else { else {
services.annotations.ensureFlowFabricRegistered(); services.annotations.ensureFlowFabricRegistered();
const positions = Cesium.Cartesian3.fromDegreesArray(route.geometry.flat()); const positions = Cesium.Cartesian3.fromDegreesArray(
route.geometry.flat(),
);
_routeEntity = _viewer.entities.add({ _routeEntity = _viewer.entities.add({
id: 'directions:route', id: 'directions:route',
polyline: { polyline: {
@@ -1563,11 +1573,18 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
if (_selectedStep !== null) _clearStepSelection(); if (_selectedStep !== null) _clearStepSelection();
}; };
if (_native) { if (_native) {
const off = _engine.input.on('click', event => handle({ position: event.position, picked: event.picked }), { pick: true }); const off = _engine.input.on(
'click',
(event) => handle({ position: event.position, picked: event.picked }),
{ pick: true },
);
_clickHandler = { destroy: off }; _clickHandler = { destroy: off };
} else { } else {
_clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); _clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
_clickHandler.setInputAction(handle, Cesium.ScreenSpaceEventType.LEFT_CLICK); _clickHandler.setInputAction(
handle,
Cesium.ScreenSpaceEventType.LEFT_CLICK,
);
} }
if (typeof document !== 'undefined') if (typeof document !== 'undefined')
document.addEventListener('keydown', _onKeyDown); document.addEventListener('keydown', _onKeyDown);
@@ -1768,8 +1785,12 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
if (_shellSeams && _suspendedFlight && _engine) { if (_shellSeams && _suspendedFlight && _engine) {
_suspendedFlight.floorFn = _shellSeams.floorFn; _suspendedFlight.floorFn = _shellSeams.floorFn;
_suspendedFlight.warmFn = _shellSeams.warmFn; _suspendedFlight.warmFn = _shellSeams.warmFn;
if (services.camera.resumeRouteFlight(_suspendedFlight, _engine)) trackFlightProgress(); if (services.camera.resumeRouteFlight(_suspendedFlight, _engine))
else { _flightId = 0; _flightStep = null; } trackFlightProgress();
else {
_flightId = 0;
_flightStep = null;
}
_suspendedFlight = null; _suspendedFlight = null;
} }
}, },
+12 -5
View File
@@ -53,7 +53,8 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
updateInterval: 60000, updateInterval: 60000,
init(viewer, { engine = null } = {}) { init(viewer, { engine = null } = {}) {
if (initialized) throw new Error('Earthquake layer is already initialized'); if (initialized)
throw new Error('Earthquake layer is already initialized');
initialized = true; initialized = true;
try { try {
if (viewer || engine) this.attachRenderer(viewer, { engine }); if (viewer || engine) this.attachRenderer(viewer, { engine });
@@ -65,7 +66,8 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
}, },
attachRenderer(viewer, { engine = null } = {}) { attachRenderer(viewer, { engine = null } = {}) {
if (!initialized || _renderer) throw new Error('Invalid earthquake scene attachment'); 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(_enabled); _renderer.setVisible(_enabled);
@@ -208,9 +210,14 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
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 observations.slice(0, limit).map((row, index) => return observations
mapAnalystRecord({ ...row, id: row.usgsId, depth: row.depthKm }, index), .slice(0, limit)
); .map((row, index) =>
mapAnalystRecord(
{ ...row, id: row.usgsId, depth: row.depthKm },
index,
),
);
}, },
getStats() { getStats() {
+10 -5
View File
@@ -94,11 +94,16 @@ test('USGS body completion honors cancellation even with an uncooperative transp
}); });
test('an observation received between scenes is queryable and draws without a second fetch', async () => { test('an observation received between scenes is queryable and draws without a second fetch', async () => {
let resolve, polls = 0; let resolve,
const h = harness({ getSnapshot() { polls = 0;
polls++; const h = harness({
return new Promise(done => { resolve = done; }); getSnapshot() {
} }); polls++;
return new Promise((done) => {
resolve = done;
});
},
});
try { try {
const pending = h.layer.update(); const pending = h.layer.update();
h.layer.detachRenderer(); h.layer.detachRenderer();
+6 -1
View File
@@ -199,7 +199,12 @@ function buildState(discs) {
function groundEffectClass(EffectDesc, NavaraEffect) { function groundEffectClass(EffectDesc, NavaraEffect) {
return class GroundEffectDesc extends EffectDesc { return class GroundEffectDesc extends EffectDesc {
static key = KEY; static key = KEY;
static insertAfter = ['gevObservedWeather', 'gevNepalEvent', 'gevRecentImagery', 'toneMapping']; static insertAfter = [
'gevObservedWeather',
'gevNepalEvent',
'gevRecentImagery',
'toneMapping',
];
static insertBefore = [ static insertBefore = [
'gevFirmsGround', 'gevFirmsGround',
'gevPreSharpenOverlay', 'gevPreSharpenOverlay',
+6 -2
View File
@@ -15,7 +15,10 @@ export function createLifecycle({
init(viewer, options) { init(viewer, options) {
this.attachRenderer(viewer, options); this.attachRenderer(viewer, options);
}, },
attachRenderer(viewer, { engine = null, waitForCameraRestore = false } = {}) { attachRenderer(
viewer,
{ engine = null, waitForCameraRestore = false } = {},
) {
if (layerState._destroyed) return; if (layerState._destroyed) return;
if (layerState._renderer) return; if (layerState._renderer) return;
layerState._renderer = config.renderer.create({ layerState._renderer = config.renderer.create({
@@ -87,7 +90,8 @@ export function createLifecycle({
this.showRenderer(); this.showRenderer();
if (!layerState._fires.length && !layerState._loading) if (!layerState._fires.length && !layerState._loading)
await components.ingestion.loadHeatmap(); await components.ingestion.loadHeatmap();
if (layerState._enabled && layerState._renderer === renderer) renderer.onEnable(); if (layerState._enabled && layerState._renderer === renderer)
renderer.onEnable();
}, },
disable() { disable() {
+6 -2
View File
@@ -15,7 +15,10 @@ export function createLifecycle({
const { registerPickOwner } = services.picking; const { registerPickOwner } = services.picking;
const binding = createAircraftSceneBinding({ const binding = createAircraftSceneBinding({
id: 'flights', flightState, services, parts, id: 'flights',
flightState,
services,
parts,
resolveModel: () => resolveAsset(PLANE_MODEL_URL), resolveModel: () => resolveAsset(PLANE_MODEL_URL),
enable: () => methods.enable(), enable: () => methods.enable(),
}); });
@@ -44,7 +47,8 @@ export function createLifecycle({
* @param {{ engine?: object }} [context] - LayerLifecycle's context. * @param {{ engine?: object }} [context] - LayerLifecycle's context.
*/ */
init(viewer, { engine = null } = {}) { init(viewer, { engine = null } = {}) {
if (flightState.initialized) throw new Error('Flight layer is already initialized'); if (flightState.initialized)
throw new Error('Flight layer is already initialized');
if (typeof flightState.feed._source?.getSnapshot !== 'function') if (typeof flightState.feed._source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required'); throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted) if (flightState.lifetime.signal.aborted)
+1 -2
View File
@@ -112,8 +112,7 @@ export function createFlightSnapshotRenderer({
// subpoint + collected floor cells for the viewer-proximate low-contact // subpoint + collected floor cells for the viewer-proximate low-contact
// clamp below — one carto read per poll, one batch warm after the loop. // clamp below — one carto read per poll, one batch warm after the loop.
const viewerCarto = const viewerCarto =
flightState._renderer?.view.camera?.positionCartographic || flightState._renderer?.view.camera?.positionCartographic || null;
null;
const viewerLatDeg = viewerCarto const viewerLatDeg = viewerCarto
? geo.math.toDegrees(viewerCarto.latitude) ? geo.math.toDegrees(viewerCarto.latitude)
: null; : null;
+9 -2
View File
@@ -157,10 +157,17 @@ test('cancellation during parsing releases every packed-image URL', async (t) =>
test('a KMZ delivered between planets is reused by the new scene and releases its icon on disable', async (t) => { test('a KMZ delivered between planets is reused by the new scene and releases its icon on disable', async (t) => {
let layer; let layer;
const h = fixture(t, { beforeFileReturns() { layer.detachRenderer(); } }); const h = fixture(t, {
beforeFileReturns() {
layer.detachRenderer();
},
});
layer = h.layer; layer = h.layer;
await layer.update(); await layer.update();
assert.deepEqual(layer.getAnalystRecords().map(r => r.name), ['Test report']); assert.deepEqual(
layer.getAnalystRecords().map((r) => r.name),
['Test report'],
);
assert.equal(h.mounted.size, 0); assert.equal(h.mounted.size, 0);
await layer.attachRenderer(h.viewer); await layer.attachRenderer(h.viewer);
assert.equal(h.mounted.size, 1); assert.equal(h.mounted.size, 1);
+13 -8
View File
@@ -32,13 +32,11 @@ export function createGhostMapsNavaraRenderer(engine, view) {
function add(style, geom, id) { function add(style, geom, id) {
const key = JSON.stringify(style); const key = JSON.stringify(style);
if (!groups.has(key)) groups.set(key, { style, features: [] }); if (!groups.has(key)) groups.set(key, { style, features: [] });
groups groups.get(key).features.push({
.get(key) type: 'Feature',
.features.push({ geometry: geom,
type: 'Feature', properties: { gevId: id },
geometry: geom, });
properties: { gevId: id },
});
} }
for (const [index, feature] of features.entries()) { for (const [index, feature] of features.entries()) {
const { style } = feature; const { style } = feature;
@@ -51,7 +49,14 @@ export function createGhostMapsNavaraRenderer(engine, view) {
}; };
for (const [part, geometry] of feature.geometries.entries()) { for (const [part, geometry] of feature.geometries.entries()) {
const id = `ghostmaps:${productId}:${index}:${part}`; const id = `ghostmaps:${productId}:${index}:${part}`;
shapes.push({ id, geometry, style: { ...style, pointRadius: 10 * clamp((style.iconScale ?? 1) * 0.7, 0.35, 1.5) } }); shapes.push({
id,
geometry,
style: {
...style,
pointRadius: 10 * clamp((style.iconScale ?? 1) * 0.7, 0.35, 1.5),
},
});
if (geometry.type === 'point') { if (geometry.type === 'point') {
const color = const color =
style.iconColor && style.icon ? style.iconColor : null; style.iconColor && style.icon ? style.iconColor : null;
+6 -2
View File
@@ -171,8 +171,12 @@ export function createGnssIntegrityLayer({
if (!initialized || renderer) if (!initialized || renderer)
throw new Error('Invalid GNSS scene attachment'); throw new Error('Invalid GNSS scene attachment');
renderer = createRenderer({ viewer, engine, viewAnchor }); renderer = createRenderer({ viewer, engine, viewAnchor });
_removeCameraEnd = renderer.camera?.moveEnd?.addEventListener(scheduleCameraRefresh); _removeCameraEnd = renderer.camera?.moveEnd?.addEventListener(
_removeCameraChanged = renderer.camera?.changed?.addEventListener(scheduleCameraRefresh); scheduleCameraRefresh,
);
_removeCameraChanged = renderer.camera?.changed?.addEventListener(
scheduleCameraRefresh,
);
_signature = null; _signature = null;
if (_enabled) expire(); if (_enabled) expire();
renderer.setVisible(_enabled); renderer.setVisible(_enabled);
+8 -2
View File
@@ -700,8 +700,14 @@ test('view anchor falls back to rectangle, then nadir, then no camera', () => {
}); });
test('an in-flight regional response supplies the next renderer without a new request', async () => { test('an in-flight regional response supplies the next renderer without a new request', async () => {
let resolve, calls = 0; let resolve,
const h = harness(() => { calls++; return new Promise(done => { resolve = done; }); }); calls = 0;
const h = harness(() => {
calls++;
return new Promise((done) => {
resolve = done;
});
});
try { try {
h.layer.enable(); h.layer.enable();
const pending = h.layer.update(); const pending = h.layer.update();
+8 -3
View File
@@ -28,7 +28,8 @@ export function createInstallationsLayer({
let renderer = null; let renderer = null;
const lifecycle = { const lifecycle = {
init(viewer, options = {}) { init(viewer, options = {}) {
if (viewer || options.engine) return lifecycle.attachRenderer(viewer, options); if (viewer || options.engine)
return lifecycle.attachRenderer(viewer, options);
}, },
attachRenderer(viewer, options = {}) { attachRenderer(viewer, options = {}) {
if (renderer) if (renderer)
@@ -41,7 +42,9 @@ export function createInstallationsLayer({
const ready = renderer.lifecycle.methods.init(viewer, options); const ready = renderer.lifecycle.methods.init(viewer, options);
if (state.enabled) { if (state.enabled) {
renderer.lifecycle.methods.enable(); renderer.lifecycle.methods.enable();
renderer.rendering.renderRecords({ claimSelection: Boolean(state.selectedId) }); renderer.rendering.renderRecords({
claimSelection: Boolean(state.selectedId),
});
} }
return ready; return ready;
}, },
@@ -63,7 +66,9 @@ export function createInstallationsLayer({
state.coverage = { kind: 'viewport' }; state.coverage = { kind: 'viewport' };
clearTimeout(state.timer); clearTimeout(state.timer);
parts.viewport.clearUnavailableRetry(); parts.viewport.clearUnavailableRetry();
services.context.clearSelectedEntityContextForLayer('military-installations'); services.context.clearSelectedEntityContextForLayer(
'military-installations',
);
parts.ingestion.setInstallationStatus('idle'); parts.ingestion.setInstallationStatus('idle');
}, },
detachRenderer(viewer) { detachRenderer(viewer) {
@@ -355,7 +355,8 @@ export function createRendering({
if (!cold.length) return; if (!cold.length) return;
const owner = layerState.dataSource; const owner = layerState.dataSource;
warmFireAnchorFloors(cold).then(() => { warmFireAnchorFloors(cold).then(() => {
if (!layerState.enabled || !owner || layerState.dataSource !== owner) return; if (!layerState.enabled || !owner || layerState.dataSource !== owner)
return;
if ( if (
!cold.some((point) => cachedGroundFloor(point.lat, point.lon) != null) !cold.some((point) => cachedGroundFloor(point.lat, point.lon) != null)
) )
+4 -1
View File
@@ -21,7 +21,10 @@ export function createViewport({ state: layerState, services, parts, source }) {
} }
if (viewer || layerState.engine) if (viewer || layerState.engine)
layerState.lastViewBox = viewportBox(viewer); layerState.lastViewBox = viewportBox(viewer);
return { box: layerState.lastViewBox ?? null, coverage: { kind: 'viewport' } }; return {
box: layerState.lastViewBox ?? null,
coverage: { kind: 'viewport' },
};
} }
function viewportBox(viewer) { function viewportBox(viewer) {
+6 -2
View File
@@ -100,7 +100,7 @@ export function createRendering({
: satelliteTrack?.orbitPath?.length > 1 : satelliteTrack?.orbitPath?.length > 1
? satelliteTrack.orbitPath ? satelliteTrack.orbitPath
: parts.paths.approximateOrbitPath(launch); : parts.paths.approximateOrbitPath(launch);
const solution = {activeTleText, satelliteTrack, orbitPath}; const solution = { activeTleText, satelliteTrack, orbitPath };
orbitSolutions.set(launch, solution); orbitSolutions.set(launch, solution);
return solution; return solution;
} }
@@ -117,7 +117,11 @@ export function createRendering({
const orbitAllowed = parts.policyHelpers.launchStatusAllowsOrbit( const orbitAllowed = parts.policyHelpers.launchStatusAllowsOrbit(
launch.status, launch.status,
); );
const { satelliteTrack, orbitPath } = orbitForLaunch(launch, activeTleText, orbitAllowed); const { satelliteTrack, orbitPath } = orbitForLaunch(
launch,
activeTleText,
orbitAllowed,
);
launch.recoveryStages.forEach((stage) => { launch.recoveryStages.forEach((stage) => {
stage.endpoint = parts.paths.landingEndpoint( stage.endpoint = parts.paths.landingEndpoint(
stage, stage,
+4 -1
View File
@@ -15,7 +15,10 @@ export function createLifecycle({
const { setMilitaryLayerActive } = services.militaryRegistry; const { setMilitaryLayerActive } = services.militaryRegistry;
const binding = createAircraftSceneBinding({ const binding = createAircraftSceneBinding({
id: 'military', flightState, services, parts, id: 'military',
flightState,
services,
parts,
resolveModel: () => resolveAsset(JET_MODEL_URL), resolveModel: () => resolveAsset(JET_MODEL_URL),
enable: () => methods.enable(), enable: () => methods.enable(),
}); });
+2 -1
View File
@@ -383,7 +383,8 @@ export function createQueries({
const trackedPos = isTracked const trackedPos = isTracked
? parts.motion._trackedDisplayCached() ? parts.motion._trackedDisplayCached()
: null; // cached, no recompute (anti-jitter) : null; // cached, no recompute (anti-jitter)
const pos = trackedPos || bb?.position || parts.motion._deadReckon(icao24); const pos =
trackedPos || bb?.position || parts.motion._deadReckon(icao24);
if (!pos) continue; if (!pos) continue;
const distance = geo.cartesian3.distance(center, pos); const distance = geo.cartesian3.distance(center, pos);
+16 -7
View File
@@ -294,12 +294,20 @@ test('the military logic draws and follows on the Navara renderer; the follow pa
}); });
test('military classification and the selected aircraft survive an update between scenes', async () => { test('military classification and the selected aircraft survive an update between scenes', async () => {
const first = createFakeEngine(), second = createFakeEngine(); const first = createFakeEngine(),
let polls = 0, binding; second = createFakeEngine();
let polls = 0,
binding;
const layer = createApplicationMilitary({ const layer = createApplicationMilitary({
surface: defaultSurface, militaryRegistry, surface: defaultSurface,
source: { getSnapshot: async () => militarySnapshotOf(['ae0001'], 1700000000000 + (++polls)*1000) }, militaryRegistry,
renderer: {create: ({engine}) => (binding = recordingAircraftRenderer(engine))}, source: {
getSnapshot: async () =>
militarySnapshotOf(['ae0001'], 1700000000000 + ++polls * 1000),
},
renderer: {
create: ({ engine }) => (binding = recordingAircraftRenderer(engine)),
},
}); });
try { try {
layer.init(null); layer.init(null);
@@ -314,7 +322,7 @@ test('military classification and the selected aircraft survive an update betwee
assert.equal(militaryRegistry.isMilitaryLayerActive(), true); assert.equal(militaryRegistry.isMilitaryLayerActive(), true);
assert.equal(layer.getTrackedInfo().icao24, 'ae0001'); assert.equal(layer.getTrackedInfo().icao24, 'ae0001');
await layer.update(null); await layer.update(null);
layer.attachRenderer(null, {engine: second}); layer.attachRenderer(null, { engine: second });
assert.ok(binding.follow.current()); assert.ok(binding.follow.current());
assert.equal(layer.getStats().lastUpdate, 1700000002000); assert.equal(layer.getStats().lastUpdate, 1700000002000);
assert.equal(polls, 2); assert.equal(polls, 2);
@@ -323,6 +331,7 @@ test('military classification and the selected aircraft survive an update betwee
assert.equal(layer.hasContact('ae0001'), null); assert.equal(layer.hasContact('ae0001'), null);
} finally { } finally {
layer.destroy(null); layer.destroy(null);
first.destroy(); second.destroy(); first.destroy();
second.destroy();
} }
}); });
+7 -2
View File
@@ -93,7 +93,11 @@ export function createMilitarySnapshotRenderer({
function presentRecords() { function presentRecords() {
for (const [id, meta] of records.data) { for (const [id, meta] of records.data) {
const position = geo.cartesian3.fromDegrees(meta.rawLon, meta.rawLat, meta.renderAltitudeM); const position = geo.cartesian3.fromDegrees(
meta.rawLon,
meta.rawLat,
meta.renderAltitudeM,
);
presentContact(id, meta, position, null, false); presentContact(id, meta, position, null, false);
} }
} }
@@ -195,7 +199,8 @@ export function createMilitarySnapshotRenderer({
} }
} }
if (flightState._renderer) presentContact(icao24, meta, position, prevMeta, groundFlipped); if (flightState._renderer)
presentContact(icao24, meta, position, prevMeta, groundFlipped);
// Takeoff while TRACKED: ground traffic drew no trail, so start one // Takeoff while TRACKED: ground traffic drew no trail, so start one
// from the fresh airborne history (touchdown needs no action — the // from the fresh airborne history (touchdown needs no action — the
// append gate above simply freezes the existing trail). // append gate above simply freezes the existing trail).
+4 -1
View File
@@ -840,7 +840,10 @@ export function createTracking({
* @param {string} icao24 - ICAO hex identifier of the aircraft to track * @param {string} icao24 - ICAO hex identifier of the aircraft to track
*/ */
function _trackFlight(icao24, { origin = 'programmatic', resume = false, follow = true } = {}) { function _trackFlight(
icao24,
{ origin = 'programmatic', resume = false, follow = true } = {},
) {
if (!resume) _clearTracking(false, { origin }); // switching planes — the new follow-camera takes over if (!resume) _clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
+30 -9
View File
@@ -31,10 +31,20 @@ const INCIWEB_INDEX_TTL_MS = 6 * 3600000;
function samePolygons(left, right) { function samePolygons(left, right) {
if (left === right) return true; if (left === right) return true;
return left?.length === right.length && right.every((rings, i) => return (
left[i].length === rings.length && rings.every((ring, j) => left?.length === right.length &&
left[i][j].length === ring.length && ring.every(([x, y], k) => right.every(
left[i][j][k][0] === x && left[i][j][k][1] === y))); (rings, i) =>
left[i].length === rings.length &&
rings.every(
(ring, j) =>
left[i][j].length === ring.length &&
ring.every(
([x, y], k) => left[i][j][k][0] === x && left[i][j][k][1] === y,
),
),
)
);
} }
/** Own one fire-perimeter display, its refresh lifecycle, and click selection. */ /** Own one fire-perimeter display, its refresh lifecycle, and click selection. */
@@ -184,7 +194,10 @@ export function createFirePerimetersLayer({
const pickId = picking.resolvePickId(picked); const pickId = picking.resolvePickId(picked);
if (typeof pickId !== 'string' || !pickId.startsWith(PICK_PREFIX)) if (typeof pickId !== 'string' || !pickId.startsWith(PICK_PREFIX))
return null; return null;
const incidentId = pickId.slice(PICK_PREFIX.length, pickId.lastIndexOf(':')); const incidentId = pickId.slice(
PICK_PREFIX.length,
pickId.lastIndexOf(':'),
);
return _rowById.has(incidentId) ? incidentId : null; return _rowById.has(incidentId) ? incidentId : null;
} }
@@ -256,7 +269,8 @@ export function createFirePerimetersLayer({
updateInterval: 300000, updateInterval: 300000,
init(viewer, { engine = null } = {}) { init(viewer, { engine = null } = {}) {
if (initialized) throw new Error('Fire perimeter layer is already initialized'); if (initialized)
throw new Error('Fire perimeter layer is already initialized');
initialized = true; initialized = true;
try { try {
if (viewer || engine) this.attachRenderer(viewer, { engine }); if (viewer || engine) this.attachRenderer(viewer, { engine });
@@ -267,8 +281,13 @@ export function createFirePerimetersLayer({
}, },
attachRenderer(viewer, { engine = null } = {}) { attachRenderer(viewer, { engine = null } = {}) {
if (!initialized || renderer) throw new Error('Invalid fire perimeter scene attachment'); if (!initialized || renderer)
renderer = createRenderer({ viewer, engine, screenSpaceEventHandlerFactory }); throw new Error('Invalid fire perimeter scene attachment');
renderer = createRenderer({
viewer,
engine,
screenSpaceEventHandlerFactory,
});
renderer.setVisible(_enabled); renderer.setVisible(_enabled);
if (geometry.size) renderer.replace([...geometry.values()]); if (geometry.size) renderer.replace([...geometry.values()]);
if (_enabled) { if (_enabled) {
@@ -333,7 +352,9 @@ export function createFirePerimetersLayer({
); );
if ( if (
signature === _snapshotSignature && signature === _snapshotSignature &&
rows.every(row => samePolygons(geometry.get(row.stableId)?.polygons, row.polygons)) rows.every((row) =>
samePolygons(geometry.get(row.stableId)?.polygons, row.polygons),
)
) { ) {
if (_selectedId) publishSelectedCard(); if (_selectedId) publishSelectedCard();
_lastUpdate = updatedAt; _lastUpdate = updatedAt;
+24 -8
View File
@@ -590,22 +590,38 @@ test('a selected fire retains its card and changed polygon through a scene hando
const fire = { ...row, stableId: 'incident:one' }; const fire = { ...row, stableId: 'incident:one' };
let geometry = fire.polygons; let geometry = fire.polygons;
let polls = 0; let polls = 0;
const h = harness({getSnapshot: async () => { polls++; return [{...fire, polygons: geometry}]; }}, { const h = harness(
pick: () => ({id: 'fire-perimeter:incident:one:0'}), {
}); getSnapshot: async () => {
polls++;
return [{ ...fire, polygons: geometry }];
},
},
{
pick: () => ({ id: 'fire-perimeter:incident:one:0' }),
},
);
try { try {
await h.layer.update(); await h.layer.update();
h.clicks.handler({position: {x: 0, y: 0}}); h.clicks.handler({ position: { x: 0, y: 0 } });
assert.equal(h.overlay.entries.get('fire-perimeters')[0].id, 'fire-perimeter-card:incident:one'); assert.equal(
h.overlay.entries.get('fire-perimeters')[0].id,
'fire-perimeter-card:incident:one',
);
h.layer.detachRenderer(); h.layer.detachRenderer();
assert.equal(h.sources.length, 0); assert.equal(h.sources.length, 0);
geometry = [[ring.map(([lon, lat]) => [lon+0.1,lat])]]; geometry = [[ring.map(([lon, lat]) => [lon + 0.1, lat])]];
assert.equal(await h.layer.update(), true); assert.equal(await h.layer.update(), true);
const record = h.layer.getAnalystRecords()[0]; const record = h.layer.getAnalystRecords()[0];
assert.equal(record.id, 'incident:one'); assert.equal(record.id, 'incident:one');
h.layer.attachRenderer(h.viewer); h.layer.attachRenderer(h.viewer);
assert.equal(h.sources[0].entities.values.length, 1); assert.equal(h.sources[0].entities.values.length, 1);
assert.equal(h.overlay.entries.get('fire-perimeters')[0].id, 'fire-perimeter-card:incident:one'); assert.equal(
h.overlay.entries.get('fire-perimeters')[0].id,
'fire-perimeter-card:incident:one',
);
assert.equal(polls, 2); assert.equal(polls, 2);
} finally { h.layer.destroy(); } } finally {
h.layer.destroy();
}
}); });
+4 -1
View File
@@ -46,7 +46,10 @@ export function createRendering({
function publishRadioOverlayEntries() { function publishRadioOverlayEntries() {
layerState._overlayPublishTimer = null; layerState._overlayPublishTimer = null;
if (!parts.renderer?.ready || !parts.interaction.radioPresentationAllowed()) { if (
!parts.renderer?.ready ||
!parts.interaction.radioPresentationAllowed()
) {
parts.clustering.resetRadioClusterOverlayIdentities(); parts.clustering.resetRadioClusterOverlayIdentities();
layerState._overlayDiagnostics = layerState._overlayDiagnostics =
parts.clustering.emptyRadioOverlayDiagnostics(); parts.clustering.emptyRadioOverlayDiagnostics();
+1 -1
View File
@@ -830,7 +830,7 @@ export function createRecentImageryLayer({
forgetLease(); forgetLease();
renderer?.destroy(); renderer?.destroy();
renderer = null; renderer = null;
_hostInfo = {collection: null, kind: 'none'}; _hostInfo = { collection: null, kind: 'none' };
_viewer = _engine = _tileset = null; _viewer = _engine = _tileset = null;
_keptGeneration = null; _keptGeneration = null;
await Promise.all([lease?.release(), _releasing]); await Promise.all([lease?.release(), _releasing]);
+2 -1
View File
@@ -266,7 +266,8 @@ export function createControls({ state: layerState, services, parts, source }) {
!layerState._points.has(id) !layerState._points.has(id)
) )
return false; return false;
if (layerState._restoringViewport && layerState._trackedNorad === id) return true; if (layerState._restoringViewport && layerState._trackedNorad === id)
return true;
parts.tracking._cancelPendingTrackingRestore(); parts.tracking._cancelPendingTrackingRestore();
parts.tracking._trackSatellite(id, { origin }); parts.tracking._trackSatellite(id, { origin });
return layerState._trackedNorad === id; return layerState._trackedNorad === id;
+53 -12
View File
@@ -73,22 +73,47 @@ export function createLifecycle({
detachRenderer() { detachRenderer() {
const renderer = layerState._renderer; const renderer = layerState._renderer;
if (!renderer) return; if (!renderer) return;
layerState._waitForRenderer ??= new Promise(resolve => { layerState._resumeRenderer = resolve; }); layerState._waitForRenderer ??= new Promise((resolve) => {
layerState._resumeRenderer = resolve;
});
layerState._clickHandler?.destroy(); layerState._clickHandler?.destroy();
layerState._trackedEntityChangedRemove?.(); layerState._trackedEntityChangedRemove?.();
layerState._preRenderListener?.(); layerState._preRenderListener?.();
layerState._clickHandler = layerState._trackedEntityChangedRemove = layerState._preRenderListener = null; layerState._clickHandler =
layerState._trackedEntityChangedRemove =
layerState._preRenderListener =
null;
document.removeEventListener('keydown', parts.interaction._onKeyDown); document.removeEventListener('keydown', parts.interaction._onKeyDown);
unregisterPickOwner('satellites'); unregisterPickOwner('satellites');
if (layerState._trackedEntity) { if (layerState._trackedEntity) {
layerState._handoffOrigin = layerState._trackedEntity.gevSelectionOrigin; layerState._handoffOrigin =
layerState._trackedEntity.gevSelectionOrigin;
renderer.follow.release(); renderer.follow.release();
renderer.removeTracked(layerState._trackedEntity); renderer.removeTracked(layerState._trackedEntity);
layerState._trackedEntity = null; layerState._trackedEntity = null;
} }
for (const [id, point] of layerState._points) { for (const [id, point] of layerState._points) {
const {position, show, pixelSize, color, outlineColor, outlineWidth, scaleByDistance, disableDepthTestDistance} = point; const {
layerState._points.set(id, {id, position: {...position}, show, pixelSize, color, outlineColor, outlineWidth, scaleByDistance, disableDepthTestDistance}); position,
show,
pixelSize,
color,
outlineColor,
outlineWidth,
scaleByDistance,
disableDepthTestDistance,
} = point;
layerState._points.set(id, {
id,
position: { ...position },
show,
pixelSize,
color,
outlineColor,
outlineWidth,
scaleByDistance,
disableDepthTestDistance,
});
} }
for (const path of layerState._orbitPaths.values()) { for (const path of layerState._orbitPaths.values()) {
renderer.removeOrbit(path.primitive); renderer.removeOrbit(path.primitive);
@@ -97,23 +122,36 @@ export function createLifecycle({
layerState._detectionObjects.clear(); layerState._detectionObjects.clear();
renderer.removePoints(layerState._pointCollection); renderer.removePoints(layerState._pointCollection);
renderer.destroy(); renderer.destroy();
layerState._engine = layerState._viewer = layerState._renderer = layerState._pointCollection = null; layerState._engine =
layerState._viewer =
layerState._renderer =
layerState._pointCollection =
null;
layerState._overlayHost.clearSource(ISS_OVERLAY_SOURCE_ID); layerState._overlayHost.clearSource(ISS_OVERLAY_SOURCE_ID);
if (layerState._enabled) releaseContinuousRender('satellites'); if (layerState._enabled) releaseContinuousRender('satellites');
}, },
attachRenderer(viewer, {engine = null, waitForCameraRestore = false} = {}) { attachRenderer(
viewer,
{ engine = null, waitForCameraRestore = false } = {},
) {
layerState._engine = engine; layerState._engine = engine;
const renderer = services.renderer.create({viewer,engine}); const renderer = services.renderer.create({ viewer, engine });
layerState._renderer = renderer; layerState._renderer = renderer;
layerState._viewer = renderer.view; layerState._viewer = renderer.view;
layerState._pointCollection = renderer.createPoints(); layerState._pointCollection = renderer.createPoints();
for (const [id, spec] of layerState._points) for (const [id, spec] of layerState._points)
layerState._points.set(id, layerState._pointCollection.add(spec)); layerState._points.set(id, layerState._pointCollection.add(spec));
layerState._pointCollection.show = layerState._enabled && layerState._params.showPoints; layerState._pointCollection.show =
layerState._enabled && layerState._params.showPoints;
for (const [id, path] of layerState._orbitPaths) { for (const [id, path] of layerState._orbitPaths) {
path.primitive = renderer.createOrbit({ positions: path.basePositions, width: id === ISS_NORAD ? 2.5 : 2, color: path.color }); path.primitive = renderer.createOrbit({
path.primitive.show = layerState._enabled && layerState._params.showOrbits; positions: path.basePositions,
width: id === ISS_NORAD ? 2.5 : 2,
color: path.color,
});
path.primitive.show =
layerState._enabled && layerState._params.showOrbits;
} }
// Source work can publish as soon as GPU collections exist. Restoring // Source work can publish as soon as GPU collections exist. Restoring
// share-selected camera state may itself wait for that publication. // share-selected camera state may itself wait for that publication.
@@ -129,7 +167,10 @@ export function createLifecycle({
layerState._restoringViewport = false; layerState._restoringViewport = false;
if (layerState._enabled) { if (layerState._enabled) {
if (layerState._trackedNorad != null && !layerState._trackedEntity) if (layerState._trackedNorad != null && !layerState._trackedEntity)
parts.tracking._trackSatellite(layerState._trackedNorad, {origin: layerState._handoffOrigin, resuming: true}); parts.tracking._trackSatellite(layerState._trackedNorad, {
origin: layerState._handoffOrigin,
resuming: true,
});
this.enable(layerState._viewer); this.enable(layerState._viewer);
} }
layerState._resumeRenderer?.(); layerState._resumeRenderer?.();
+4 -1
View File
@@ -412,7 +412,10 @@ export function createTracking({
refreshTrackedReadout(layerState._trackedEntity); refreshTrackedReadout(layerState._trackedEntity);
} }
function _trackSatellite(noradId, { origin = 'programmatic', resuming = false } = {}) { function _trackSatellite(
noradId,
{ origin = 'programmatic', resuming = false } = {},
) {
if (!resuming) _clearTracking(false, { origin }); if (!resuming) _clearTracking(false, { origin });
const point = layerState._points.get(noradId); const point = layerState._points.get(noradId);
+20 -14
View File
@@ -2,15 +2,8 @@
import { Effect, EffectAttribute } from 'postprocessing'; import { Effect, EffectAttribute } from 'postprocessing';
import { Matrix4, Uniform } from 'three'; import { Matrix4, Uniform } from 'three';
import { rectangleFrame } from './navaraCellGeometry.js'; import { rectangleFrame } from './navaraCellGeometry.js';
import { import { viewFrame, groundClipBounds } from './navaraGroundGeometry.js';
viewFrame, import { createGroundGrid, GRID_COLS, GRID_ROWS } from './navaraGroundGrid.js';
groundClipBounds,
} from './navaraGroundGeometry.js';
import {
createGroundGrid,
GRID_COLS,
GRID_ROWS,
} from './navaraGroundGrid.js';
import { import {
createGroundTexture, createGroundTexture,
createGroundStorage, createGroundStorage,
@@ -269,26 +262,39 @@ function groundEffectClass(EffectDesc, NavaraEffect) {
// The SDK allows one active effect per key. Compose enabled datasets in // The SDK allows one active effect per key. Compose enabled datasets in
// catalog order so both can be drawn without replacing one another. // catalog order so both can be drawn without replacing one another.
const views = new WeakMap(); const views = new WeakMap();
export function createGroundCellDisplay(engine, { view, EffectDesc, NavaraEffect }, order) { export function createGroundCellDisplay(
engine,
{ view, EffectDesc, NavaraEffect },
order,
) {
if (!registeredViews.has(view)) { if (!registeredViews.has(view)) {
view.registerEffect(KEY, groundEffectClass(EffectDesc, NavaraEffect)); view.registerEffect(KEY, groundEffectClass(EffectDesc, NavaraEffect));
registeredViews.add(view); registeredViews.add(view);
} }
let pass = views.get(view); let pass = views.get(view);
if (!pass) { if (!pass) {
pass = { inputs: new Set(), owner: { state: buildState([]) }, handle: null }; pass = {
inputs: new Set(),
owner: { state: buildState([]) },
handle: null,
};
views.set(view, pass); views.set(view, pass);
} }
const input = { cells: [], visible: false, order }; const input = { cells: [], visible: false, order };
pass.inputs.add(input); pass.inputs.add(input);
function sync() { function sync() {
const cells = [...pass.inputs].filter(item => item.visible) const cells = [...pass.inputs]
.sort((a,b) => a.order - b.order).flatMap(item => item.cells); .filter((item) => item.visible)
.sort((a, b) => a.order - b.order)
.flatMap((item) => item.cells);
const previous = pass.owner.state; const previous = pass.owner.state;
pass.owner.state = buildState(cells); pass.owner.state = buildState(cells);
try { try {
if (!pass.handle && cells.length) { if (!pass.handle && cells.length) {
pass.handle = view.addEffect({ [KEY]: { owner: pass.owner }, visible: true }); pass.handle = view.addEffect({
[KEY]: { owner: pass.owner },
visible: true,
});
} else { } else {
pass.handle?.update({ visible: cells.length > 0 }); pass.handle?.update({ visible: cells.length > 0 });
} }
+22 -5
View File
@@ -30,7 +30,10 @@ export function createCameraFraming({ state }) {
const scene = state.viewer?.scene; const scene = state.viewer?.scene;
const carto = Cesium.Cartographic.fromDegrees(lon, lat); const carto = Cesium.Cartographic.fromDegrees(lon, lat);
const engine = state.viewer?.engine; const engine = state.viewer?.engine;
let height = engine ? engine.surface.sampleHeight(carto) ?? engine.surface.terrainHeight(carto) : sampledSurfaceHeight(scene, carto); let height = engine
? (engine.surface.sampleHeight(carto) ??
engine.surface.terrainHeight(carto))
: sampledSurfaceHeight(scene, carto);
if (height === null) height = scene?.globe?.getHeight?.(carto); if (height === null) height = scene?.globe?.getHeight?.(carto);
if (!plausibleSurfaceHeight(height)) height = fallback; if (!plausibleSurfaceHeight(height)) height = fallback;
return plausibleSurfaceHeight(height) ? height : 0; return plausibleSurfaceHeight(height) ? height : 0;
@@ -55,10 +58,24 @@ export function createCameraFraming({ state }) {
if (flight === current) flight = null; if (flight === current) flight = null;
}; };
if (state.viewer.engine) { if (state.viewer.engine) {
state.viewer.engine.camera.flyToSphere({ state.viewer.engine.camera.flyToSphere(
center:Cesium.Cartesian3.fromDegrees(position.lon,position.lat,ground+2),radius:4 {
}, {heading:Cesium.Math.toRadians(bearing || 0),pitch:Cesium.Math.toRadians(-32),range:140,duration:1.6, center: Cesium.Cartesian3.fromDegrees(
complete:release,cancel:release}); position.lon,
position.lat,
ground + 2,
),
radius: 4,
},
{
heading: Cesium.Math.toRadians(bearing || 0),
pitch: Cesium.Math.toRadians(-32),
range: 140,
duration: 1.6,
complete: release,
cancel: release,
},
);
return; return;
} }
state.viewer.camera.flyToBoundingSphere( state.viewer.camera.flyToBoundingSphere(
+4 -1
View File
@@ -11,7 +11,10 @@ export function createCredits() {
function show(viewer, def) { function show(viewer, def) {
if (shown.has(def.id)) return; if (shown.has(def.id)) return;
if (viewer?.engine) { if (viewer?.engine) {
shown.set(def.id, viewer.engine.credits.addStatic(def.credit.html, { onScreen: true })); shown.set(
def.id,
viewer.engine.credits.addStatic(def.credit.html, { onScreen: true }),
);
return; return;
} }
if (!viewer?.creditDisplay) return; if (!viewer?.creditDisplay) return;
+3 -1
View File
@@ -32,7 +32,9 @@ export function createGroundClick({ state, openNearest, isAvailable }) {
) )
.map((entry) => entry.def.id); .map((entry) => entry.def.id);
if (!providerIds.length) return false; if (!providerIds.length) return false;
const position = state.viewer.engine ? state.viewer.engine.picker.pickGlobe(screenPosition) : scenePick.pickGroundPosition(state.viewer, screenPosition); const position = state.viewer.engine
? state.viewer.engine.picker.pickGlobe(screenPosition)
: scenePick.pickGroundPosition(state.viewer, screenPosition);
const carto = position && geo.cartographic.fromCartesian(position); const carto = position && geo.cartographic.fromCartesian(position);
if (!carto) return false; if (!carto) return false;
return openNearest( return openNearest(
+34 -22
View File
@@ -195,7 +195,8 @@ export function createStreetLevelLayer({
cancelGroundRetry(); cancelGroundRetry();
const ready = state.enabled && groundClickHeightOk(state.viewer); const ready = state.enabled && groundClickHeightOk(state.viewer);
if ( if (
state.enabled && state.viewer && state.enabled &&
state.viewer &&
!ready && !ready &&
groundRetries < GROUND_RETRY_LIMIT && groundRetries < GROUND_RETRY_LIMIT &&
groundUnderCamera(state.viewer) === null groundUnderCamera(state.viewer) === null
@@ -266,12 +267,15 @@ export function createStreetLevelLayer({
/** Ground under the screen centre; null for sky, undefined when it cannot aim. */ /** Ground under the screen centre; null for sky, undefined when it cannot aim. */
function screenCentreGround() { function screenCentreGround() {
const canvas = state.viewer?.engine?.host.canvas ?? state.viewer?.scene?.canvas; const canvas =
state.viewer?.engine?.host.canvas ?? state.viewer?.scene?.canvas;
const pick = state.services.scenePick?.pickGroundPosition; const pick = state.services.scenePick?.pickGroundPosition;
if (!pick || !canvas?.clientWidth || !canvas?.clientHeight) if (!pick || !canvas?.clientWidth || !canvas?.clientHeight)
return undefined; return undefined;
const center = { x:canvas.clientWidth/2, y:canvas.clientHeight/2 }; const center = { x: canvas.clientWidth / 2, y: canvas.clientHeight / 2 };
const position = state.viewer.engine ? state.viewer.engine.picker.pickGlobe(center) : pick(state.viewer,center); const position = state.viewer.engine
? state.viewer.engine.picker.pickGlobe(center)
: pick(state.viewer, center);
const carto = position && geo.cartographic.fromCartesian(position); const carto = position && geo.cartographic.fromCartesian(position);
return carto return carto
? { ? {
@@ -485,10 +489,13 @@ export function createStreetLevelLayer({
}, },
init(viewer, options) { init(viewer, options) {
this.attachRenderer(viewer,options); this.attachRenderer(viewer, options);
for (const entry of state.providers.values()) checkStatus(entry); for (const entry of state.providers.values()) checkStatus(entry);
}, },
attachRenderer(viewer, {engine = null, waitForCameraRestore = false} = {}) { attachRenderer(
viewer,
{ engine = null, waitForCameraRestore = false } = {},
) {
const map = viewer ?? services.nativeRenderer?.(engine); const map = viewer ?? services.nativeRenderer?.(engine);
state.viewer = map; state.viewer = map;
waitingForView = waitForCameraRestore; waitingForView = waitForCameraRestore;
@@ -497,45 +504,50 @@ export function createStreetLevelLayer({
parts.marker.setVisible(state.enabled); parts.marker.setVisible(state.enabled);
for (const entry of state.providers.values()) { for (const entry of state.providers.values()) {
entry.instance.init(map); entry.instance.init(map);
if(state.enabled && entry.on){ if (state.enabled && entry.on) {
entry.instance.activate(map); entry.instance.activate(map);
entry.instance.repaintMap?.(); entry.instance.repaintMap?.();
if(providerAvailable(entry)) parts.credits.show(map,entry.def); if (providerAvailable(entry)) parts.credits.show(map, entry.def);
} }
} }
if (state.enabled) { if (state.enabled) {
parts.selection.install(map); parts.selection.install(map);
watchCamera(map); watchCamera(map);
parts.marker.set(state.street.position,state.street.bearing,state.providers.get(state.street.providerId)?.def.marker); parts.marker.set(
state.street.position,
state.street.bearing,
state.providers.get(state.street.providerId)?.def.marker,
);
remeasureGround(); remeasureGround();
} }
}, },
resumeViewport() { resumeViewport() {
if (!waitingForView) return; if (!waitingForView) return;
waitingForView=false; waitingForView = false;
if(state.enabled && state.viewer) { if (state.enabled && state.viewer) {
for(const entry of activeEntries())entry.instance.refreshCoverage?.(); for (const entry of activeEntries()) entry.instance.refreshCoverage?.();
remeasureGround(); remeasureGround();
} }
}, },
detachRenderer() { detachRenderer() {
const map=state.viewer; const map = state.viewer;
if(!map)return; if (!map) return;
waitingForView=true; waitingForView = true;
removeCameraListener?.();removeCameraListener=null; removeCameraListener?.();
removeCameraListener = null;
cancelGroundRetry(); cancelGroundRetry();
parts.framing.cancel(); parts.framing.cancel();
parts.selection.uninstall(); parts.selection.uninstall();
for(const entry of state.providers.values()){ for (const entry of state.providers.values()) {
if(entry.instance.detachMap)entry.instance.detachMap(); if (entry.instance.detachMap) entry.instance.detachMap();
else if(entry.on)entry.instance.deactivate(); else if (entry.on) entry.instance.deactivate();
} }
parts.marker.destroy(map); parts.marker.destroy(map);
parts.credits.hideAll(map); parts.credits.hideAll(map);
unsubscribeMapStack?.(); unsubscribeMapStack?.();
unsubscribeMapStack=null; unsubscribeMapStack = null;
mapStack=null; mapStack = null;
state.viewer=null; state.viewer = null;
}, },
enable(viewer) { enable(viewer) {
state.enabled = true; state.enabled = true;
+25 -7
View File
@@ -246,8 +246,13 @@ export function createLineCoverage({
const sequence = entry.sequenceList.find(({ id }) => id === sequenceId); const sequence = entry.sequenceList.find(({ id }) => id === sequenceId);
if (sequence && passes(sequence, current)) parts.push(...sequence.parts); if (sequence && passes(sequence, current)) parts.push(...sequence.parts);
} }
if(state.viewer?.engine){ if (state.viewer?.engine) {
state.coverage.highlight=state.viewer.lines([{id:sequenceId,parts}],colors.selected,tuning.lineWidthPx+1,pickPrefix); state.coverage.highlight = state.viewer.lines(
[{ id: sequenceId, parts }],
colors.selected,
tuning.lineWidthPx + 1,
pickPrefix,
);
requestRender(); requestRender();
return; return;
} }
@@ -270,8 +275,13 @@ export function createLineCoverage({
// Lookup, picking and counts follow what is drawn, not the whole tile. // Lookup, picking and counts follow what is drawn, not the whole tile.
const sequences = drawnSequences(entry); const sequences = drawnSequences(entry);
entry.sequences = new Map(sequences.map((s) => [s.id, s])); entry.sequences = new Map(sequences.map((s) => [s.id, s]));
if(state.viewer?.engine){ if (state.viewer?.engine) {
const group = state.viewer.lines(sequences,colors.coverage,tuning.lineWidthPx,pickPrefix); const group = state.viewer.lines(
sequences,
colors.coverage,
tuning.lineWidthPx,
pickPrefix,
);
entry.primitives = group ? [group] : []; entry.primitives = group ? [group] : [];
return; return;
} }
@@ -409,7 +419,9 @@ export function createLineCoverage({
*/ */
function wantedTiles(viewer) { function wantedTiles(viewer) {
const ground = groundUnderCamera(viewer); const ground = groundUnderCamera(viewer);
const cameraHeight = viewer.engine?.camera.pose().cartographic.height ?? viewer.camera?.positionCartographic?.height; const cameraHeight =
viewer.engine?.camera.pose().cartographic.height ??
viewer.camera?.positionCartographic?.height;
const height = Number.isFinite(cameraHeight) const height = Number.isFinite(cameraHeight)
? cameraHeight - (ground ?? 0) ? cameraHeight - (ground ?? 0)
: null; : null;
@@ -438,7 +450,13 @@ export function createLineCoverage({
/** Recompute the tile set for the current camera and reconcile primitives. */ /** Recompute the tile set for the current camera and reconcile primitives. */
function refresh() { function refresh() {
const viewer = state.viewer; const viewer = state.viewer;
if (!viewer || state.context.waitingForView?.() || !state.context.isActive() || state.coverage.blocked) return; if (
!viewer ||
state.context.waitingForView?.() ||
!state.context.isActive() ||
state.coverage.blocked
)
return;
const { zoom, tiles, hint } = wantedTiles(viewer); const { zoom, tiles, hint } = wantedTiles(viewer);
if (hint) { if (hint) {
state.coverage.hint = hint; state.coverage.hint = hint;
@@ -550,7 +568,7 @@ export function createLineCoverage({
releaseGraphics() { releaseGraphics() {
detach(); detach();
highlight(null); highlight(null);
for(const entry of state.coverage.tiles.values()) detachPrimitive(entry); for (const entry of state.coverage.tiles.values()) detachPrimitive(entry);
}, },
refresh, refresh,
clear, clear,
@@ -256,7 +256,9 @@ export function createPanoramaxProvider({
* from high up the click may land tens of metres off the line. * from high up the click may land tens of metres off the line.
*/ */
function lineClickRadius() { function lineClickRadius() {
const height = state.viewer?.engine?.camera.pose().cartographic.height ?? state.viewer?.camera?.positionCartographic?.height; const height =
state.viewer?.engine?.camera.pose().cartographic.height ??
state.viewer?.camera?.positionCartographic?.height;
if (!Number.isFinite(height)) return NEAREST_RADIUS_M; if (!Number.isFinite(height)) return NEAREST_RADIUS_M;
return Math.min( return Math.min(
LINE_CLICK_RADIUS_MAX_M, LINE_CLICK_RADIUS_MAX_M,
+19 -18
View File
@@ -10,11 +10,13 @@ export function createSelection({ state, parts }) {
const viewer = state.viewer; const viewer = state.viewer;
if (!viewer || !state.enabled) return; if (!viewer || !state.enabled) return;
if (input?.isPointerFree && !input.isPointerFree()) return; if (input?.isPointerFree && !input.isPointerFree()) return;
const picked = viewer.engine ? click.picked : viewer.scene.pick( const picked = viewer.engine
click.position, ? click.picked
CLICK_PICK_WINDOW_PX, : viewer.scene.pick(
CLICK_PICK_WINDOW_PX, click.position,
); CLICK_PICK_WINDOW_PX,
CLICK_PICK_WINDOW_PX,
);
const id = picking?.resolvePickId const id = picking?.resolvePickId
? picking.resolvePickId(picked) ? picking.resolvePickId(picked)
: picked?.id; : picked?.id;
@@ -33,10 +35,9 @@ export function createSelection({ state, parts }) {
/** Lon/lat of the ground under a screen point, or null (sky, no picker). */ /** Lon/lat of the ground under a screen point, or null (sky, no picker). */
function groundPointAt(viewer, screenPosition) { function groundPointAt(viewer, screenPosition) {
const position = viewer.engine ? viewer.engine.picker.pickGlobe(screenPosition) : state.services.scenePick?.pickGroundPosition?.( const position = viewer.engine
viewer, ? viewer.engine.picker.pickGlobe(screenPosition)
screenPosition, : state.services.scenePick?.pickGroundPosition?.(viewer, screenPosition);
);
const carto = position && Cesium.Cartographic.fromCartesian(position); const carto = position && Cesium.Cartographic.fromCartesian(position);
return carto return carto
? { ? {
@@ -69,16 +70,16 @@ export function createSelection({ state, parts }) {
function install(viewer) { function install(viewer) {
if (state.clickHandler) return; if (state.clickHandler) return;
if (viewer.engine) { if (viewer.engine) {
const remove = viewer.engine.input.on('click',onClick,{pick:true}); const remove = viewer.engine.input.on('click', onClick, { pick: true });
state.clickHandler = { isDestroyed:()=>false,destroy:remove }; state.clickHandler = { isDestroyed: () => false, destroy: remove };
} else { } else {
state.clickHandler = new Cesium.ScreenSpaceEventHandler( state.clickHandler = new Cesium.ScreenSpaceEventHandler(
viewer.scene.canvas, viewer.scene.canvas,
); );
state.clickHandler.setInputAction( state.clickHandler.setInputAction(
onClick, onClick,
Cesium.ScreenSpaceEventType.LEFT_CLICK, Cesium.ScreenSpaceEventType.LEFT_CLICK,
); );
} }
document.addEventListener('keydown', onKeyDown); document.addEventListener('keydown', onKeyDown);
picking?.registerPickOwner?.(STREET_LEVEL_LAYER_ID, parts.router.ownsPick); picking?.registerPickOwner?.(STREET_LEVEL_LAYER_ID, parts.router.ownsPick);
+2 -1
View File
@@ -142,7 +142,8 @@ export function createSequenceCones({
* reloads a selected one (a provider that loads part of a sequence moved on). * reloads a selected one (a provider that loads part of a sequence moved on).
*/ */
async function select(sequenceId, { force = false } = {}) { async function select(sequenceId, { force = false } = {}) {
if (!sequenceId || (!state.viewer && !state.context.waitingForView?.())) return; if (!sequenceId || (!state.viewer && !state.context.waitingForView?.()))
return;
// Already on screen, or already on its way. // Already on screen, or already on its way.
if ( if (
!force && !force &&
+22 -5
View File
@@ -2,7 +2,8 @@ import * as Cesium from 'cesium';
import { wrapLon } from './tileMath.js'; import { wrapLon } from './tileMath.js';
export const cameraLocation = (view) => export const cameraLocation = (view) =>
view?.engine?.camera.pose().cartographic ?? view?.camera?.positionCartographic; view?.engine?.camera.pose().cartographic ??
view?.camera?.positionCartographic;
const canvasOf = (view) => view?.engine?.host.canvas ?? view?.scene?.canvas; const canvasOf = (view) => view?.engine?.host.canvas ?? view?.scene?.canvas;
/** Equirectangular metres between {lon, lat} points, the short way round ±180°; street scale. */ /** Equirectangular metres between {lon, lat} points, the short way round ±180°; street scale. */
export function metresBetween(a, b) { export function metresBetween(a, b) {
@@ -70,7 +71,9 @@ function raisedEllipsoid(ellipsoid, height) {
function groundHit(camera, point, ellipsoid, maxRange, engine) { function groundHit(camera, point, ellipsoid, maxRange, engine) {
let cartesian = null; let cartesian = null;
try { try {
cartesian = engine ? engine.picker.pickGlobe(point) : camera.pickEllipsoid(point, ellipsoid); cartesian = engine
? engine.picker.pickGlobe(point)
: camera.pickEllipsoid(point, ellipsoid);
} catch { } catch {
cartesian = null; cartesian = null;
} }
@@ -78,10 +81,18 @@ function groundHit(camera, point, ellipsoid, maxRange, engine) {
if ( if (
Number.isFinite(maxRange) && Number.isFinite(maxRange) &&
(engine?.camera.livePosition || camera.positionWC) && (engine?.camera.livePosition || camera.positionWC) &&
Cesium.Cartesian3.distance(cartesian, engine?.camera.livePosition ?? camera.positionWC) > maxRange Cesium.Cartesian3.distance(
cartesian,
engine?.camera.livePosition ?? camera.positionWC,
) > maxRange
) )
return null; return null;
return Cesium.Cartographic.fromCartesian(cartesian, engine ? Cesium.Ellipsoid.WGS84 : ellipsoid) || null; return (
Cesium.Cartographic.fromCartesian(
cartesian,
engine ? Cesium.Ellipsoid.WGS84 : ellipsoid,
) || null
);
} }
/** Camera height (m) above the ground under it. */ /** Camera height (m) above the ground under it. */
@@ -120,7 +131,13 @@ export function visibleBbox(
for (let j = 0; j <= BBOX_GRID; j++) { for (let j = 0; j <= BBOX_GRID; j++) {
point.x = (width * i) / BBOX_GRID; point.x = (width * i) / BBOX_GRID;
point.y = (height * j) / BBOX_GRID; point.y = (height * j) / BBOX_GRID;
const carto = groundHit(camera, point, ellipsoid, maxRange, viewer.engine); const carto = groundHit(
camera,
point,
ellipsoid,
maxRange,
viewer.engine,
);
if (carto) hits.push(carto); if (carto) hits.push(carto);
} }
} }
+8 -2
View File
@@ -292,8 +292,14 @@ export function updateCableReferenceTip(
return true; return true;
} }
export function updateCableReferenceStem(record, cameraPositionWC, canvasHeight, fov) { export function updateCableReferenceStem(
if (!updateCableReferenceTip(record, cameraPositionWC, canvasHeight, fov)) return false; record,
cameraPositionWC,
canvasHeight,
fov,
) {
if (!updateCableReferenceTip(record, cameraPositionWC, canvasHeight, fov))
return false;
record.stemPositionBufferIndex = 1 - record.stemPositionBufferIndex; record.stemPositionBufferIndex = 1 - record.stemPositionBufferIndex;
const stemPositions = const stemPositions =
record.stemPositionBuffers[record.stemPositionBufferIndex]; record.stemPositionBuffers[record.stemPositionBufferIndex];
+6 -2
View File
@@ -86,11 +86,15 @@ export const CABLE_GLOBE_STACK_IDS = Object.freeze(
export const MARKER_COLLECTION_EXPECTATIONS = Object.freeze([ export const MARKER_COLLECTION_EXPECTATIONS = Object.freeze([
Object.freeze({ Object.freeze({
key: '_billboardCollection', key: '_billboardCollection',
get type() { return Cesium.BillboardCollection; }, get type() {
return Cesium.BillboardCollection;
},
}), }),
Object.freeze({ Object.freeze({
key: '_pointCollection', key: '_pointCollection',
get type() { return Cesium.PointPrimitiveCollection; }, get type() {
return Cesium.PointPrimitiveCollection;
},
}), }),
]); ]);
+7 -2
View File
@@ -320,7 +320,9 @@ export function createAnimation({
// Jam heat-lines throb (~1.6 s period) — one shared material uniform for // Jam heat-lines throb (~1.6 s period) — one shared material uniform for
// the whole jam batch, no geometry rebuild. Null outside live heatline mode. // the whole jam batch, no geometry rebuild. Null outside live heatline mode.
if (layerState._native && layerState._heatJamPrim) { if (layerState._native && layerState._heatJamPrim) {
layerState._heatJamPrim.setAlpha(HEAT_JAM_BASE_ALPHA + HEAT_JAM_PULSE_ALPHA * Math.sin(now / 260)); layerState._heatJamPrim.setAlpha(
HEAT_JAM_BASE_ALPHA + HEAT_JAM_PULSE_ALPHA * Math.sin(now / 260),
);
} }
if (layerState._heatJamPrim?.appearance) { if (layerState._heatJamPrim?.appearance) {
layerState._heatJamPrim.appearance.material.uniforms.color.alpha = layerState._heatJamPrim.appearance.material.uniforms.color.alpha =
@@ -353,7 +355,10 @@ export function createAnimation({
/** Remove all point primitives and reset dot/road arrays and counters. */ /** Remove all point primitives and reset dot/road arrays and counters. */
function clearDots() { function clearDots() {
services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic'); services.credits?.hideOsmCredit?.(
layerState._engine ?? layerState._viewer,
'traffic',
);
parts.retention.clear(); parts.retention.clear();
if (layerState._dots.length) layerState._motion.rebuilds++; if (layerState._dots.length) layerState._motion.rebuilds++;
if (layerState._pointCollection) layerState._pointCollection.removeAll(); if (layerState._pointCollection) layerState._pointCollection.removeAll();
+12 -4
View File
@@ -87,7 +87,10 @@ export function createFlow({ state: layerState, services, parts, source }) {
layerState._flowStatusKnown = true; layerState._flowStatusKnown = true;
if (layerState._liveMode) { if (layerState._liveMode) {
console.log('[Data:Traffic] TomTom key present — live flow mode'); console.log('[Data:Traffic] TomTom key present — live flow mode');
registerDynamicCredit(layerState._engine ?? layerState._viewer, TOMTOM_CREDIT); registerDynamicCredit(
layerState._engine ?? layerState._viewer,
TOMTOM_CREDIT,
);
} }
}) })
.catch((e) => { .catch((e) => {
@@ -236,7 +239,8 @@ export function createFlow({ state: layerState, services, parts, source }) {
return false; return false;
if (layerState._waitForScene) { if (layerState._waitForScene) {
await layerState._waitForScene; await layerState._waitForScene;
if (generation !== layerState._loadGeneration || signal?.aborted) return false; if (generation !== layerState._loadGeneration || signal?.aborted)
return false;
} }
const revision = (layerState._surfaceRevision || 0) + 1; const revision = (layerState._surfaceRevision || 0) + 1;
layerState._surfaceRevision = revision; layerState._surfaceRevision = revision;
@@ -384,7 +388,9 @@ export function createFlow({ state: layerState, services, parts, source }) {
maxX = -Infinity, maxX = -Infinity,
maxY = -Infinity; maxY = -Infinity;
for (const point of road.waypoints) { for (const point of road.waypoints) {
const screen = scene.project ? scene.project(point, {}) : Cesium.SceneTransforms.worldToWindowCoordinates(scene, point); const screen = scene.project
? scene.project(point, {})
: Cesium.SceneTransforms.worldToWindowCoordinates(scene, point);
if (!screen) continue; if (!screen) continue;
minX = Math.min(minX, screen.x); minX = Math.min(minX, screen.x);
maxX = Math.max(maxX, screen.x); maxX = Math.max(maxX, screen.x);
@@ -411,7 +417,9 @@ export function createFlow({ state: layerState, services, parts, source }) {
roadDotBudget(altitude), roadDotBudget(altitude),
); );
const admitted = selected.filter((r, i) => budgets[i] > 0); const admitted = selected.filter((r, i) => budgets[i] > 0);
const camera = (layerState._engine?.camera.livePosition ?? layerState._viewer?.camera?.positionWC); const camera =
layerState._engine?.camera.livePosition ??
layerState._viewer?.camera?.positionWC;
if (camera) if (camera)
admitted.sort( admitted.sort(
(a, b) => (a, b) =>
+3 -1
View File
@@ -102,7 +102,9 @@ export function createReticleFootprint() {
dlon -= 360 * Math.round(dlon / 360); dlon -= 360 * Math.round(dlon / 360);
dx = dlon * 111.195 * Math.cos(nadir.latitude); dx = dlon * 111.195 * Math.cos(nadir.latitude);
} else { } else {
const picked = engine ? camera.pickRay(p) : camera.getPickRay(p, (ray ??= new Cesium.Ray())) || ray; const picked = engine
? camera.pickRay(p)
: camera.getPickRay(p, (ray ??= new Cesium.Ray())) || ray;
if (!picked) continue; if (!picked) continue;
const d = picked.direction; const d = picked.direction;
dx = dx =
+7 -5
View File
@@ -275,11 +275,13 @@ export function createIngestion({
let estimated = services.ground?.cachedGroundFloor?.(centerLat, centerLon); let estimated = services.ground?.cachedGroundFloor?.(centerLat, centerLon);
if (!Number.isFinite(estimated)) { if (!Number.isFinite(estimated)) {
try { try {
estimated = scene?.engine ? scene.sampleHeight(location) : scene?.globe?.show estimated = scene?.engine
? scene.globe.getHeight?.(location) ? scene.sampleHeight(location)
: scene?.sampleHeightSupported : scene?.globe?.show
? scene.sampleHeight(location, [layerState._pointCollection]) ? scene.globe.getHeight?.(location)
: undefined; : scene?.sampleHeightSupported
? scene.sampleHeight(location, [layerState._pointCollection])
: undefined;
} catch { } catch {
/* the local mesh can still be loading */ /* the local mesh can still be loading */
} }
+114 -40
View File
@@ -21,10 +21,12 @@ export function createLifecycle({
} }
function bindScene() { function bindScene() {
const viewer = layerState._viewer, engine = layerState._engine; const viewer = layerState._viewer,
engine = layerState._engine;
const camera = engine?.camera ?? viewer.camera; const camera = engine?.camera ?? viewer.camera;
const frames = engine?.frames ?? viewer.scene; const frames = engine?.frames ?? viewer.scene;
if (viewer) layerState._surfaceFrameRemover = observeTrafficSurface(viewer.scene); if (viewer)
layerState._surfaceFrameRemover = observeTrafficSurface(viewer.scene);
layerState._pointCollection.show = true; layerState._pointCollection.show = true;
layerState._lastAnimTime = 0; layerState._lastAnimTime = 0;
parts.animation.syncRenderDemand(); parts.animation.syncRenderDemand();
@@ -36,11 +38,17 @@ export function createLifecycle({
parts.timing.clearTrafficTimingEntries(); parts.timing.clearTrafficTimingEntries();
layerState._trafficTimingCurrentAnchor = null; layerState._trafficTimingCurrentAnchor = null;
layerState._trafficTimingPostRenderRemovers = new Set(); layerState._trafficTimingPostRenderRemovers = new Set();
layerState._trafficTimingMoveEndRemover = camera.moveEnd.addEventListener(parts.timing.markTrafficTimingMoveEnd); layerState._trafficTimingMoveEndRemover = camera.moveEnd.addEventListener(
parts.timing.markTrafficTimingMoveEnd,
);
} }
layerState._cameraRemover = camera.changed.addEventListener(parts.viewport.onCameraChanged); layerState._cameraRemover = camera.changed.addEventListener(
layerState._arrivalRemover = camera.moveEnd.addEventListener(() => parts.viewport.onCameraChanged({immediate:true})); parts.viewport.onCameraChanged,
claimCameraSensitivity(engine ?? camera,'traffic',0.05); );
layerState._arrivalRemover = camera.moveEnd.addEventListener(() =>
parts.viewport.onCameraChanged({ immediate: true }),
);
claimCameraSensitivity(engine ?? camera, 'traffic', 0.05);
} }
function unbindScene() { function unbindScene() {
@@ -48,15 +56,22 @@ export function createLifecycle({
layerState._surfaceFrameRemover?.(); layerState._surfaceFrameRemover?.();
layerState._preRenderRemover?.(); layerState._preRenderRemover?.();
layerState._trafficTimingMoveEndRemover?.(); layerState._trafficTimingMoveEndRemover?.();
for (const remove of layerState._trafficTimingPostRenderRemovers || []) remove(); for (const remove of layerState._trafficTimingPostRenderRemovers || [])
remove();
layerState._trafficTimingPostRenderRemovers = null; layerState._trafficTimingPostRenderRemovers = null;
layerState._trafficTimingCurrentAnchor = null; layerState._trafficTimingCurrentAnchor = null;
layerState._cameraRemover?.(); layerState._cameraRemover?.();
// Some legacy camera events do not return a remover. // Some legacy camera events do not return a remover.
if (!layerState._cameraRemover) camera?.changed?.removeEventListener?.(parts.viewport.onCameraChanged); if (!layerState._cameraRemover)
camera?.changed?.removeEventListener?.(parts.viewport.onCameraChanged);
layerState._arrivalRemover?.(); layerState._arrivalRemover?.();
layerState._surfaceFrameRemover = layerState._preRenderRemover = layerState._cameraRemover = layerState._arrivalRemover = layerState._trafficTimingMoveEndRemover = null; layerState._surfaceFrameRemover =
releaseCameraSensitivity(layerState._engine ?? camera,'traffic'); layerState._preRenderRemover =
layerState._cameraRemover =
layerState._arrivalRemover =
layerState._trafficTimingMoveEndRemover =
null;
releaseCameraSensitivity(layerState._engine ?? camera, 'traffic');
} }
const methods = { const methods = {
@@ -99,8 +114,12 @@ export function createLifecycle({
layerState._stylePreset = layerState._stylePreset =
document?.documentElement?.dataset?.gevStyle || 'normal'; document?.documentElement?.dataset?.gevStyle || 'normal';
if (!layerState._styleListenerBound) { if (!layerState._styleListenerBound) {
layerState._styleListener = (e) => parts.style.setStylePreset(e?.detail?.style); layerState._styleListener = (e) =>
window.addEventListener('gev:style-change', layerState._styleListener); parts.style.setStylePreset(e?.detail?.style);
window.addEventListener(
'gev:style-change',
layerState._styleListener,
);
layerState._styleListenerBound = true; layerState._styleListenerBound = true;
} }
} }
@@ -108,25 +127,42 @@ export function createLifecycle({
console.log('[Data:Traffic] Initialized'); console.log('[Data:Traffic] Initialized');
}, },
attachRenderer(viewer, {engine=null,waitForCameraRestore=false}={}) { attachRenderer(
viewer,
{ engine = null, waitForCameraRestore = false } = {},
) {
layerState._restoringViewport = waitForCameraRestore; layerState._restoringViewport = waitForCameraRestore;
layerState._viewer = viewer; layerState._viewer = viewer;
layerState._engine = engine; layerState._engine = engine;
layerState._native = !viewer && engine ? services.nativeRenderer(engine) : null; layerState._native =
!viewer && engine ? services.nativeRenderer(engine) : null;
layerState._heatSupported = null; layerState._heatSupported = null;
const points = layerState._native?.points ?? new Cesium.PointPrimitiveCollection({blendOption: Cesium.BlendOption.TRANSLUCENT}); const points =
layerState._native?.points ??
new Cesium.PointPrimitiveCollection({
blendOption: Cesium.BlendOption.TRANSLUCENT,
});
if (viewer) viewer.scene.primitives.add(points); if (viewer) viewer.scene.primitives.add(points);
layerState._pointCollection = points; layerState._pointCollection = points;
for(const dot of layerState._dots) dot.point = points.add(dot.point); for (const dot of layerState._dots) dot.point = points.add(dot.point);
points.show = layerState._enabled; points.show = layerState._enabled;
if (!waitForCameraRestore) readyForSurfaceWork(); if (!waitForCameraRestore) readyForSurfaceWork();
if (layerState._enabled) { if (layerState._enabled) {
bindScene(); bindScene();
parts.rendering.rebuildHeatLines(layerState._roads); parts.rendering.rebuildHeatLines(layerState._roads);
if (layerState._dots.length && layerState._roads.some(r=>!r.directFlow)) if (
services.credits?.showOsmCredit?.(engine ?? viewer,'traffic',{openMapTiles:true}); layerState._dots.length &&
if (layerState._liveMode) services.credits?.registerDynamicCredit?.(engine ?? viewer, services.credits.TOMTOM_CREDIT); layerState._roads.some((r) => !r.directFlow)
parts.viewport.onCameraChanged({immediate:true}); )
services.credits?.showOsmCredit?.(engine ?? viewer, 'traffic', {
openMapTiles: true,
});
if (layerState._liveMode)
services.credits?.registerDynamicCredit?.(
engine ?? viewer,
services.credits.TOMTOM_CREDIT,
);
parts.viewport.onCameraChanged({ immediate: true });
} }
}, },
resumeViewport() { resumeViewport() {
@@ -136,40 +172,70 @@ export function createLifecycle({
if (!layerState._enabled) return; if (!layerState._enabled) return;
if (layerState._populationPending) parts.retention.resume(); if (layerState._populationPending) parts.retention.resume();
const previous = layerState._surfaceInput; const previous = layerState._surfaceInput;
if (previous && !layerState._fetching && layerState._lastBounds === previous.bounds) { if (
previous &&
!layerState._fetching &&
layerState._lastBounds === previous.bounds
) {
// A completed pass, not a refinement: the incoming globe often still // A completed pass, not a refinement: the incoming globe often still
// shows coarse tiles, so it samples provisionally and refines once its // shows coarse tiles, so it samples provisionally and refines once its
// surface has loaded ('Handoff refined'). A refinement here never // surface has loaded ('Handoff refined'). A refinement here never
// refined again and left vehicles on those coarse heights. // refined again and left vehicles on those coarse heights.
void parts.flow.applyFlowThenRender(previous.roads, previous.bounds, layerState._loadGeneration, void parts.flow
parts.viewport.getCameraAltitude(), 'Handoff').catch(error => { .applyFlowThenRender(
if (error?.name !== 'AbortError') console.warn('[Data:Traffic] Surface refresh failed', error); previous.roads,
previous.bounds,
layerState._loadGeneration,
parts.viewport.getCameraAltitude(),
'Handoff',
)
.catch((error) => {
if (error?.name !== 'AbortError')
console.warn('[Data:Traffic] Surface refresh failed', error);
}); });
} }
parts.viewport.onCameraChanged({immediate:true}); parts.viewport.onCameraChanged({ immediate: true });
}, },
detachRenderer() { detachRenderer() {
if(!layerState._pointCollection)return; if (!layerState._pointCollection) return;
unbindScene(); unbindScene();
clearTimeout(layerState._fetchTimeout); clearTimeout(layerState._fetchTimeout);
layerState._surfaceRefineRemove?.(); layerState._surfaceRefineRemove?.();
layerState._surfaceRefineRemove = null; layerState._surfaceRefineRemove = null;
layerState._surfaceRevision = (layerState._surfaceRevision || 0)+1; layerState._surfaceRevision = (layerState._surfaceRevision || 0) + 1;
layerState._preparedRoads?.clear(); layerState._preparedRoads?.clear();
parts.rendering.removeHeatLines(); parts.rendering.removeHeatLines();
services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer,'traffic'); services.credits?.hideOsmCredit?.(
for(const dot of layerState._dots) { layerState._engine ?? layerState._viewer,
const p=dot.point; 'traffic',
dot.point={position:Cesium.Cartesian3.clone(p.position),pixelSize:p.pixelSize,color:p.color,show:p.show, );
outlineColor:p.outlineColor,outlineWidth:p.outlineWidth,scaleByDistance:p.scaleByDistance, for (const dot of layerState._dots) {
translucencyByDistance:p.translucencyByDistance,disableDepthTestDistance:0}; const p = dot.point;
dot.point = {
position: Cesium.Cartesian3.clone(p.position),
pixelSize: p.pixelSize,
color: p.color,
show: p.show,
outlineColor: p.outlineColor,
outlineWidth: p.outlineWidth,
scaleByDistance: p.scaleByDistance,
translucencyByDistance: p.translucencyByDistance,
disableDepthTestDistance: 0,
};
} }
layerState._viewer?.scene.primitives.remove(layerState._pointCollection); layerState._viewer?.scene.primitives.remove(layerState._pointCollection);
layerState._native?.destroy(); layerState._native?.destroy();
if(layerState._renderHeld)services.render.releaseContinuousRender('traffic'); if (layerState._renderHeld)
layerState._renderHeld=false; services.render.releaseContinuousRender('traffic');
layerState._pointCollection=layerState._viewer=layerState._engine=layerState._native=null; layerState._renderHeld = false;
layerState._waitForScene ??= new Promise(resolve=>{layerState._sceneReady=resolve;}); layerState._pointCollection =
layerState._viewer =
layerState._engine =
layerState._native =
null;
layerState._waitForScene ??= new Promise((resolve) => {
layerState._sceneReady = resolve;
});
}, },
/** /**
@@ -220,7 +286,10 @@ export function createLifecycle({
disable(viewer) { disable(viewer) {
layerState._enabled = false; layerState._enabled = false;
layerState._surfaceInput = null; layerState._surfaceInput = null;
services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic'); services.credits?.hideOsmCredit?.(
layerState._engine ?? layerState._viewer,
'traffic',
);
parts.animation.syncRenderDemand(); parts.animation.syncRenderDemand();
clearTimeout(layerState._fetchTimeout); clearTimeout(layerState._fetchTimeout);
clearInterval(layerState._enableKickTimer); clearInterval(layerState._enableKickTimer);
@@ -255,7 +324,9 @@ export function createLifecycle({
destroy(viewer) { destroy(viewer) {
this.disable(viewer); this.disable(viewer);
if (layerState._pointCollection) { if (layerState._pointCollection) {
layerState._viewer?.scene.primitives.remove(layerState._pointCollection); layerState._viewer?.scene.primitives.remove(
layerState._pointCollection,
);
layerState._native?.destroy(); layerState._native?.destroy();
layerState._native = null; layerState._native = null;
layerState._pointCollection = null; layerState._pointCollection = null;
@@ -263,7 +334,10 @@ export function createLifecycle({
parts.rendering.removeHeatLines(); parts.rendering.removeHeatLines();
layerState._tileCache.clear(); layerState._tileCache.clear();
if (layerState._styleListenerBound && typeof window !== 'undefined') { if (layerState._styleListenerBound && typeof window !== 'undefined') {
window.removeEventListener('gev:style-change', layerState._styleListener); window.removeEventListener(
'gev:style-change',
layerState._styleListener,
);
layerState._styleListenerBound = false; layerState._styleListenerBound = false;
} }
layerState._sceneReady?.(); layerState._sceneReady?.();
+1 -2
View File
@@ -144,8 +144,7 @@ export const HEAT_JAM_COLOR = rgbaFromCss('#e05252');
/** @const {Cesium.Color} Slow corridor color (bucket amber, faint + static). */ /** @const {Cesium.Color} Slow corridor color (bucket amber, faint + static). */
export const HEAT_SLOW_COLOR = export const HEAT_SLOW_COLOR = rgbaFromCss('#f0b23e').withAlpha(0.2);
rgbaFromCss('#f0b23e').withAlpha(0.2);
/** @const {number} Far-distance scale floor for jam dots (shipped: 0.3). */ /** @const {number} Far-distance scale floor for jam dots (shipped: 0.3). */
+28 -8
View File
@@ -129,9 +129,20 @@ export function createRendering({
: HEAT_SLOW_COLOR; : HEAT_SLOW_COLOR;
if (layerState._native) { if (layerState._native) {
const roadsFor = bucket => kept.filter(item=>item.bucket===bucket).map(item=>item.road); const roadsFor = (bucket) =>
layerState._heatJamPrim = layerState._native.makeHeat(roadsFor('jam'), jamLineColor, HEAT_LINE_JAM_WIDTH, HEAT_JAM_BASE_ALPHA); kept.filter((item) => item.bucket === bucket).map((item) => item.road);
layerState._heatSlowPrim = layerState._native.makeHeat(roadsFor('slow'), slowLineColor, HEAT_LINE_SLOW_WIDTH, slowLineColor.alpha); layerState._heatJamPrim = layerState._native.makeHeat(
roadsFor('jam'),
jamLineColor,
HEAT_LINE_JAM_WIDTH,
HEAT_JAM_BASE_ALPHA,
);
layerState._heatSlowPrim = layerState._native.makeHeat(
roadsFor('slow'),
slowLineColor,
HEAT_LINE_SLOW_WIDTH,
slowLineColor.alpha,
);
layerState._heatLineCount = kept.length; layerState._heatLineCount = kept.length;
parts.animation.syncRenderDemand(); parts.animation.syncRenderDemand();
return; return;
@@ -228,7 +239,8 @@ export function createRendering({
areaDist = Math.max( areaDist = Math.max(
areaDist, areaDist,
Cesium.Cartesian3.distance( Cesium.Cartesian3.distance(
layerState._engine?.camera.livePosition ?? layerState._viewer.camera.positionWC, layerState._engine?.camera.livePosition ??
layerState._viewer.camera.positionWC,
wp, wp,
), ),
); );
@@ -247,10 +259,18 @@ export function createRendering({
: null; : null;
parts.retention.reconcile(filteredRoads, altitude, replace); parts.retention.reconcile(filteredRoads, altitude, replace);
if (layerState._dots.length && filteredRoads.some((r) => !r.directFlow)) if (layerState._dots.length && filteredRoads.some((r) => !r.directFlow))
services.credits?.showOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic', { services.credits?.showOsmCredit?.(
openMapTiles: true, layerState._engine ?? layerState._viewer,
}); 'traffic',
else services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic'); {
openMapTiles: true,
},
);
else
services.credits?.hideOsmCredit?.(
layerState._engine ?? layerState._viewer,
'traffic',
);
parts.model.recolorDotsInPlace(label); parts.model.recolorDotsInPlace(label);
const renderMetrics = state const renderMetrics = state
+11 -2
View File
@@ -217,7 +217,8 @@ export function createRetention({ state, parts }) {
const blend = Math.min(1, dt * 12); const blend = Math.min(1, dt * 12);
const scene = state._trafficScene?.() ?? state._viewer?.scene; const scene = state._trafficScene?.() ?? state._viewer?.scene;
const canProject = const canProject =
scene?.canvas?.clientHeight && (scene.project || (scene.camera && scene.frameState)); scene?.canvas?.clientHeight &&
(scene.project || (scene.camera && scene.frameState));
state._heightPending = false; state._heightPending = false;
for (let r = 0; r < state._roads.length; r++) { for (let r = 0; r < state._roads.length; r++) {
const road = state._roads[r]; const road = state._roads[r];
@@ -300,5 +301,13 @@ export function createRetention({ state, parts }) {
state._populationPending = false; state._populationPending = false;
state._heightPending = false; state._heightPending = false;
} }
return { reconcile, retarget, easeHeights, finishHeights, clear, refreshCounts, resume: schedule }; return {
reconcile,
retarget,
easeHeights,
finishHeights,
clear,
refreshCounts,
resume: schedule,
};
} }
+2 -2
View File
@@ -18,8 +18,8 @@ export function createState({ services }) {
layerState._viewer = null; layerState._viewer = null;
layerState._engine = null; layerState._engine = null;
layerState._native = null; layerState._native = null;
layerState._trafficScene = () => layerState._native?.surface ?? layerState._viewer?.scene; layerState._trafficScene = () =>
layerState._native?.surface ?? layerState._viewer?.scene;
/** @type {Cesium.PointPrimitiveCollection|null} */ /** @type {Cesium.PointPrimitiveCollection|null} */
+6 -3
View File
@@ -50,7 +50,7 @@ export function observeTrafficSurface(scene) {
} }
function currentRenderedFrame(scene) { function currentRenderedFrame(scene) {
if (scene.engine) return {ready:scene.ready}; if (scene.engine) return { ready: scene.ready };
if (!scene.postRender?.addEventListener || !scene.camera?.viewMatrix) if (!scene.postRender?.addEventListener || !scene.camera?.viewMatrix)
return { ready: surfaceTilesReady(scene) }; return { ready: surfaceTilesReady(scene) };
const frame = renderedFrames.get(scene); const frame = renderedFrames.get(scene);
@@ -280,7 +280,9 @@ export async function prepareRoadSurfaces(
cache = { surface, heights: new Map() }; cache = { surface, heights: new Map() };
sceneCaches.set(scene, cache); sceneCaches.set(scene, cache);
} }
const cameraCarto = scene.engine?.camera.pose().cartographic ?? scene.camera?.positionCartographic; const cameraCarto =
scene.engine?.camera.pose().cartographic ??
scene.camera?.positionCartographic;
const cameraLon = cameraCarto const cameraLon = cameraCarto
? Cesium.Math.toDegrees(cameraCarto.longitude) ? Cesium.Math.toDegrees(cameraCarto.longitude)
: 0; : 0;
@@ -345,7 +347,8 @@ export async function prepareRoadSurfaces(
if (validHeight(sampled)) metrics.depthHits++; if (validHeight(sampled)) metrics.depthHits++;
} }
if ( if (
!scene.engine && !scene.globe?.show && !scene.engine &&
!scene.globe?.show &&
!validHeight(sampled) && !validHeight(sampled) &&
scene.sampleHeightSupported scene.sampleHeightSupported
) { ) {
+31 -8
View File
@@ -24,7 +24,9 @@ export function createViewport({ state: layerState, services, parts, source }) {
*/ */
function getCameraAltitude() { function getCameraAltitude() {
const carto = layerState._engine?.camera.pose().cartographic ?? layerState._viewer?.camera.positionCartographic; const carto =
layerState._engine?.camera.pose().cartographic ??
layerState._viewer?.camera.positionCartographic;
return carto ? carto.height : Infinity; return carto ? carto.height : Infinity;
} }
@@ -35,7 +37,9 @@ export function createViewport({ state: layerState, services, parts, source }) {
*/ */
function getViewBounds() { function getViewBounds() {
const rect = layerState._engine?.camera.viewRectangle() ?? layerState._viewer?.camera.computeViewRectangle(); const rect =
layerState._engine?.camera.viewRectangle() ??
layerState._viewer?.camera.computeViewRectangle();
if (!rect) return null; if (!rect) return null;
return { return {
south: geo.math.toDegrees(rect.south), south: geo.math.toDegrees(rect.south),
@@ -60,18 +64,23 @@ export function createViewport({ state: layerState, services, parts, source }) {
*/ */
function getFetchCenter() { function getFetchCenter() {
const carto = layerState._engine?.camera.pose().cartographic ?? layerState._viewer?.camera.positionCartographic; const carto =
layerState._engine?.camera.pose().cartographic ??
layerState._viewer?.camera.positionCartographic;
if (!carto) return null; if (!carto) return null;
const nadirLat = geo.math.toDegrees(carto.latitude); const nadirLat = geo.math.toDegrees(carto.latitude);
const nadirLon = geo.math.toDegrees(carto.longitude); const nadirLon = geo.math.toDegrees(carto.longitude);
let hitLat; let hitLat;
let hitLon; let hitLon;
const canvas = layerState._engine?.host.canvas ?? layerState._viewer.scene.canvas; const canvas =
layerState._engine?.host.canvas ?? layerState._viewer.scene.canvas;
const width = canvas.clientWidth || canvas.width; const width = canvas.clientWidth || canvas.width;
const height = canvas.clientHeight || canvas.height; const height = canvas.clientHeight || canvas.height;
if (width > 0 && height > 0) { if (width > 0 && height > 0) {
const hit = (layerState._engine?.camera ?? layerState._viewer.camera).pickEllipsoid( const hit = (
layerState._engine?.camera ?? layerState._viewer.camera
).pickEllipsoid(
geo.cartesian2.create(width / 2, height / 2), geo.cartesian2.create(width / 2, height / 2),
Cesium.Ellipsoid.WGS84, Cesium.Ellipsoid.WGS84,
); );
@@ -174,7 +183,12 @@ export function createViewport({ state: layerState, services, parts, source }) {
/** Debounce camera events and sample the actual settled view, including final flight steps. */ /** Debounce camera events and sample the actual settled view, including final flight steps. */
function onCameraChanged(options) { function onCameraChanged(options) {
if (!layerState._enabled || layerState._waitForScene || layerState._restoringViewport) return; if (
!layerState._enabled ||
layerState._waitForScene ||
layerState._restoringViewport
)
return;
clearTimeout(layerState._fetchTimeout); clearTimeout(layerState._fetchTimeout);
if (getCameraAltitude() > ACTIVATION_ALTITUDE) { if (getCameraAltitude() > ACTIVATION_ALTITUDE) {
clearTimeout(layerState._retryTimer); clearTimeout(layerState._retryTimer);
@@ -197,10 +211,19 @@ export function createViewport({ state: layerState, services, parts, source }) {
: null; : null;
layerState._fetchTimeout = setTimeout( layerState._fetchTimeout = setTimeout(
() => { () => {
if (!layerState._enabled || layerState._waitForScene || layerState._restoringViewport) return; if (
!layerState._enabled ||
layerState._waitForScene ||
layerState._restoringViewport
)
return;
const alt = getCameraAltitude(); const alt = getCameraAltitude();
if (alt > ACTIVATION_ALTITUDE) return onCameraChanged(); if (alt > ACTIVATION_ALTITUDE) return onCameraChanged();
const footprint = reticleFootprint(layerState._viewer, alt, layerState._engine); const footprint = reticleFootprint(
layerState._viewer,
alt,
layerState._engine,
);
const bounds = footprint || getViewBounds(); const bounds = footprint || getViewBounds();
if (!bounds) return; if (!bounds) return;
const center = footprint ? null : getFetchCenter(); const center = footprint ? null : getFetchCenter();
+15 -4
View File
@@ -141,7 +141,10 @@ export function createLifecycle({
state._viewer = null; state._viewer = null;
throw error; throw error;
} }
state._overlayHost.setVisible(TRANSIT_SELECTED_OVERLAY_SOURCE_ID, state._enabled); state._overlayHost.setVisible(
TRANSIT_SELECTED_OVERLAY_SOURCE_ID,
state._enabled,
);
if (state._enabled) { if (state._enabled) {
for (const [key, entry] of state._vehicles) { for (const [key, entry] of state._vehicles) {
parts.rendering.addMarker(key, entry); parts.rendering.addMarker(key, entry);
@@ -203,7 +206,8 @@ export function createLifecycle({
parts.rendering.refreshVisibility(); parts.rendering.refreshVisibility();
parts.trails.rebind(); parts.trails.rebind();
parts.selection.refreshSelectedCard(true); parts.selection.refreshSelectedCard(true);
for (const feed of state._activeFeeds.values()) parts.renderer.addCredit(feed); for (const feed of state._activeFeeds.values())
parts.renderer.addCredit(feed);
}, },
resumeViewport() { resumeViewport() {
if (!state._restoringViewport) return; if (!state._restoringViewport) return;
@@ -217,7 +221,10 @@ export function createLifecycle({
clearInterval(state._maintenanceTimer); clearInterval(state._maintenanceTimer);
clearTimeout(state._cameraDebounceTimer); clearTimeout(state._cameraDebounceTimer);
clearTimeout(state._visibilityTimer); clearTimeout(state._visibilityTimer);
state._maintenanceTimer = state._cameraDebounceTimer = state._visibilityTimer = null; state._maintenanceTimer =
state._cameraDebounceTimer =
state._visibilityTimer =
null;
parts.selection.removeClickHandler(); parts.selection.removeClickHandler();
unregisterPickOwner('transit'); unregisterPickOwner('transit');
state._cameraChangedRemove?.(); state._cameraChangedRemove?.();
@@ -238,7 +245,11 @@ export function createLifecycle({
if (state._renderHeld) releaseContinuousRender('transit'); if (state._renderHeld) releaseContinuousRender('transit');
state._renderHeld = false; state._renderHeld = false;
parts.renderer.destroy(); parts.renderer.destroy();
parts.renderer = state._markers = state._animatedMarkers = state._viewer = null; parts.renderer =
state._markers =
state._animatedMarkers =
state._viewer =
null;
}, },
/** /**
+2 -1
View File
@@ -79,7 +79,8 @@ export function createViewport({ state, services, parts }) {
globalThis.document?.visibilityState === 'hidden' globalThis.document?.visibilityState === 'hidden'
) )
return; return;
if (!state._enabled || !parts.renderer?.ready || state._restoringViewport) return; if (!state._enabled || !parts.renderer?.ready || state._restoringViewport)
return;
refreshViewBounds(); refreshViewBounds();
const altitude = getCameraAltitude(); const altitude = getCameraAltitude();
state._altitudeGateOpen = altitudeGateOpen(altitude); state._altitudeGateOpen = altitudeGateOpen(altitude);
+24 -9
View File
@@ -304,21 +304,36 @@ test('a pending AIS poll and selected trail survive replacement of the planet',
const injected = services(recorders[0].renderer); const injected = services(recorders[0].renderer);
const restored = []; const restored = [];
injected.sprites.restoreSpriteOrderOnEnable = (id) => restored.push(id); injected.sprites.restoreSpriteOrderOnEnable = (id) => restored.push(id);
injected.renderer.create = ({ engine }) => recorders[engines.indexOf(engine)].renderer; injected.renderer.create = ({ engine }) =>
recorders[engines.indexOf(engine)].renderer;
const previousDocument = globalThis.document; const previousDocument = globalThis.document;
globalThis.document = Object.assign(new EventTarget(), { getElementById: () => null }); globalThis.document = Object.assign(new EventTarget(), {
t.after(() => { globalThis.document = previousDocument; }); getElementById: () => null,
let polls = 0, histories = 0, deliver; });
t.after(() => {
globalThis.document = previousDocument;
});
let polls = 0,
histories = 0,
deliver;
const snapshot = (records) => ({ const snapshot = (records) => ({
records, source: 'AIS fixture', complete: true, transportStatus: 'live', records,
rawRowCount: records.length, observedAtMs: 500, freshness: 'current', source: 'AIS fixture',
complete: true,
transportStatus: 'live',
rawRowCount: records.length,
observedAtMs: 500,
freshness: 'current',
}); });
const layer = createVesselLayer({ const layer = createVesselLayer({
services: injected, services: injected,
source: { source: {
getSnapshot: () => ++polls === 1 getSnapshot: () =>
? snapshot([row('111', 40.6, -73.9)]) ++polls === 1
: new Promise((resolve) => { deliver = resolve; }), ? snapshot([row('111', 40.6, -73.9)])
: new Promise((resolve) => {
deliver = resolve;
}),
getTrack: async () => { getTrack: async () => {
histories++; histories++;
return { records: [{ latitude: 40.59, longitude: -73.92 }] }; return { records: [{ latitude: 40.59, longitude: -73.92 }] };
+34 -12
View File
@@ -108,10 +108,17 @@ export function createWeatherLayer({
clock ? clock.getState().mode === 'latest' : followLatest; clock ? clock.getState().mode === 'latest' : followLatest;
const getHost = () => const getHost = () =>
sceneEngine?.kind === 'navara' sceneEngine?.kind === 'navara'
? { kind: sceneEngine.maps.photorealActive() ? 'tileset' : 'globe', collection: sceneEngine } ? {
: imageryHost?.() ?? { collection: viewer?.imageryLayers, kind: 'globe' }; kind: sceneEngine.maps.photorealActive() ? 'tileset' : 'globe',
collection: sceneEngine,
}
: (imageryHost?.() ?? {
collection: viewer?.imageryLayers,
kind: 'globe',
});
const notify = () => listener?.(); const notify = () => listener?.();
const shownTime = () => (noFrame ? null : rendering?.getDiagnostics().time ?? lastShown); const shownTime = () =>
noFrame ? null : (rendering?.getDiagnostics().time ?? lastShown);
const unsubscribeClock = clock?.subscribe(() => { const unsubscribeClock = clock?.subscribe(() => {
if (!clock.getState().playing || suspended()) rendering?.cancelPrefetch?.(); if (!clock.getState().playing || suspended()) rendering?.cancelPrefetch?.();
notify(); notify();
@@ -298,23 +305,32 @@ export function createWeatherLayer({
motion = matchMedia?.('(prefers-reduced-motion: reduce)'); motion = matchMedia?.('(prefers-reduced-motion: reduce)');
motion?.addEventListener?.('change', onVisibility); motion?.addEventListener?.('change', onVisibility);
documentRef?.addEventListener?.('visibilitychange', onVisibility); documentRef?.addEventListener?.('visibilitychange', onVisibility);
if (nextViewer || options.engine) layer.attachRenderer(nextViewer, options); if (nextViewer || options.engine)
layer.attachRenderer(nextViewer, options);
}, },
attachRenderer(nextViewer, { engine = null } = {}) { attachRenderer(nextViewer, { engine = null } = {}) {
viewer = nextViewer; viewer = nextViewer;
sceneEngine = engine; sceneEngine = engine;
const options = { viewer, cesium, getHost, onChange: notify }; const options = { viewer, cesium, getHost, onChange: notify };
rendering = engine?.kind === 'navara' rendering =
? createNativeRendering(engine, options) engine?.kind === 'navara'
: createRendering(options); ? createNativeRendering(engine, options)
: createRendering(options);
checkHost(false); checkHost(false);
eventTarget?.addEventListener?.('gev:map-stack-changed', onMapStackChanged); eventTarget?.addEventListener?.(
'gev:map-stack-changed',
onMapStackChanged,
);
rendering.setAlpha(opacity === 'light' ? 0.4 : satellite ? 0.7 : 0.8); rendering.setAlpha(opacity === 'light' ? 0.4 : satellite ? 0.7 : 0.8);
if (!enabled) return; if (!enabled) return;
watchCamera(); watchCamera();
if (!manifest) return; if (!manifest) return;
const time = clock && !isLatest() ? clock.selectFor(id, clock.getState().target) : const time =
(followLatest ? manifest.latest : lastShown); clock && !isLatest()
? clock.selectFor(id, clock.getState().target)
: followLatest
? manifest.latest
: lastShown;
if (time) void show(time); if (time) void show(time);
else rendering.setHidden(true); else rendering.setHidden(true);
}, },
@@ -326,7 +342,10 @@ export function createWeatherLayer({
timer = null; timer = null;
removeCamera?.(); removeCamera?.();
removeCamera = null; removeCamera = null;
eventTarget?.removeEventListener?.('gev:map-stack-changed', onMapStackChanged); eventTarget?.removeEventListener?.(
'gev:map-stack-changed',
onMapStackChanged,
);
rendering?.clear(); rendering?.clear();
rendering = null; rendering = null;
hostCollection = null; hostCollection = null;
@@ -622,7 +641,10 @@ export function createWeatherLayer({
viewer?.scene?.globe?.ellipsoid, viewer?.scene?.globe?.ellipsoid,
); );
const camera = point const camera = point
? cesium.Cartographic.fromCartesian(point, viewer?.scene?.globe?.ellipsoid) ? cesium.Cartographic.fromCartesian(
point,
viewer?.scene?.globe?.ellipsoid,
)
: null; : null;
const lon = camera ? cesium.Math.toDegrees(camera.longitude) : 0; const lon = camera ? cesium.Math.toDegrees(camera.longitude) : 0;
const lat = camera ? cesium.Math.toDegrees(camera.latitude) : 0; const lat = camera ? cesium.Math.toDegrees(camera.latitude) : 0;
+7 -1
View File
@@ -16,7 +16,13 @@ const KEY = 'gevObservedWeather';
const owners = new WeakMap(); const owners = new WeakMap();
const products = ['Wind', 'Satellite', 'Radar', 'Lightning']; const products = ['Wind', 'Satellite', 'Radar', 'Lightning'];
const slotFor = (p) => const slotFor = (p) =>
p === 'wind' ? 'Wind' : p === 'radar' ? 'Radar' : p === 'lightning' ? 'Lightning' : 'Satellite'; p === 'wind'
? 'Wind'
: p === 'radar'
? 'Radar'
: p === 'lightning'
? 'Lightning'
: 'Satellite';
const emptyRect = [0, 0, -1, -1]; const emptyRect = [0, 0, -1, -1];
const rectArray = (r) => (r ? [r.west, r.south, r.east, r.north] : emptyRect); const rectArray = (r) => (r ? [r.west, r.south, r.east, r.north] : emptyRect);
+3 -4
View File
@@ -407,7 +407,8 @@ export function createWeatherShell({
cacheBytes = WEATHER_SHELL_CACHE_BYTES, cacheBytes = WEATHER_SHELL_CACHE_BYTES,
createSurface = createShellSurface, createSurface = createShellSurface,
requestRender = () => viewer.scene.requestRender(), requestRender = () => viewer.scene.requestRender(),
viewRectangle = () => viewer.camera?.computeViewRectangle?.(viewer.scene.globe?.ellipsoid), viewRectangle = () =>
viewer.camera?.computeViewRectangle?.(viewer.scene.globe?.ellipsoid),
moveEnd = viewer?.camera?.moveEnd, moveEnd = viewer?.camera?.moveEnd,
}) { }) {
if (!Object.hasOwn(WEATHER_IMAGE_SIZES, product)) if (!Object.hasOwn(WEATHER_IMAGE_SIZES, product))
@@ -572,9 +573,7 @@ export function createWeatherShell({
} }
/** Recompute the window from the camera's ground footprint; returns whether it changed. */ /** Recompute the window from the camera's ground footprint; returns whether it changed. */
function computeView() { function computeView() {
const rectangle = detailed const rectangle = detailed ? viewRectangle() : null;
? viewRectangle()
: null;
const degrees = cesium.Math.toDegrees; const degrees = cesium.Math.toDegrees;
const footprint = rectangle && { const footprint = rectangle && {
west: degrees(rectangle.west), west: degrees(rectangle.west),
+21 -13
View File
@@ -85,7 +85,10 @@ export function createWindLayer({
const sample = (position) => { const sample = (position) => {
if (!position && engine && !viewer) { if (!position && engine && !viewer) {
const canvas = engine.host.canvas; const canvas = engine.host.canvas;
position = engine.camera.pickEllipsoid({ x: canvas.clientWidth / 2, y: canvas.clientHeight / 2 }); position = engine.camera.pickEllipsoid({
x: canvas.clientWidth / 2,
y: canvas.clientHeight / 2,
});
} }
reading = inspectWindAtCenter(manifest, viewer, cesium, { reading = inspectWindAtCenter(manifest, viewer, cesium, {
position, position,
@@ -122,20 +125,25 @@ export function createWindLayer({
attachRenderer(nextViewer, { engine: nextEngine = null } = {}) { attachRenderer(nextViewer, { engine: nextEngine = null } = {}) {
viewer = nextViewer; viewer = nextViewer;
engine = nextEngine; engine = nextEngine;
rendering = engine?.kind === 'navara' ? createNativeRendering(engine) : createRendering({ rendering =
cesium, engine?.kind === 'navara'
container: container ?? nextViewer.container, ? createNativeRendering(engine)
getViewer: () => viewer, : createRendering({
getHost: () => cesium,
imageryHost?.() ?? { container: container ?? nextViewer.container,
collection: viewer.imageryLayers ?? viewer.scene?.imageryLayers, getViewer: () => viewer,
kind: 'globe', getHost: () =>
}, imageryHost?.() ?? {
onStatusChange: notify, collection:
}); viewer.imageryLayers ?? viewer.scene?.imageryLayers,
kind: 'globe',
},
onStatusChange: notify,
});
rendering.attach(); rendering.attach();
rendering.setOptions?.({ overlay, paused }); rendering.setOptions?.({ overlay, paused });
const target = container ?? nextViewer?.container ?? engine?.host.container; const target =
container ?? nextViewer?.container ?? engine?.host.container;
inspectionMarker = createWindInspectionMarker({ inspectionMarker = createWindInspectionMarker({
container: target, container: target,
viewer, viewer,
+10 -6
View File
@@ -44,10 +44,12 @@ export function createWindInspectionMarker({
) )
return; return;
present({ owner, container, visible: false, left: 0, top: 0 }); present({ owner, container, visible: false, left: 0, top: 0 });
const occluder = !engine && new cesium.EllipsoidalOccluder( const occluder =
scene.globe?.ellipsoid || cesium.Ellipsoid.WGS84, !engine &&
viewer.camera.positionWC, new cesium.EllipsoidalOccluder(
); scene.globe?.ellipsoid || cesium.Ellipsoid.WGS84,
viewer.camera.positionWC,
);
const render = () => { const render = () => {
if (!shown()) return; if (!shown()) return;
if (occluder) occluder.cameraPosition = viewer.camera.positionWC; if (occluder) occluder.cameraPosition = viewer.camera.positionWC;
@@ -60,8 +62,10 @@ export function createWindInspectionMarker({
point.y >= 0 && point.y >= 0 &&
point.x <= canvas.clientWidth && point.x <= canvas.clientWidth &&
point.y <= canvas.clientHeight && point.y <= canvas.clientHeight &&
(engine ? engine.projector.horizonVisible(position) : (engine
scene.mode !== cesium.SceneMode.SCENE3D || occluder.isPointVisible(position)); ? engine.projector.horizonVisible(position)
: scene.mode !== cesium.SceneMode.SCENE3D ||
occluder.isPointVisible(position));
if (!visible) { if (!visible) {
// A hidden marker keeps its last place, as the element did. // A hidden marker keeps its last place, as the element did.
if (marker.visible) present({ visible: false }); if (marker.visible) present({ visible: false });
+44 -35
View File
@@ -30,10 +30,7 @@ export function createPackPresentations(
let key, let key,
cards = []; cards = [];
function present() { function present() {
store.publish( store.publish(owner, cards.length ? { key, container, cards } : null);
owner,
cards.length ? { key, container, cards } : null,
);
} }
function addCard(pack) { function addCard(pack) {
if (!cards.length) key = ++panelKeys; if (!cards.length) key = ++panelKeys;
@@ -205,27 +202,35 @@ export function createPackPresentations(
signal.throwIfAborted(); signal.throwIfAborted();
if (image.naturalWidth * image.naturalHeight > 16 * 1024 * 1024) if (image.naturalWidth * image.naturalHeight > 16 * 1024 * 1024)
throw new Error('Image dimensions exceed limit'); throw new Error('Image dimensions exceed limit');
handle.display(native => native ? native.image(pack,image,handle) : handle.primitive( handle.display((native) =>
new Cesium.Primitive({ native
asynchronous: false, ? native.image(pack, image, handle)
geometryInstances: new Cesium.GeometryInstance({ : handle.primitive(
id: { packId: pack.id }, new Cesium.Primitive({
geometry: new Cesium.RectangleGeometry({ asynchronous: false,
rectangle: Cesium.Rectangle.fromDegrees(...pack.placement.bounds), geometryInstances: new Cesium.GeometryInstance({
height: pack.placement.height, id: { packId: pack.id },
vertexFormat: geometry: new Cesium.RectangleGeometry({
Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat, rectangle: Cesium.Rectangle.fromDegrees(
}), ...pack.placement.bounds,
}), ),
appearance: new Cesium.MaterialAppearance({ height: pack.placement.height,
material: Cesium.Material.fromType('Image', { image }), vertexFormat:
materialSupport: Cesium.MaterialAppearance.MaterialSupport.TEXTURED, Cesium.MaterialAppearance.MaterialSupport.TEXTURED
flat: true, .vertexFormat,
faceForward: true, }),
translucent: true, }),
}), appearance: new Cesium.MaterialAppearance({
}), material: Cesium.Material.fromType('Image', { image }),
)); materialSupport:
Cesium.MaterialAppearance.MaterialSupport.TEXTURED,
flat: true,
faceForward: true,
translucent: true,
}),
}),
),
);
}), }),
media: guarded(({ asset, pack, anchors }, handle) => { media: guarded(({ asset, pack, anchors }, handle) => {
if ( if (
@@ -243,16 +248,20 @@ export function createPackPresentations(
(item) => item.id === pack.placement.anchorId, (item) => item.id === pack.placement.anchorId,
); );
if (!anchor) throw new Error('Missing media anchor'); if (!anchor) throw new Error('Missing media anchor');
handle.display(native => native ? native.media(pack,anchor,handle) : handle.add({ handle.display((native) =>
name: pack.id, native
position: xyz([anchor.lon, anchor.lat, anchor.alt]), ? native.media(pack, anchor, handle)
point: { pixelSize: 16, color: Cesium.Color.YELLOW }, : handle.add({
label: { name: pack.id,
text: pack.id, position: xyz([anchor.lon, anchor.lat, anchor.alt]),
font: '14px sans-serif', point: { pixelSize: 16, color: Cesium.Color.YELLOW },
pixelOffset: new Cesium.Cartesian2(0, -24), label: {
}, text: pack.id,
})); font: '14px sans-serif',
pixelOffset: new Cesium.Cartesian2(0, -24),
},
}),
);
handle.media({ handle.media({
kind: asset.mimeType.startsWith('video/') ? 'video' : 'audio', kind: asset.mimeType.startsWith('video/') ? 'video' : 'audio',
src: handle.url(new Blob([asset.bytes], { type: asset.mimeType })), src: handle.url(new Blob([asset.bytes], { type: asset.mimeType })),
+7 -3
View File
@@ -86,9 +86,13 @@ export class ContextControls {
} }
} }
canRetainScene() { canRetainScene() {
return ['flights', 'space-missions'].includes(this._contextMode) && !this._contextModeChanging && return (
!this._contextRestoreState && !this._clearSelectedLayersPromise && ['flights', 'space-missions'].includes(this._contextMode) &&
!this._contextLayerReactionPromises.size; !this._contextModeChanging &&
!this._contextRestoreState &&
!this._clearSelectedLayersPromise &&
!this._contextLayerReactionPromises.size
);
} }
captureScene() { captureScene() {
return { mode: this._contextMode, snapshot: this._contextSessionSnapshot }; return { mode: this._contextMode, snapshot: this._contextSessionSnapshot };
+16 -5
View File
@@ -176,7 +176,8 @@ 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 retained =
engineSwitch && this._layerStateCoordinator?.retainedTrackingTarget();
const applyCamera = const applyCamera =
Number.isInteger(generation) && Number.isInteger(generation) &&
this.navigation._reassertNavigationHandoff(generation, { this.navigation._reassertNavigationHandoff(generation, {
@@ -246,7 +247,15 @@ export class ShareRestoration {
*/ */
async _runShareRestore( async _runShareRestore(
state, state,
{ applyCamera, instant = false, retained, retainScene = false, navigationToken, layersPromise, settle }, {
applyCamera,
instant = false,
retained,
retainScene = false,
navigationToken,
layersPromise,
settle,
},
) { ) {
const generation = this._shareRestoreGeneration; const generation = this._shareRestoreGeneration;
const current = () => const current = () =>
@@ -259,11 +268,13 @@ export class ShareRestoration {
}); });
const layers = await (layersPromise || Promise.resolve([])); const layers = await (layersPromise || Promise.resolve([]));
if (!current()) return; if (!current()) return;
if (retainScene && !retained) retained = this._layerStateCoordinator?.retainedTrackingTarget(); if (retainScene && !retained)
retained = this._layerStateCoordinator?.retainedTrackingTarget();
const tracking = const tracking =
share.camera === 'applied' share.camera === 'applied'
? (retained && this._layerStateCoordinator.resumeRetainedTracking(retained)) || ? (retained &&
await this._layerStateCoordinator?.restoreShareTrackingSelection?.() this._layerStateCoordinator.resumeRetainedTracking(retained)) ||
(await this._layerStateCoordinator?.restoreShareTrackingSelection?.())
: { : {
status: 'superseded', status: 'superseded',
cleared: cleared: