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) {
await page.evaluateOnNewDocument((stations) => {
localStorage.clear();
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
window.__qaRadio = {
stations,
generation: 1,
plays: [],
delay: 0,
response: 'normal',
pending: null,
};
HTMLMediaElement.prototype.play = function () {
window.qaPlayer = this;
window.__qaRadio.plays.push(this.src);
if (window.__qaRadio.delayPlay) {
window.__qaRadio.delayPlay = false;
return new Promise((resolve) => {
window.__qaRadio.releasePlay = () => {
this.dispatchEvent(new Event('playing'));
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;
await page.evaluateOnNewDocument((stations) => {
localStorage.clear();
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
window.__qaRadio = {
stations,
generation: 1,
plays: [],
delay: 0,
response: 'normal',
pending: null,
};
HTMLMediaElement.prototype.play = function () {
window.qaPlayer = this;
window.__qaRadio.plays.push(this.src);
if (window.__qaRadio.delayPlay) {
window.__qaRadio.delayPlay = false;
return new Promise((resolve) => {
window.__qaRadio.releasePlay = () => {
this.dispatchEvent(new Event('playing'));
resolve();
};
});
init?.signal?.throwIfAborted();
return new Response(
mode === 'failure' ? '{}' : JSON.stringify(response),
{
status: mode === 'failure' ? 503 : 200,
headers: { 'Content-Type': 'application/json' },
},
}
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/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);
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(
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();
if (layerSession && !layerSession.weatherClock) {
layerSession.weatherClock = weatherClock;
layerSession.lifetime.signal.addEventListener('abort', weatherClock.destroy, { once: true });
layerSession.lifetime.signal.addEventListener(
'abort',
weatherClock.destroy,
{ once: true },
);
}
const dispose = () => {
signal.removeEventListener('abort', dispose);
@@ -98,7 +102,9 @@ export function createApplicationCatalog({
if (!layerSession?.militaryRegistry) {
const lifetime = layerSession?.lifetime.signal ?? signal;
if (sourceComposition.isConfigured('military'))
militaryRegistry.configureSource(sources.military, { signal: lifetime });
militaryRegistry.configureSource(sources.military, {
signal: lifetime,
});
if (layerSession) {
layerSession.militaryRegistry = militaryRegistry;
lifetime.addEventListener('abort', militaryRegistry.dispose, {
@@ -129,12 +135,14 @@ export function createApplicationCatalog({
),
);
const military = once(() =>
sessionModel('military', () => createApplicationMilitary({
surface,
source: sources.military,
militaryRegistry,
resolveAsset,
})),
sessionModel('military', () =>
createApplicationMilitary({
surface,
source: sources.military,
militaryRegistry,
resolveAsset,
}),
),
);
const vessels = once(() =>
sessionModel('ais-live-vessels', () =>
@@ -153,39 +161,53 @@ export function createApplicationCatalog({
),
);
const satellites = once(() =>
sessionModel('satellites', () => createApplicationSatellites({
source: sources.satellites,
})),
sessionModel('satellites', () =>
createApplicationSatellites({
source: sources.satellites,
}),
),
);
// Catalog order: [layer id(s), construction]. Only the layers the
// engine can show are constructed (renderers.js).
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',
() =>
sessionModel('bhote-koshi-locator', () => createBhoteKoshiLocatorLayer({
boundaryResolver: nepalBoundaryResolver,
createNativeRenderer: (engine, data) => rendererFor('bhote-koshi-locator', engine, { data }),
})),
sessionModel('bhote-koshi-locator', () =>
createBhoteKoshiLocatorLayer({
boundaryResolver: nepalBoundaryResolver,
createNativeRenderer: (engine, data) =>
rendererFor('bhote-koshi-locator', engine, { data }),
}),
),
],
['flights', flights],
['military', military],
[
'local-adsb',
() =>
sessionModel('local-adsb', () => createApplicationLocalAdsb({
surface,
// QA fixture receivers (?qaFeed=local-adsb:fixture, dev only).
...(sources.localAdsbFeeds
? { feeds: sources.localAdsbFeeds }
: {}),
enrichment: sources.flights,
displayParams: () => flights().getParams(),
...(resolveAsset ? { resolveAsset } : {}),
})),
sessionModel('local-adsb', () =>
createApplicationLocalAdsb({
surface,
// QA fixture receivers (?qaFeed=local-adsb:fixture, dev only).
...(sources.localAdsbFeeds
? { feeds: sources.localAdsbFeeds }
: {}),
enrichment: sources.flights,
displayParams: () => flights().getParams(),
...(resolveAsset ? { resolveAsset } : {}),
}),
),
],
[
'earthquakes',
@@ -196,144 +218,215 @@ export function createApplicationCatalog({
],
[
'air-quality',
() => sessionModel('air-quality', () => createApplicationAirQuality({ source: sources['air-quality'] })),
() =>
sessionModel('air-quality', () =>
createApplicationAirQuality({ source: sources['air-quality'] }),
),
],
[
'fire-perimeters',
() =>
sessionModel('fire-perimeters', () => createApplicationFirePerimeters({
source: sources['fire-perimeters'],
})),
sessionModel('fire-perimeters', () =>
createApplicationFirePerimeters({
source: sources['fire-perimeters'],
}),
),
],
[
'gnss-integrity',
() =>
sessionModel('gnss-integrity', () => createApplicationGnssIntegrity({
source: sources['gnss-integrity'],
windowMs: sources['gnss-integrity'].windowMs,
})),
sessionModel('gnss-integrity', () =>
createApplicationGnssIntegrity({
source: sources['gnss-integrity'],
windowMs: sources['gnss-integrity'].windowMs,
}),
),
],
[
's2-ghostmaps',
() => sessionModel('s2-ghostmaps', () => createApplicationGhostMaps({ source: sources['s2-ghostmaps'] })),
() =>
sessionModel('s2-ghostmaps', () =>
createApplicationGhostMaps({ source: sources['s2-ghostmaps'] }),
),
],
[
'alpr-cameras',
() => sessionModel('alpr-cameras', () => createApplicationAlpr({ surface, source: sources.alpr })),
() =>
sessionModel('alpr-cameras', () =>
createApplicationAlpr({ surface, source: sources.alpr }),
),
],
[
'street-level',
() =>
sessionModel('street-level', () => createApplicationStreetLevel({
surface,
nativeRenderer:engine=>rendererFor('street-level',engine),
sources: { mapillary: sources.mapillary },
providers: streetLevelProviders,
})),
sessionModel('street-level', () =>
createApplicationStreetLevel({
surface,
nativeRenderer: (engine) => rendererFor('street-level', engine),
sources: { mapillary: sources.mapillary },
providers: streetLevelProviders,
}),
),
],
['satellites', satellites],
[
'rocket-launches',
() =>
sessionModel('rocket-launches', () => createApplicationLaunches({
source: sources.launches,
satellites: satellites(),
})),
sessionModel('rocket-launches', () =>
createApplicationLaunches({
source: sources.launches,
satellites: satellites(),
}),
),
],
[
'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',
() => sessionModel('radio', () => createApplicationRadio({ surface, source: sources.radio })),
() =>
sessionModel('radio', () =>
createApplicationRadio({ surface, source: sources.radio }),
),
],
[
'transit',
() => sessionModel('transit', () => createApplicationTransit({ surface, source: sources.transit })),
() =>
sessionModel('transit', () =>
createApplicationTransit({ surface, source: sources.transit }),
),
],
[
'bikeshare',
() => sessionModel('bikeshare', () => createApplicationBikeshare({ source: sources.bikeshare })),
() =>
sessionModel('bikeshare', () =>
createApplicationBikeshare({ source: sources.bikeshare }),
),
],
[
'directions',
() => sessionModel('directions', () => createApplicationDirections({ source: sources.directions, surface })),
() =>
sessionModel('directions', () =>
createApplicationDirections({
source: sources.directions,
surface,
}),
),
],
[
'recent-imagery',
() =>
sessionModel('recent-imagery', () => createApplicationRecentImagery({
source: sources['recent-imagery'],
})),
sessionModel('recent-imagery', () =>
createApplicationRecentImagery({
source: sources['recent-imagery'],
}),
),
],
['ais-live-vessels', vessels],
['military-installations', installations],
[
'military-awareness',
() => sessionModel('military-awareness', () =>
createApplicationAwareness({
flights: flights(),
military: military(),
vessels: vessels(),
installations: installations(),
}),
),
() =>
sessionModel('military-awareness', () =>
createApplicationAwareness({
flights: flights(),
military: military(),
vessels: vessels(),
installations: installations(),
}),
),
],
[
'wind',
() => sessionModel('wind', () => createWindLayer({
feed: sources.wind,
clock: weatherClock,
createNativeRendering: (engine) => rendererFor('wind', engine),
})),
() =>
sessionModel('wind', () =>
createWindLayer({
feed: sources.wind,
clock: weatherClock,
createNativeRendering: (engine) => rendererFor('wind', engine),
}),
),
],
...['weather-radar', 'weather-satellite', 'weather-lightning'].map(
(id) => [
id,
() => sessionModel(id, () =>
createWeatherLayer({
feed: sources.weather,
id,
clock: weatherClock,
createNativeRendering: (engine, options) => rendererFor(id, engine, options),
}),
),
() =>
sessionModel(id, () =>
createWeatherLayer({
feed: sources.weather,
id,
clock: weatherClock,
createNativeRendering: (engine, options) =>
rendererFor(id, engine, options),
}),
),
],
),
[
'weather-cyclones',
() => sessionModel('weather-cyclones', () => createCyclonesLayer({
feed: sources.cyclones,
createNativeRendering: (engine, options) => rendererFor('weather-cyclones', engine, options),
})),
() =>
sessionModel('weather-cyclones', () =>
createCyclonesLayer({
feed: sources.cyclones,
createNativeRendering: (engine, options) =>
rendererFor('weather-cyclones', engine, options),
}),
),
],
[
['local-datacenters', 'local-dams'],
() => sessionModel('local-infrastructure', () =>
createInfrastructureLayers(localGeoJsonServices, (config, services) =>
createInfrastructureSessionLayer(config, services, (engine, descriptor, ops) =>
rendererFor(config.id,engine,{config:descriptor,services:ops}))),
),
() =>
sessionModel('local-infrastructure', () =>
createInfrastructureLayers(
localGeoJsonServices,
(config, services) =>
createInfrastructureSessionLayer(
config,
services,
(engine, descriptor, ops) =>
rendererFor(config.id, engine, {
config: descriptor,
services: ops,
}),
),
),
),
],
[
'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',
() =>
sessionModel('local-firms', () => createApplicationFirms({
surface,
id: 'local-firms',
name: 'FIRMS Active Fires',
icon: '▲',
source: 'NASA FIRMS · LIVE',
feed: sources.firms,
})),
sessionModel('local-firms', () =>
createApplicationFirms({
surface,
id: 'local-firms',
name: 'FIRMS Active Fires',
icon: '▲',
source: 'NASA FIRMS · LIVE',
feed: sources.firms,
}),
),
],
];
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. */
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. */
export function createApplicationGnssIntegrity(options) {
return createGnssIntegrityLayer({
createRenderer: ({ viewer, engine, viewAnchor }) => engine
? rendererFor('gnss-integrity', engine, { viewer, viewAnchor })
: createGnssCesiumRenderer({ viewer, viewAnchor }),
createRenderer: ({ viewer, engine, viewAnchor }) =>
engine
? rendererFor('gnss-integrity', engine, { viewer, viewAnchor })
: createGnssCesiumRenderer({ viewer, viewAnchor }),
...options,
});
}
+10 -3
View File
@@ -24,9 +24,16 @@ export function createApplicationFirePerimeters(options) {
openExternal: (url) => window.open(url, '_blank', 'noopener,noreferrer'),
screenSpaceEventHandlerFactory: (viewer) =>
new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas),
createRenderer: ({viewer, engine, screenSpaceEventHandlerFactory}) => engine
? rendererFor('fire-perimeters', engine, {viewer, screenSpaceEventHandlerFactory})
: createPerimetersCesiumRenderer({viewer, screenSpaceEventHandlerFactory}),
createRenderer: ({ viewer, engine, screenSpaceEventHandlerFactory }) =>
engine
? rendererFor('fire-perimeters', engine, {
viewer,
screenSpaceEventHandlerFactory,
})
: createPerimetersCesiumRenderer({
viewer,
screenSpaceEventHandlerFactory,
}),
picking,
pointer: { isPointerFree },
...options,
+2 -1
View File
@@ -97,7 +97,8 @@ export const CESIUM_LAYER_RENDERERS = Object.freeze({
earthquakes: createEarthquakesCesiumRenderer,
'air-quality': (_engine, options) => createAirQualityCesiumRenderer(options),
'gnss-integrity': (_engine, options) => createGnssCesiumRenderer(options),
'fire-perimeters': (_engine, options) => createPerimetersCesiumRenderer(options),
'fire-perimeters': (_engine, options) =>
createPerimetersCesiumRenderer(options),
'local-firms': createFirmsCesiumRenderer,
'rocket-launches': createLaunchesCesiumRenderer,
satellites: createSatellitesCesiumRenderer,
+11 -9
View File
@@ -5,18 +5,20 @@ import { rendererFor } from './renderers.js';
import { overlayHost } from './overlayHost.js';
/** Wire cable source geometry to the application overlay host. */
export function createApplicationCables({ session = false, ...options }) {
const createCesium = (source) => createSubmarineCableLayer({
overlayHost,
screenSpaceEventHandlerFactory: (canvas) =>
new Cesium.ScreenSpaceEventHandler(canvas),
mapStackEventTarget: typeof window !== 'undefined' ? window : null,
...options,
source,
});
const createCesium = (source) =>
createSubmarineCableLayer({
overlayHost,
screenSpaceEventHandlerFactory: (canvas) =>
new Cesium.ScreenSpaceEventHandler(canvas),
mapStackEventTarget: typeof window !== 'undefined' ? window : null,
...options,
source,
});
if (!session) return createCesium(options.source);
return createCableSessionLayer({
source: options.source,
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 }) {
return createTrafficLayer({
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
// 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) => {
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();
globalThis.window = {};
globalThis.localStorage = { getItem:()=>null,setItem(){},removeItem(){} };
globalThis.localStorage = {
getItem: () => null,
setItem() {},
removeItem() {},
};
globalThis.document = {
hidden: false,
getElementById: () => 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),
removeEventListener: (type, fn) => {
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._renderer = { view: {}, surfaceExclusions: () => [] };
const {applySnapshot: apply} = createMilitarySnapshotRenderer({
const { applySnapshot: apply } = createMilitarySnapshotRenderer({
flightState,
records,
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;
}
const pixel = engine ? engine.projector.worldToWindow(anchor, {}) : Cesium.SceneTransforms.worldToWindowCoordinates(
viewer.scene,
anchor,
);
const pixel = engine
? engine.projector.worldToWindow(anchor, {})
: Cesium.SceneTransforms.worldToWindowCoordinates(viewer.scene, anchor);
if (!pixel) return null;
const canvasBounds = (engine?.host.canvas ?? viewer?.canvas)?.getBoundingClientRect?.();
const canvasBounds = (
engine?.host.canvas ?? viewer?.canvas
)?.getBoundingClientRect?.();
const rootBounds = root.getBoundingClientRect?.();
if (!canvasBounds || !rootBounds) return null;
const x = pixel.x + canvasBounds.left - rootBounds.left;
@@ -638,7 +639,11 @@ function createRoot(documentRef, viewer) {
const root = documentRef.createElement('div');
root.id = 'bhote-koshi-embedded-media-root';
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;
}
+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_EXIT_SECONDS = 0.65;
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 CINEMATIC_INPUT_EVENTS = Object.freeze(['pointerdown', 'wheel']);
const CINEMATIC_VIEW_SPECS = Object.freeze([
+21 -8
View File
@@ -291,7 +291,10 @@ export function createBhoteKoshiLocatorLayer({
let _pausedApproach = null;
const cameraForScene = () => _engine?.camera ?? _viewer?.camera;
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 _enabled = false;
let _boundaryReady = false;
@@ -651,10 +654,17 @@ export function createBhoteKoshiLocatorLayer({
roll: Cesium.Math.toRadians(pose.roll),
},
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: () => {
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);
},
cancel: finish,
@@ -1042,14 +1052,14 @@ export function createBhoteKoshiLocatorLayer({
showOsmCredit(creditTarget(), BHOTE_KOSHI_LOCATOR_LAYER_ID);
}
function attachRenderer(viewer, {engine = null} = {}) {
function attachRenderer(viewer, { engine = null } = {}) {
_viewer = viewer;
_engine = engine;
if (!_enabled) return true;
connectDisplay();
publishOverlayEntries({moving: _placeRevealMoving});
publishOverlayEntries({ moving: _placeRevealMoving });
if (_pausedApproach) {
const {phase, remaining} = _pausedApproach;
const { phase, remaining } = _pausedApproach;
_pausedApproach = null;
if (phase === 'armed') armTriggerApproach();
else flyTriggerStage((_triggerApproachAttempt = {}), phase, remaining);
@@ -1060,8 +1070,11 @@ export function createBhoteKoshiLocatorLayer({
function detachRenderer() {
const phase = _triggerApproachAttempt?.phase;
if (phase && phase !== 'finished') {
_pausedApproach = {phase, remaining: Math.max(0, (_triggerApproachAttempt.endsAt - now()) / 1000)};
clearTriggerApproach({preserveCallout: true});
_pausedApproach = {
phase,
remaining: Math.max(0, (_triggerApproachAttempt.endsAt - now()) / 1000),
};
clearTriggerApproach({ preserveCallout: true });
}
_native?.destroy();
_native = null;
+5 -1
View File
@@ -43,7 +43,11 @@ export function createApplicationDirectionsLayer({
createNativeRenderer,
groundFloor = ground,
} = {}) {
return createDirectionsLayer({ services: { ...directionsServices, ground: groundFloor }, source, createNativeRenderer });
return createDirectionsLayer({
services: { ...directionsServices, ground: groundFloor },
source,
createNativeRenderer,
});
}
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.
* @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 damsUrl = INFRASTRUCTURE_DATA_URLS['local-dams'];
const datacenters = createLayer(
+27 -9
View File
@@ -406,12 +406,21 @@ export class LayerStateCoordinator {
);
// A same-document engine choice retains operational layer intents.
// The share still owns durable preferences and later history visits.
if (live && this.dataManager.layers.get(entry.id) === live.entry &&
!live.entry.lifecycleUncertain && live.entry.enabled === targetEnabled) {
if (
live &&
this.dataManager.layers.get(entry.id) === live.entry &&
!live.entry.lifecycleUncertain &&
live.entry.enabled === targetEnabled
) {
return {
layerId: entry.id, targetEnabled, origin, phase: 'retained',
layerId: entry.id,
targetEnabled,
origin,
phase: 'retained',
...currentLayerOutcome(this.dataManager, entry.id),
appliedOptions: {}, persistenceWrite: false, succeeded: true,
appliedOptions: {},
persistenceWrite: false,
succeeded: true,
};
}
const options = layerOptionsForRestore(this._durableState, entry.id);
@@ -512,24 +521,33 @@ export class LayerStateCoordinator {
const model = this.dataManager.layers.get(selected.layerId)?.module;
if (
model?.attachRenderer &&
String(model.getParams?.()[selected.optionKey]) === String(selected.targetId) &&
String(model.getParams?.()[selected.optionKey]) ===
String(selected.targetId) &&
model.hasContact?.(selected.targetId) === true
) return selected;
)
return selected;
return null;
}
resumeRetainedTracking(previous) {
const selected = this.retainedTrackingTarget();
if (
!selected || selected.layerId !== previous.layerId ||
!selected ||
selected.layerId !== previous.layerId ||
selected.targetId !== previous.targetId
) return null;
)
return null;
const model = this.dataManager.layers.get(selected.layerId).module;
const followed = model.trackById(selected.targetId, {
origin: LAYER_RESTORE_ORIGINS.share,
});
if (!followed) return null;
const result = { ...selected, status: 'found', classification: 'followed', cleared: false };
const result = {
...selected,
status: 'found',
classification: 'followed',
cleared: false,
};
this._publishTrackingRestoreStatus(result);
return result;
}
+5 -2
View File
@@ -585,7 +585,8 @@ export function createLocalGeoJsonLayer(
// windows (before vs after the add settles) need different cleanup.
let addedToScene = false;
try {
let features = _cachedFeatures ?? (readFeatures ? await readFeatures() : null);
let features =
_cachedFeatures ?? (readFeatures ? await readFeatures() : null);
if (_destroyed) return;
if (!features) {
const response = await fetch(url, {
@@ -1065,7 +1066,9 @@ export function createLocalGeoJsonLayer(
},
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;
viewer.selectedEntity = entity;
selectEntityContext(entity);
+9 -3
View File
@@ -25,7 +25,11 @@ const STATION_MARKER_RADIUS_M = 6_000;
const READOUT_LIMIT = 5;
/** 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')
throw new TypeError('Air quality requires a snapshot source');
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,
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;
try {
if (viewer || engine) this.attachRenderer(viewer, { engine });
@@ -72,7 +77,8 @@ export function createAirQualityLayer({ source, overlayHost, createRenderer = cr
},
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.setVisible(_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 () => {
let polls = 0;
const h = harness({ getSnapshot: async () => {
polls++;
return [reading('Toronto', 7)];
} });
const h = harness({
getSnapshot: async () => {
polls++;
return [reading('Toronto', 7)];
},
});
try {
h.layer.detachRenderer();
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. */
export function createAirQualityNavaraRenderer(engine, { view }) {
const display = createNavaraPolygonDisplay(engine, view, (color, show) => ({
polygon: {color, show, transparent: true, opacity: 0.35, clampToGround: true,
outline: true, outlineShow: true, outlineColor: color, outlineWidth: 2},
polygon: {
color,
show,
transparent: true,
opacity: 0.35,
clampToGround: true,
outline: true,
outlineShow: true,
outlineColor: color,
outlineWidth: 2,
},
}));
return {
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
// hit the cap must still warn about coverage, not just about freshness.
if (!restoringView) renderer?.renderRecords();
setAlprStatus((renderer?.count() ?? records.length) ? (stale ? 'stale' : 'ready') : 'empty');
setAlprStatus(
(renderer?.count() ?? records.length)
? stale
? 'stale'
: 'ready'
: 'empty',
);
} catch (error) {
if (
error?.name === 'AbortError' ||
+10 -2
View File
@@ -45,7 +45,11 @@ export function createFocus({ state: layerState, services, parts, source }) {
)
return true;
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;
}
@@ -177,7 +181,11 @@ export function createFocus({ state: layerState, services, parts, source }) {
announceNavigationAuthority('context-vessel-autofocus', {
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 {
+38 -38
View File
@@ -51,43 +51,42 @@ export function createLifecycle({
'gev:awareness-subject-cleared',
layerState.subjectClearListener,
);
layerState.preRenderRemover =
layerState.display.onFrame(() => {
if (!layerState.enabled) return;
const now = Date.now();
// "Did the camera move" uses the SAME quantized pose signature the fleet
// rotation pass gates on (iconOrientation.cameraPoseSignature) rather than
// camera.changed, whose granularity is globally degraded by other layers
// mutating camera.percentageChanged.
const poseSig = cameraMotionSignature(layerState.display.camera);
if (poseSig !== layerState.lastCameraPoseSig) {
layerState.lastCameraPoseSig = poseSig;
layerState.lastCameraPoseChangeMs = now;
}
const decision = parts.model.awarenessRefreshDecision({
nowMs: now,
lastRefreshMs: layerState.lastSubjectRefreshMs,
lastPoseChangeMs: layerState.lastCameraPoseChangeMs,
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.preRenderRemover = layerState.display.onFrame(() => {
if (!layerState.enabled) return;
const now = Date.now();
// "Did the camera move" uses the SAME quantized pose signature the fleet
// rotation pass gates on (iconOrientation.cameraPoseSignature) rather than
// camera.changed, whose granularity is globally degraded by other layers
// mutating camera.percentageChanged.
const poseSig = cameraMotionSignature(layerState.display.camera);
if (poseSig !== layerState.lastCameraPoseSig) {
layerState.lastCameraPoseSig = poseSig;
layerState.lastCameraPoseChangeMs = now;
}
const decision = parts.model.awarenessRefreshDecision({
nowMs: now,
lastRefreshMs: layerState.lastSubjectRefreshMs,
lastPoseChangeMs: layerState.lastCameraPoseChangeMs,
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.runtimeListenersAttached = true;
}
@@ -184,10 +183,11 @@ export function createLifecycle({
layerState.display?.destroy?.();
layerState.display = viewer
? createAwarenessCesiumRenderer(viewer)
: services.renderer?.(engine) ?? null;
: (services.renderer?.(engine) ?? null);
if (layerState.enabled) {
attachRuntimeListeners();
if (layerState.subject) parts.rendering.renderVisual(layerState.subject);
if (layerState.subject)
parts.rendering.renderVisual(layerState.subject);
parts.rendering.scheduleDirectionOverlayUpdate(true);
}
},
+2 -1
View File
@@ -75,7 +75,8 @@ export function createBikeshareLayer({ services, source, rendererFactory }) {
state._viewer = state._engine = null;
renderer = null;
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) {
return renderer?.lifecycle.methods.enable(...args);
@@ -137,7 +137,9 @@ export function createLifecycle({
document.removeEventListener('keydown', parts.selection._onKeyDown);
unregisterPickOwner('bikeshare');
if (layerState._cameraChangedAttached) {
viewer.camera.changed.removeEventListener(parts.viewport.onCameraChanged);
viewer.camera.changed.removeEventListener(
parts.viewport.onCameraChanged,
);
releaseCameraSensitivity(viewer.camera, 'bikeshare');
layerState._cameraChangedAttached = false;
}
+18 -9
View File
@@ -133,8 +133,7 @@ export function createLifecycle({
// swaps (bing→osm) no-op.
layerState._lastAppliedRegime = parts.ground.currentSurfaceRegime();
if (!layerState._mapStackListener && typeof window !== 'undefined') {
layerState._mapStackListener = () =>
parts.ground.handleMapStackChanged();
layerState._mapStackListener = () => parts.ground.handleMapStackChanged();
window.addEventListener(
'gev:map-stack-changed',
layerState._mapStackListener,
@@ -222,7 +221,6 @@ export function createLifecycle({
parts.hover.handleHoverMove(movement?.endPosition),
},
);
}
function createRenderer(viewer, options) {
if (parts.renderer) throw new Error('CCTV layer is already initialized');
@@ -431,13 +429,17 @@ export function createLifecycle({
layerState._restoringViewport = Boolean(options.waitForCameraRestore);
renderer.createMarkers();
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.groundSamples = {};
record.probeClampRangeM = null;
}
bindScene(viewer, options);
if (!layerState._restoringViewport && layerState._enabled) this.resumeDisplay();
if (!layerState._restoringViewport && layerState._enabled)
this.resumeDisplay();
},
resumeViewport() {
@@ -462,7 +464,10 @@ export function createLifecycle({
parts.renderer.hideCredit();
unregisterPickOwner('cctv');
if (layerState._mapStackListener) {
window.removeEventListener('gev:map-stack-changed', layerState._mapStackListener);
window.removeEventListener(
'gev:map-stack-changed',
layerState._mapStackListener,
);
layerState._mapStackListener = null;
}
layerState._viewResizeObserver?.disconnect();
@@ -470,7 +475,10 @@ export function createLifecycle({
layerState._horizonCullListener?.();
layerState._moveStartListener?.();
layerState._removeFocusAppearListener?.();
layerState._horizonCullListener = layerState._moveStartListener = layerState._removeFocusAppearListener = null;
layerState._horizonCullListener =
layerState._moveStartListener =
layerState._removeFocusAppearListener =
null;
layerState._gizmo?.destroy();
layerState._gizmo = null;
releaseContinuousRender('cctv-adjust');
@@ -483,7 +491,7 @@ export function createLifecycle({
parts.hover.clearHoverCard();
parts.projection.clearProjectionOverlay();
layerState._cctvOverlayHost.clearSource(CCTV_OVERLAY_SOURCE_ID);
parts.geometry.destroyCoverageEntities({retainMedia:true});
parts.geometry.destroyCoverageEntities({ retainMedia: true });
for (const record of layerState._records) {
record.billboard = null;
record.coverageEntities = [];
@@ -536,7 +544,8 @@ export function createLifecycle({
const activeRecord = parts.selection.getActiveRecord();
if (activeRecord) {
parts.projection.ensureProjectionRuntime(activeRecord);
if (refreshImage) parts.frames.refreshProjectionImage(activeRecord, true);
if (refreshImage)
parts.frames.refreshProjectionImage(activeRecord, true);
}
parts.rendering.refreshHorizonCulling();
// Re-enabled mid-flight: moveStart has already fired.
+18 -6
View File
@@ -103,7 +103,8 @@ export function createProjection({
function setPlaneVisible(runtime, visible) {
if (!runtime) return;
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 (layerState._projectionOverlayOwnerId !== runtime.cameraId) {
layerState._cctvOverlayHost.setEntries(
@@ -256,7 +257,8 @@ export function createProjection({
runtime.imageLoading = 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.presentation.notifyListeners();
},
@@ -314,11 +316,21 @@ export function createProjection({
if (record.projection && !record.projection.disposed) {
const runtime = record.projection;
if (runtime.needsPlane && parts.renderer?.ready) {
const geometry = record.frustumGeometry || 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);
const geometry =
record.frustumGeometry ||
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);
if (runtime.video?.videoWidth) parts.renderer.setPlaneImage(runtime, runtime.video);
if (runtime.video?.videoWidth)
parts.renderer.setPlaneImage(runtime, runtime.video);
runtime.needsPlane = false;
runtime.lastSwappedCanvasStamp = -1;
}
+26 -10
View File
@@ -110,7 +110,12 @@ export function createCyclonesLayer({
function installSelection() {
const canvas = engine?.host.canvas ?? viewer?.scene?.canvas;
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);
clickHandler = owner;
let capturedHit = null;
@@ -185,7 +190,8 @@ export function createCyclonesLayer({
if (id) layer.setParams({ stormId: id });
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);
}
function removeSelection() {
@@ -202,14 +208,16 @@ export function createCyclonesLayer({
source: 'NOAA NHC / CPHC',
updateInterval: 300_000,
init(nextViewer, options = {}) {
if (nextViewer || options.engine) return layer.attachRenderer(nextViewer, options);
if (nextViewer || options.engine)
return layer.attachRenderer(nextViewer, options);
},
attachRenderer(nextViewer, options = {}) {
viewer = nextViewer;
engine = options.engine ?? null;
rendering = engine?.kind === 'navara'
? createNativeRendering(engine, { overlayHost })
: createRendering({ viewer, cesium, overlayHost });
rendering =
engine?.kind === 'navara'
? createNativeRendering(engine, { overlayHost })
: createRendering({ viewer, cesium, overlayHost });
if (!enabled) return;
installSelection();
if (snapshot && !snapshot.unavailable) {
@@ -287,8 +295,15 @@ export function createCyclonesLayer({
let display;
do {
display = rendering;
applied = display ? await display.setSnapshot(next, { signal: controller.signal }) : true;
} while (display !== rendering && enabled && !controller.signal.aborted && request === controller);
applied = display
? await display.setSnapshot(next, { signal: controller.signal })
: true;
} while (
display !== rendering &&
enabled &&
!controller.signal.aborted &&
request === controller
);
if (
!applied ||
!enabled ||
@@ -351,8 +366,9 @@ export function createCyclonesLayer({
selectedId !== storm.id
)
return;
const fly = engine ? engine.camera.flyToSphere.bind(engine.camera) :
viewer.camera.flyToBoundingSphere.bind(viewer.camera);
const fly = engine
? engine.camera.flyToSphere.bind(engine.camera)
: viewer.camera.flyToBoundingSphere.bind(viewer.camera);
return fly(sphere, {
duration: matchMedia?.('(prefers-reduced-motion: reduce)')
?.matches
+13 -5
View File
@@ -680,10 +680,18 @@ test('advisories delivered between scenes keep a deliberate selection without an
let reads = 0;
const drawn = [];
const layer = createCyclonesLayer({
feed: { getSnapshot: () => ++reads === 1 ? snapshot([storm()]) : pending.promise },
feed: {
getSnapshot: () =>
++reads === 1 ? snapshot([storm()]) : pending.promise,
},
createRendering: () => ({
setSnapshot(next) { drawn.push(next.storms.map(s=>s.advisoryNumber)); return true; },
setSelection() {}, clear() {}, destroy() {},
setSnapshot(next) {
drawn.push(next.storms.map((s) => s.advisoryNumber));
return true;
},
setSelection() {},
clear() {},
destroy() {},
}),
});
layer.init({});
@@ -692,11 +700,11 @@ test('advisories delivered between scenes keep a deliberate selection without an
layer.setParams({ stormId: 'ep152026' });
const refresh = layer.update();
layer.detachRenderer();
pending.resolve(snapshot([{...storm(), advisoryNumber: '11'}]));
pending.resolve(snapshot([{ ...storm(), advisoryNumber: '11' }]));
await refresh;
assert.equal(layer.getDiagnostics().selectedId, 'ep152026');
await layer.attachRenderer({});
assert.deepEqual(drawn, [['10'],['11']]);
assert.deepEqual(drawn, [['10'], ['11']]);
assert.equal(reads, 2);
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_B_COLOR = () => Cesium.Color.fromCssColorString('#ff6b6b');
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_SELECTED_PIXEL_SIZE = 13;
/** 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
* @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')
throw new TypeError('Directions require a route source');
/**
@@ -970,10 +975,9 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
}
function pickGround(screenPosition) {
const cartesian = _native ? _engine.picker.pickGlobe(screenPosition) : services.scenePick.pickGroundPosition(
_viewer,
screenPosition,
);
const cartesian = _native
? _engine.picker.pickGlobe(screenPosition)
: services.scenePick.pickGroundPosition(_viewer, screenPosition);
if (!cartesian) return null;
const carto = Cesium.Cartographic.fromCartesian(cartesian);
if (!carto) return null;
@@ -1011,7 +1015,10 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
}
function removeEntity(entity) {
if (_native) { entity?.destroy(); return; }
if (_native) {
entity?.destroy();
return;
}
if (entity && _viewer && !_viewer.isDestroyed?.())
_viewer.entities.remove(entity);
}
@@ -1099,10 +1106,13 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
function drawRoute(route) {
clearRouteGraphics();
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 {
services.annotations.ensureFlowFabricRegistered();
const positions = Cesium.Cartesian3.fromDegreesArray(route.geometry.flat());
const positions = Cesium.Cartesian3.fromDegreesArray(
route.geometry.flat(),
);
_routeEntity = _viewer.entities.add({
id: 'directions:route',
polyline: {
@@ -1563,11 +1573,18 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
if (_selectedStep !== null) _clearStepSelection();
};
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 };
} else {
_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')
document.addEventListener('keydown', _onKeyDown);
@@ -1768,8 +1785,12 @@ export function createDirectionsLayer({ services, source, createNativeRenderer }
if (_shellSeams && _suspendedFlight && _engine) {
_suspendedFlight.floorFn = _shellSeams.floorFn;
_suspendedFlight.warmFn = _shellSeams.warmFn;
if (services.camera.resumeRouteFlight(_suspendedFlight, _engine)) trackFlightProgress();
else { _flightId = 0; _flightStep = null; }
if (services.camera.resumeRouteFlight(_suspendedFlight, _engine))
trackFlightProgress();
else {
_flightId = 0;
_flightStep = null;
}
_suspendedFlight = null;
}
},
+12 -5
View File
@@ -53,7 +53,8 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
updateInterval: 60000,
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;
try {
if (viewer || engine) this.attachRenderer(viewer, { engine });
@@ -65,7 +66,8 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
},
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 });
if (!_renderer) throw new Error('Earthquake renderer unavailable');
_renderer.setVisible(_enabled);
@@ -208,9 +210,14 @@ export function createEarthquakesLayer({ source, overlayHost, renderer } = {}) {
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
: 2000;
return observations.slice(0, limit).map((row, index) =>
mapAnalystRecord({ ...row, id: row.usgsId, depth: row.depthKm }, index),
);
return observations
.slice(0, limit)
.map((row, index) =>
mapAnalystRecord(
{ ...row, id: row.usgsId, depth: row.depthKm },
index,
),
);
},
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 () => {
let resolve, polls = 0;
const h = harness({ getSnapshot() {
polls++;
return new Promise(done => { resolve = done; });
} });
let resolve,
polls = 0;
const h = harness({
getSnapshot() {
polls++;
return new Promise((done) => {
resolve = done;
});
},
});
try {
const pending = h.layer.update();
h.layer.detachRenderer();
+6 -1
View File
@@ -199,7 +199,12 @@ function buildState(discs) {
function groundEffectClass(EffectDesc, NavaraEffect) {
return class GroundEffectDesc extends EffectDesc {
static key = KEY;
static insertAfter = ['gevObservedWeather', 'gevNepalEvent', 'gevRecentImagery', 'toneMapping'];
static insertAfter = [
'gevObservedWeather',
'gevNepalEvent',
'gevRecentImagery',
'toneMapping',
];
static insertBefore = [
'gevFirmsGround',
'gevPreSharpenOverlay',
+6 -2
View File
@@ -15,7 +15,10 @@ export function createLifecycle({
init(viewer, options) {
this.attachRenderer(viewer, options);
},
attachRenderer(viewer, { engine = null, waitForCameraRestore = false } = {}) {
attachRenderer(
viewer,
{ engine = null, waitForCameraRestore = false } = {},
) {
if (layerState._destroyed) return;
if (layerState._renderer) return;
layerState._renderer = config.renderer.create({
@@ -87,7 +90,8 @@ export function createLifecycle({
this.showRenderer();
if (!layerState._fires.length && !layerState._loading)
await components.ingestion.loadHeatmap();
if (layerState._enabled && layerState._renderer === renderer) renderer.onEnable();
if (layerState._enabled && layerState._renderer === renderer)
renderer.onEnable();
},
disable() {
+6 -2
View File
@@ -15,7 +15,10 @@ export function createLifecycle({
const { registerPickOwner } = services.picking;
const binding = createAircraftSceneBinding({
id: 'flights', flightState, services, parts,
id: 'flights',
flightState,
services,
parts,
resolveModel: () => resolveAsset(PLANE_MODEL_URL),
enable: () => methods.enable(),
});
@@ -44,7 +47,8 @@ export function createLifecycle({
* @param {{ engine?: object }} [context] - LayerLifecycle's context.
*/
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')
throw new TypeError('A snapshot source is required');
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
// clamp below — one carto read per poll, one batch warm after the loop.
const viewerCarto =
flightState._renderer?.view.camera?.positionCartographic ||
null;
flightState._renderer?.view.camera?.positionCartographic || null;
const viewerLatDeg = viewerCarto
? geo.math.toDegrees(viewerCarto.latitude)
: 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) => {
let layer;
const h = fixture(t, { beforeFileReturns() { layer.detachRenderer(); } });
const h = fixture(t, {
beforeFileReturns() {
layer.detachRenderer();
},
});
layer = h.layer;
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);
await layer.attachRenderer(h.viewer);
assert.equal(h.mounted.size, 1);
+13 -8
View File
@@ -32,13 +32,11 @@ export function createGhostMapsNavaraRenderer(engine, view) {
function add(style, geom, id) {
const key = JSON.stringify(style);
if (!groups.has(key)) groups.set(key, { style, features: [] });
groups
.get(key)
.features.push({
type: 'Feature',
geometry: geom,
properties: { gevId: id },
});
groups.get(key).features.push({
type: 'Feature',
geometry: geom,
properties: { gevId: id },
});
}
for (const [index, feature] of features.entries()) {
const { style } = feature;
@@ -51,7 +49,14 @@ export function createGhostMapsNavaraRenderer(engine, view) {
};
for (const [part, geometry] of feature.geometries.entries()) {
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') {
const color =
style.iconColor && style.icon ? style.iconColor : null;
+6 -2
View File
@@ -171,8 +171,12 @@ export function createGnssIntegrityLayer({
if (!initialized || renderer)
throw new Error('Invalid GNSS scene attachment');
renderer = createRenderer({ viewer, engine, viewAnchor });
_removeCameraEnd = renderer.camera?.moveEnd?.addEventListener(scheduleCameraRefresh);
_removeCameraChanged = renderer.camera?.changed?.addEventListener(scheduleCameraRefresh);
_removeCameraEnd = renderer.camera?.moveEnd?.addEventListener(
scheduleCameraRefresh,
);
_removeCameraChanged = renderer.camera?.changed?.addEventListener(
scheduleCameraRefresh,
);
_signature = null;
if (_enabled) expire();
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 () => {
let resolve, calls = 0;
const h = harness(() => { calls++; return new Promise(done => { resolve = done; }); });
let resolve,
calls = 0;
const h = harness(() => {
calls++;
return new Promise((done) => {
resolve = done;
});
});
try {
h.layer.enable();
const pending = h.layer.update();
+8 -3
View File
@@ -28,7 +28,8 @@ export function createInstallationsLayer({
let renderer = null;
const lifecycle = {
init(viewer, options = {}) {
if (viewer || options.engine) return lifecycle.attachRenderer(viewer, options);
if (viewer || options.engine)
return lifecycle.attachRenderer(viewer, options);
},
attachRenderer(viewer, options = {}) {
if (renderer)
@@ -41,7 +42,9 @@ export function createInstallationsLayer({
const ready = renderer.lifecycle.methods.init(viewer, options);
if (state.enabled) {
renderer.lifecycle.methods.enable();
renderer.rendering.renderRecords({ claimSelection: Boolean(state.selectedId) });
renderer.rendering.renderRecords({
claimSelection: Boolean(state.selectedId),
});
}
return ready;
},
@@ -63,7 +66,9 @@ export function createInstallationsLayer({
state.coverage = { kind: 'viewport' };
clearTimeout(state.timer);
parts.viewport.clearUnavailableRetry();
services.context.clearSelectedEntityContextForLayer('military-installations');
services.context.clearSelectedEntityContextForLayer(
'military-installations',
);
parts.ingestion.setInstallationStatus('idle');
},
detachRenderer(viewer) {
@@ -355,7 +355,8 @@ export function createRendering({
if (!cold.length) return;
const owner = layerState.dataSource;
warmFireAnchorFloors(cold).then(() => {
if (!layerState.enabled || !owner || layerState.dataSource !== owner) return;
if (!layerState.enabled || !owner || layerState.dataSource !== owner)
return;
if (
!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)
layerState.lastViewBox = viewportBox(viewer);
return { box: layerState.lastViewBox ?? null, coverage: { kind: 'viewport' } };
return {
box: layerState.lastViewBox ?? null,
coverage: { kind: 'viewport' },
};
}
function viewportBox(viewer) {
+6 -2
View File
@@ -100,7 +100,7 @@ export function createRendering({
: satelliteTrack?.orbitPath?.length > 1
? satelliteTrack.orbitPath
: parts.paths.approximateOrbitPath(launch);
const solution = {activeTleText, satelliteTrack, orbitPath};
const solution = { activeTleText, satelliteTrack, orbitPath };
orbitSolutions.set(launch, solution);
return solution;
}
@@ -117,7 +117,11 @@ export function createRendering({
const orbitAllowed = parts.policyHelpers.launchStatusAllowsOrbit(
launch.status,
);
const { satelliteTrack, orbitPath } = orbitForLaunch(launch, activeTleText, orbitAllowed);
const { satelliteTrack, orbitPath } = orbitForLaunch(
launch,
activeTleText,
orbitAllowed,
);
launch.recoveryStages.forEach((stage) => {
stage.endpoint = parts.paths.landingEndpoint(
stage,
+4 -1
View File
@@ -15,7 +15,10 @@ export function createLifecycle({
const { setMilitaryLayerActive } = services.militaryRegistry;
const binding = createAircraftSceneBinding({
id: 'military', flightState, services, parts,
id: 'military',
flightState,
services,
parts,
resolveModel: () => resolveAsset(JET_MODEL_URL),
enable: () => methods.enable(),
});
+2 -1
View File
@@ -383,7 +383,8 @@ export function createQueries({
const trackedPos = isTracked
? parts.motion._trackedDisplayCached()
: 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;
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 () => {
const first = createFakeEngine(), second = createFakeEngine();
let polls = 0, binding;
const first = createFakeEngine(),
second = createFakeEngine();
let polls = 0,
binding;
const layer = createApplicationMilitary({
surface: defaultSurface, militaryRegistry,
source: { getSnapshot: async () => militarySnapshotOf(['ae0001'], 1700000000000 + (++polls)*1000) },
renderer: {create: ({engine}) => (binding = recordingAircraftRenderer(engine))},
surface: defaultSurface,
militaryRegistry,
source: {
getSnapshot: async () =>
militarySnapshotOf(['ae0001'], 1700000000000 + ++polls * 1000),
},
renderer: {
create: ({ engine }) => (binding = recordingAircraftRenderer(engine)),
},
});
try {
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(layer.getTrackedInfo().icao24, 'ae0001');
await layer.update(null);
layer.attachRenderer(null, {engine: second});
layer.attachRenderer(null, { engine: second });
assert.ok(binding.follow.current());
assert.equal(layer.getStats().lastUpdate, 1700000002000);
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);
} finally {
layer.destroy(null);
first.destroy(); second.destroy();
first.destroy();
second.destroy();
}
});
+7 -2
View File
@@ -93,7 +93,11 @@ export function createMilitarySnapshotRenderer({
function presentRecords() {
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);
}
}
@@ -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
// from the fresh airborne history (touchdown needs no action — the
// 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
*/
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
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) {
if (left === right) return true;
return left?.length === right.length && right.every((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)));
return (
left?.length === right.length &&
right.every(
(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. */
@@ -184,7 +194,10 @@ export function createFirePerimetersLayer({
const pickId = picking.resolvePickId(picked);
if (typeof pickId !== 'string' || !pickId.startsWith(PICK_PREFIX))
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;
}
@@ -256,7 +269,8 @@ export function createFirePerimetersLayer({
updateInterval: 300000,
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;
try {
if (viewer || engine) this.attachRenderer(viewer, { engine });
@@ -267,8 +281,13 @@ export function createFirePerimetersLayer({
},
attachRenderer(viewer, { engine = null } = {}) {
if (!initialized || renderer) throw new Error('Invalid fire perimeter scene attachment');
renderer = createRenderer({ viewer, engine, screenSpaceEventHandlerFactory });
if (!initialized || renderer)
throw new Error('Invalid fire perimeter scene attachment');
renderer = createRenderer({
viewer,
engine,
screenSpaceEventHandlerFactory,
});
renderer.setVisible(_enabled);
if (geometry.size) renderer.replace([...geometry.values()]);
if (_enabled) {
@@ -333,7 +352,9 @@ export function createFirePerimetersLayer({
);
if (
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();
_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' };
let geometry = fire.polygons;
let polls = 0;
const h = harness({getSnapshot: async () => { polls++; return [{...fire, polygons: geometry}]; }}, {
pick: () => ({id: 'fire-perimeter:incident:one:0'}),
});
const h = harness(
{
getSnapshot: async () => {
polls++;
return [{ ...fire, polygons: geometry }];
},
},
{
pick: () => ({ id: 'fire-perimeter:incident:one:0' }),
},
);
try {
await h.layer.update();
h.clicks.handler({position: {x: 0, y: 0}});
assert.equal(h.overlay.entries.get('fire-perimeters')[0].id, 'fire-perimeter-card:incident:one');
h.clicks.handler({ position: { x: 0, y: 0 } });
assert.equal(
h.overlay.entries.get('fire-perimeters')[0].id,
'fire-perimeter-card:incident:one',
);
h.layer.detachRenderer();
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);
const record = h.layer.getAnalystRecords()[0];
assert.equal(record.id, 'incident:one');
h.layer.attachRenderer(h.viewer);
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);
} finally { h.layer.destroy(); }
} finally {
h.layer.destroy();
}
});
+4 -1
View File
@@ -46,7 +46,10 @@ export function createRendering({
function publishRadioOverlayEntries() {
layerState._overlayPublishTimer = null;
if (!parts.renderer?.ready || !parts.interaction.radioPresentationAllowed()) {
if (
!parts.renderer?.ready ||
!parts.interaction.radioPresentationAllowed()
) {
parts.clustering.resetRadioClusterOverlayIdentities();
layerState._overlayDiagnostics =
parts.clustering.emptyRadioOverlayDiagnostics();
+1 -1
View File
@@ -830,7 +830,7 @@ export function createRecentImageryLayer({
forgetLease();
renderer?.destroy();
renderer = null;
_hostInfo = {collection: null, kind: 'none'};
_hostInfo = { collection: null, kind: 'none' };
_viewer = _engine = _tileset = null;
_keptGeneration = null;
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)
)
return false;
if (layerState._restoringViewport && layerState._trackedNorad === id) return true;
if (layerState._restoringViewport && layerState._trackedNorad === id)
return true;
parts.tracking._cancelPendingTrackingRestore();
parts.tracking._trackSatellite(id, { origin });
return layerState._trackedNorad === id;
+53 -12
View File
@@ -73,22 +73,47 @@ export function createLifecycle({
detachRenderer() {
const renderer = layerState._renderer;
if (!renderer) return;
layerState._waitForRenderer ??= new Promise(resolve => { layerState._resumeRenderer = resolve; });
layerState._waitForRenderer ??= new Promise((resolve) => {
layerState._resumeRenderer = resolve;
});
layerState._clickHandler?.destroy();
layerState._trackedEntityChangedRemove?.();
layerState._preRenderListener?.();
layerState._clickHandler = layerState._trackedEntityChangedRemove = layerState._preRenderListener = null;
layerState._clickHandler =
layerState._trackedEntityChangedRemove =
layerState._preRenderListener =
null;
document.removeEventListener('keydown', parts.interaction._onKeyDown);
unregisterPickOwner('satellites');
if (layerState._trackedEntity) {
layerState._handoffOrigin = layerState._trackedEntity.gevSelectionOrigin;
layerState._handoffOrigin =
layerState._trackedEntity.gevSelectionOrigin;
renderer.follow.release();
renderer.removeTracked(layerState._trackedEntity);
layerState._trackedEntity = null;
}
for (const [id, point] of layerState._points) {
const {position, show, pixelSize, color, outlineColor, outlineWidth, scaleByDistance, disableDepthTestDistance} = point;
layerState._points.set(id, {id, position: {...position}, show, pixelSize, color, outlineColor, outlineWidth, scaleByDistance, disableDepthTestDistance});
const {
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()) {
renderer.removeOrbit(path.primitive);
@@ -97,23 +122,36 @@ export function createLifecycle({
layerState._detectionObjects.clear();
renderer.removePoints(layerState._pointCollection);
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);
if (layerState._enabled) releaseContinuousRender('satellites');
},
attachRenderer(viewer, {engine = null, waitForCameraRestore = false} = {}) {
attachRenderer(
viewer,
{ engine = null, waitForCameraRestore = false } = {},
) {
layerState._engine = engine;
const renderer = services.renderer.create({viewer,engine});
const renderer = services.renderer.create({ viewer, engine });
layerState._renderer = renderer;
layerState._viewer = renderer.view;
layerState._pointCollection = renderer.createPoints();
for (const [id, spec] of layerState._points)
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) {
path.primitive = renderer.createOrbit({ positions: path.basePositions, width: id === ISS_NORAD ? 2.5 : 2, color: path.color });
path.primitive.show = layerState._enabled && layerState._params.showOrbits;
path.primitive = renderer.createOrbit({
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
// share-selected camera state may itself wait for that publication.
@@ -129,7 +167,10 @@ export function createLifecycle({
layerState._restoringViewport = false;
if (layerState._enabled) {
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);
}
layerState._resumeRenderer?.();
+4 -1
View File
@@ -412,7 +412,10 @@ export function createTracking({
refreshTrackedReadout(layerState._trackedEntity);
}
function _trackSatellite(noradId, { origin = 'programmatic', resuming = false } = {}) {
function _trackSatellite(
noradId,
{ origin = 'programmatic', resuming = false } = {},
) {
if (!resuming) _clearTracking(false, { origin });
const point = layerState._points.get(noradId);
+20 -14
View File
@@ -2,15 +2,8 @@
import { Effect, EffectAttribute } from 'postprocessing';
import { Matrix4, Uniform } from 'three';
import { rectangleFrame } from './navaraCellGeometry.js';
import {
viewFrame,
groundClipBounds,
} from './navaraGroundGeometry.js';
import {
createGroundGrid,
GRID_COLS,
GRID_ROWS,
} from './navaraGroundGrid.js';
import { viewFrame, groundClipBounds } from './navaraGroundGeometry.js';
import { createGroundGrid, GRID_COLS, GRID_ROWS } from './navaraGroundGrid.js';
import {
createGroundTexture,
createGroundStorage,
@@ -269,26 +262,39 @@ function groundEffectClass(EffectDesc, NavaraEffect) {
// The SDK allows one active effect per key. Compose enabled datasets in
// catalog order so both can be drawn without replacing one another.
const views = new WeakMap();
export function createGroundCellDisplay(engine, { view, EffectDesc, NavaraEffect }, order) {
export function createGroundCellDisplay(
engine,
{ view, EffectDesc, NavaraEffect },
order,
) {
if (!registeredViews.has(view)) {
view.registerEffect(KEY, groundEffectClass(EffectDesc, NavaraEffect));
registeredViews.add(view);
}
let pass = views.get(view);
if (!pass) {
pass = { inputs: new Set(), owner: { state: buildState([]) }, handle: null };
pass = {
inputs: new Set(),
owner: { state: buildState([]) },
handle: null,
};
views.set(view, pass);
}
const input = { cells: [], visible: false, order };
pass.inputs.add(input);
function sync() {
const cells = [...pass.inputs].filter(item => item.visible)
.sort((a,b) => a.order - b.order).flatMap(item => item.cells);
const cells = [...pass.inputs]
.filter((item) => item.visible)
.sort((a, b) => a.order - b.order)
.flatMap((item) => item.cells);
const previous = pass.owner.state;
pass.owner.state = buildState(cells);
try {
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 {
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 carto = Cesium.Cartographic.fromDegrees(lon, lat);
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 (!plausibleSurfaceHeight(height)) height = fallback;
return plausibleSurfaceHeight(height) ? height : 0;
@@ -55,10 +58,24 @@ export function createCameraFraming({ state }) {
if (flight === current) flight = null;
};
if (state.viewer.engine) {
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,
complete:release,cancel:release});
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,
complete: release,
cancel: release,
},
);
return;
}
state.viewer.camera.flyToBoundingSphere(
+4 -1
View File
@@ -11,7 +11,10 @@ export function createCredits() {
function show(viewer, def) {
if (shown.has(def.id)) return;
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;
}
if (!viewer?.creditDisplay) return;
+3 -1
View File
@@ -32,7 +32,9 @@ export function createGroundClick({ state, openNearest, isAvailable }) {
)
.map((entry) => entry.def.id);
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);
if (!carto) return false;
return openNearest(
+34 -22
View File
@@ -195,7 +195,8 @@ export function createStreetLevelLayer({
cancelGroundRetry();
const ready = state.enabled && groundClickHeightOk(state.viewer);
if (
state.enabled && state.viewer &&
state.enabled &&
state.viewer &&
!ready &&
groundRetries < GROUND_RETRY_LIMIT &&
groundUnderCamera(state.viewer) === null
@@ -266,12 +267,15 @@ export function createStreetLevelLayer({
/** Ground under the screen centre; null for sky, undefined when it cannot aim. */
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;
if (!pick || !canvas?.clientWidth || !canvas?.clientHeight)
return undefined;
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 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 carto = position && geo.cartographic.fromCartesian(position);
return carto
? {
@@ -485,10 +489,13 @@ export function createStreetLevelLayer({
},
init(viewer, options) {
this.attachRenderer(viewer,options);
this.attachRenderer(viewer, options);
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);
state.viewer = map;
waitingForView = waitForCameraRestore;
@@ -497,45 +504,50 @@ export function createStreetLevelLayer({
parts.marker.setVisible(state.enabled);
for (const entry of state.providers.values()) {
entry.instance.init(map);
if(state.enabled && entry.on){
if (state.enabled && entry.on) {
entry.instance.activate(map);
entry.instance.repaintMap?.();
if(providerAvailable(entry)) parts.credits.show(map,entry.def);
if (providerAvailable(entry)) parts.credits.show(map, entry.def);
}
}
if (state.enabled) {
parts.selection.install(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();
}
},
resumeViewport() {
if (!waitingForView) return;
waitingForView=false;
if(state.enabled && state.viewer) {
for(const entry of activeEntries())entry.instance.refreshCoverage?.();
waitingForView = false;
if (state.enabled && state.viewer) {
for (const entry of activeEntries()) entry.instance.refreshCoverage?.();
remeasureGround();
}
},
detachRenderer() {
const map=state.viewer;
if(!map)return;
waitingForView=true;
removeCameraListener?.();removeCameraListener=null;
const map = state.viewer;
if (!map) return;
waitingForView = true;
removeCameraListener?.();
removeCameraListener = null;
cancelGroundRetry();
parts.framing.cancel();
parts.selection.uninstall();
for(const entry of state.providers.values()){
if(entry.instance.detachMap)entry.instance.detachMap();
else if(entry.on)entry.instance.deactivate();
for (const entry of state.providers.values()) {
if (entry.instance.detachMap) entry.instance.detachMap();
else if (entry.on) entry.instance.deactivate();
}
parts.marker.destroy(map);
parts.credits.hideAll(map);
unsubscribeMapStack?.();
unsubscribeMapStack=null;
mapStack=null;
state.viewer=null;
unsubscribeMapStack = null;
mapStack = null;
state.viewer = null;
},
enable(viewer) {
state.enabled = true;
+25 -7
View File
@@ -246,8 +246,13 @@ export function createLineCoverage({
const sequence = entry.sequenceList.find(({ id }) => id === sequenceId);
if (sequence && passes(sequence, current)) parts.push(...sequence.parts);
}
if(state.viewer?.engine){
state.coverage.highlight=state.viewer.lines([{id:sequenceId,parts}],colors.selected,tuning.lineWidthPx+1,pickPrefix);
if (state.viewer?.engine) {
state.coverage.highlight = state.viewer.lines(
[{ id: sequenceId, parts }],
colors.selected,
tuning.lineWidthPx + 1,
pickPrefix,
);
requestRender();
return;
}
@@ -270,8 +275,13 @@ export function createLineCoverage({
// Lookup, picking and counts follow what is drawn, not the whole tile.
const sequences = drawnSequences(entry);
entry.sequences = new Map(sequences.map((s) => [s.id, s]));
if(state.viewer?.engine){
const group = state.viewer.lines(sequences,colors.coverage,tuning.lineWidthPx,pickPrefix);
if (state.viewer?.engine) {
const group = state.viewer.lines(
sequences,
colors.coverage,
tuning.lineWidthPx,
pickPrefix,
);
entry.primitives = group ? [group] : [];
return;
}
@@ -409,7 +419,9 @@ export function createLineCoverage({
*/
function wantedTiles(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)
? cameraHeight - (ground ?? 0)
: null;
@@ -438,7 +450,13 @@ export function createLineCoverage({
/** Recompute the tile set for the current camera and reconcile primitives. */
function refresh() {
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);
if (hint) {
state.coverage.hint = hint;
@@ -550,7 +568,7 @@ export function createLineCoverage({
releaseGraphics() {
detach();
highlight(null);
for(const entry of state.coverage.tiles.values()) detachPrimitive(entry);
for (const entry of state.coverage.tiles.values()) detachPrimitive(entry);
},
refresh,
clear,
@@ -256,7 +256,9 @@ export function createPanoramaxProvider({
* from high up the click may land tens of metres off the line.
*/
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;
return Math.min(
LINE_CLICK_RADIUS_MAX_M,
+19 -18
View File
@@ -10,11 +10,13 @@ export function createSelection({ state, parts }) {
const viewer = state.viewer;
if (!viewer || !state.enabled) return;
if (input?.isPointerFree && !input.isPointerFree()) return;
const picked = viewer.engine ? click.picked : viewer.scene.pick(
click.position,
CLICK_PICK_WINDOW_PX,
CLICK_PICK_WINDOW_PX,
);
const picked = viewer.engine
? click.picked
: viewer.scene.pick(
click.position,
CLICK_PICK_WINDOW_PX,
CLICK_PICK_WINDOW_PX,
);
const id = picking?.resolvePickId
? picking.resolvePickId(picked)
: 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). */
function groundPointAt(viewer, screenPosition) {
const position = viewer.engine ? viewer.engine.picker.pickGlobe(screenPosition) : state.services.scenePick?.pickGroundPosition?.(
viewer,
screenPosition,
);
const position = viewer.engine
? viewer.engine.picker.pickGlobe(screenPosition)
: state.services.scenePick?.pickGroundPosition?.(viewer, screenPosition);
const carto = position && Cesium.Cartographic.fromCartesian(position);
return carto
? {
@@ -69,16 +70,16 @@ export function createSelection({ state, parts }) {
function install(viewer) {
if (state.clickHandler) return;
if (viewer.engine) {
const remove = viewer.engine.input.on('click',onClick,{pick:true});
state.clickHandler = { isDestroyed:()=>false,destroy:remove };
const remove = viewer.engine.input.on('click', onClick, { pick: true });
state.clickHandler = { isDestroyed: () => false, destroy: remove };
} else {
state.clickHandler = new Cesium.ScreenSpaceEventHandler(
viewer.scene.canvas,
);
state.clickHandler.setInputAction(
onClick,
Cesium.ScreenSpaceEventType.LEFT_CLICK,
);
state.clickHandler = new Cesium.ScreenSpaceEventHandler(
viewer.scene.canvas,
);
state.clickHandler.setInputAction(
onClick,
Cesium.ScreenSpaceEventType.LEFT_CLICK,
);
}
document.addEventListener('keydown', onKeyDown);
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).
*/
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.
if (
!force &&
+22 -5
View File
@@ -2,7 +2,8 @@ import * as Cesium from 'cesium';
import { wrapLon } from './tileMath.js';
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;
/** Equirectangular metres between {lon, lat} points, the short way round ±180°; street scale. */
export function metresBetween(a, b) {
@@ -70,7 +71,9 @@ function raisedEllipsoid(ellipsoid, height) {
function groundHit(camera, point, ellipsoid, maxRange, engine) {
let cartesian = null;
try {
cartesian = engine ? engine.picker.pickGlobe(point) : camera.pickEllipsoid(point, ellipsoid);
cartesian = engine
? engine.picker.pickGlobe(point)
: camera.pickEllipsoid(point, ellipsoid);
} catch {
cartesian = null;
}
@@ -78,10 +81,18 @@ function groundHit(camera, point, ellipsoid, maxRange, engine) {
if (
Number.isFinite(maxRange) &&
(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 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. */
@@ -120,7 +131,13 @@ export function visibleBbox(
for (let j = 0; j <= BBOX_GRID; j++) {
point.x = (width * i) / 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);
}
}
+8 -2
View File
@@ -292,8 +292,14 @@ export function updateCableReferenceTip(
return true;
}
export function updateCableReferenceStem(record, cameraPositionWC, canvasHeight, fov) {
if (!updateCableReferenceTip(record, cameraPositionWC, canvasHeight, fov)) return false;
export function updateCableReferenceStem(
record,
cameraPositionWC,
canvasHeight,
fov,
) {
if (!updateCableReferenceTip(record, cameraPositionWC, canvasHeight, fov))
return false;
record.stemPositionBufferIndex = 1 - record.stemPositionBufferIndex;
const stemPositions =
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([
Object.freeze({
key: '_billboardCollection',
get type() { return Cesium.BillboardCollection; },
get type() {
return Cesium.BillboardCollection;
},
}),
Object.freeze({
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
// the whole jam batch, no geometry rebuild. Null outside live heatline mode.
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) {
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. */
function clearDots() {
services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic');
services.credits?.hideOsmCredit?.(
layerState._engine ?? layerState._viewer,
'traffic',
);
parts.retention.clear();
if (layerState._dots.length) layerState._motion.rebuilds++;
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;
if (layerState._liveMode) {
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) => {
@@ -236,7 +239,8 @@ export function createFlow({ state: layerState, services, parts, source }) {
return false;
if (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;
layerState._surfaceRevision = revision;
@@ -384,7 +388,9 @@ export function createFlow({ state: layerState, services, parts, source }) {
maxX = -Infinity,
maxY = -Infinity;
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;
minX = Math.min(minX, screen.x);
maxX = Math.max(maxX, screen.x);
@@ -411,7 +417,9 @@ export function createFlow({ state: layerState, services, parts, source }) {
roadDotBudget(altitude),
);
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)
admitted.sort(
(a, b) =>
+3 -1
View File
@@ -102,7 +102,9 @@ export function createReticleFootprint() {
dlon -= 360 * Math.round(dlon / 360);
dx = dlon * 111.195 * Math.cos(nadir.latitude);
} 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;
const d = picked.direction;
dx =
+7 -5
View File
@@ -275,11 +275,13 @@ export function createIngestion({
let estimated = services.ground?.cachedGroundFloor?.(centerLat, centerLon);
if (!Number.isFinite(estimated)) {
try {
estimated = scene?.engine ? scene.sampleHeight(location) : scene?.globe?.show
? scene.globe.getHeight?.(location)
: scene?.sampleHeightSupported
? scene.sampleHeight(location, [layerState._pointCollection])
: undefined;
estimated = scene?.engine
? scene.sampleHeight(location)
: scene?.globe?.show
? scene.globe.getHeight?.(location)
: scene?.sampleHeightSupported
? scene.sampleHeight(location, [layerState._pointCollection])
: undefined;
} catch {
/* the local mesh can still be loading */
}
+114 -40
View File
@@ -21,10 +21,12 @@ export function createLifecycle({
}
function bindScene() {
const viewer = layerState._viewer, engine = layerState._engine;
const viewer = layerState._viewer,
engine = layerState._engine;
const camera = engine?.camera ?? viewer.camera;
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._lastAnimTime = 0;
parts.animation.syncRenderDemand();
@@ -36,11 +38,17 @@ export function createLifecycle({
parts.timing.clearTrafficTimingEntries();
layerState._trafficTimingCurrentAnchor = null;
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._arrivalRemover = camera.moveEnd.addEventListener(() => parts.viewport.onCameraChanged({immediate:true}));
claimCameraSensitivity(engine ?? camera,'traffic',0.05);
layerState._cameraRemover = camera.changed.addEventListener(
parts.viewport.onCameraChanged,
);
layerState._arrivalRemover = camera.moveEnd.addEventListener(() =>
parts.viewport.onCameraChanged({ immediate: true }),
);
claimCameraSensitivity(engine ?? camera, 'traffic', 0.05);
}
function unbindScene() {
@@ -48,15 +56,22 @@ export function createLifecycle({
layerState._surfaceFrameRemover?.();
layerState._preRenderRemover?.();
layerState._trafficTimingMoveEndRemover?.();
for (const remove of layerState._trafficTimingPostRenderRemovers || []) remove();
for (const remove of layerState._trafficTimingPostRenderRemovers || [])
remove();
layerState._trafficTimingPostRenderRemovers = null;
layerState._trafficTimingCurrentAnchor = null;
layerState._cameraRemover?.();
// 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._surfaceFrameRemover = layerState._preRenderRemover = layerState._cameraRemover = layerState._arrivalRemover = layerState._trafficTimingMoveEndRemover = null;
releaseCameraSensitivity(layerState._engine ?? camera,'traffic');
layerState._surfaceFrameRemover =
layerState._preRenderRemover =
layerState._cameraRemover =
layerState._arrivalRemover =
layerState._trafficTimingMoveEndRemover =
null;
releaseCameraSensitivity(layerState._engine ?? camera, 'traffic');
}
const methods = {
@@ -99,8 +114,12 @@ export function createLifecycle({
layerState._stylePreset =
document?.documentElement?.dataset?.gevStyle || 'normal';
if (!layerState._styleListenerBound) {
layerState._styleListener = (e) => parts.style.setStylePreset(e?.detail?.style);
window.addEventListener('gev:style-change', layerState._styleListener);
layerState._styleListener = (e) =>
parts.style.setStylePreset(e?.detail?.style);
window.addEventListener(
'gev:style-change',
layerState._styleListener,
);
layerState._styleListenerBound = true;
}
}
@@ -108,25 +127,42 @@ export function createLifecycle({
console.log('[Data:Traffic] Initialized');
},
attachRenderer(viewer, {engine=null,waitForCameraRestore=false}={}) {
attachRenderer(
viewer,
{ engine = null, waitForCameraRestore = false } = {},
) {
layerState._restoringViewport = waitForCameraRestore;
layerState._viewer = viewer;
layerState._engine = engine;
layerState._native = !viewer && engine ? services.nativeRenderer(engine) : null;
layerState._native =
!viewer && engine ? services.nativeRenderer(engine) : 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);
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;
if (!waitForCameraRestore) readyForSurfaceWork();
if (layerState._enabled) {
bindScene();
parts.rendering.rebuildHeatLines(layerState._roads);
if (layerState._dots.length && layerState._roads.some(r=>!r.directFlow))
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});
if (
layerState._dots.length &&
layerState._roads.some((r) => !r.directFlow)
)
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() {
@@ -136,40 +172,70 @@ export function createLifecycle({
if (!layerState._enabled) return;
if (layerState._populationPending) parts.retention.resume();
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
// shows coarse tiles, so it samples provisionally and refines once its
// surface has loaded ('Handoff refined'). A refinement here never
// refined again and left vehicles on those coarse heights.
void parts.flow.applyFlowThenRender(previous.roads, previous.bounds, layerState._loadGeneration,
parts.viewport.getCameraAltitude(), 'Handoff').catch(error => {
if (error?.name !== 'AbortError') console.warn('[Data:Traffic] Surface refresh failed', error);
void parts.flow
.applyFlowThenRender(
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() {
if(!layerState._pointCollection)return;
if (!layerState._pointCollection) return;
unbindScene();
clearTimeout(layerState._fetchTimeout);
layerState._surfaceRefineRemove?.();
layerState._surfaceRefineRemove = null;
layerState._surfaceRevision = (layerState._surfaceRevision || 0)+1;
layerState._surfaceRevision = (layerState._surfaceRevision || 0) + 1;
layerState._preparedRoads?.clear();
parts.rendering.removeHeatLines();
services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer,'traffic');
for(const dot of layerState._dots) {
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};
services.credits?.hideOsmCredit?.(
layerState._engine ?? layerState._viewer,
'traffic',
);
for (const dot of layerState._dots) {
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._native?.destroy();
if(layerState._renderHeld)services.render.releaseContinuousRender('traffic');
layerState._renderHeld=false;
layerState._pointCollection=layerState._viewer=layerState._engine=layerState._native=null;
layerState._waitForScene ??= new Promise(resolve=>{layerState._sceneReady=resolve;});
if (layerState._renderHeld)
services.render.releaseContinuousRender('traffic');
layerState._renderHeld = false;
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) {
layerState._enabled = false;
layerState._surfaceInput = null;
services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic');
services.credits?.hideOsmCredit?.(
layerState._engine ?? layerState._viewer,
'traffic',
);
parts.animation.syncRenderDemand();
clearTimeout(layerState._fetchTimeout);
clearInterval(layerState._enableKickTimer);
@@ -255,7 +324,9 @@ export function createLifecycle({
destroy(viewer) {
this.disable(viewer);
if (layerState._pointCollection) {
layerState._viewer?.scene.primitives.remove(layerState._pointCollection);
layerState._viewer?.scene.primitives.remove(
layerState._pointCollection,
);
layerState._native?.destroy();
layerState._native = null;
layerState._pointCollection = null;
@@ -263,7 +334,10 @@ export function createLifecycle({
parts.rendering.removeHeatLines();
layerState._tileCache.clear();
if (layerState._styleListenerBound && typeof window !== 'undefined') {
window.removeEventListener('gev:style-change', layerState._styleListener);
window.removeEventListener(
'gev:style-change',
layerState._styleListener,
);
layerState._styleListenerBound = false;
}
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). */
export const HEAT_SLOW_COLOR =
rgbaFromCss('#f0b23e').withAlpha(0.2);
export const HEAT_SLOW_COLOR = rgbaFromCss('#f0b23e').withAlpha(0.2);
/** @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;
if (layerState._native) {
const roadsFor = bucket => kept.filter(item=>item.bucket===bucket).map(item=>item.road);
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);
const roadsFor = (bucket) =>
kept.filter((item) => item.bucket === bucket).map((item) => item.road);
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;
parts.animation.syncRenderDemand();
return;
@@ -228,7 +239,8 @@ export function createRendering({
areaDist = Math.max(
areaDist,
Cesium.Cartesian3.distance(
layerState._engine?.camera.livePosition ?? layerState._viewer.camera.positionWC,
layerState._engine?.camera.livePosition ??
layerState._viewer.camera.positionWC,
wp,
),
);
@@ -247,10 +259,18 @@ export function createRendering({
: null;
parts.retention.reconcile(filteredRoads, altitude, replace);
if (layerState._dots.length && filteredRoads.some((r) => !r.directFlow))
services.credits?.showOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic', {
openMapTiles: true,
});
else services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic');
services.credits?.showOsmCredit?.(
layerState._engine ?? layerState._viewer,
'traffic',
{
openMapTiles: true,
},
);
else
services.credits?.hideOsmCredit?.(
layerState._engine ?? layerState._viewer,
'traffic',
);
parts.model.recolorDotsInPlace(label);
const renderMetrics = state
+11 -2
View File
@@ -217,7 +217,8 @@ export function createRetention({ state, parts }) {
const blend = Math.min(1, dt * 12);
const scene = state._trafficScene?.() ?? state._viewer?.scene;
const canProject =
scene?.canvas?.clientHeight && (scene.project || (scene.camera && scene.frameState));
scene?.canvas?.clientHeight &&
(scene.project || (scene.camera && scene.frameState));
state._heightPending = false;
for (let r = 0; r < state._roads.length; r++) {
const road = state._roads[r];
@@ -300,5 +301,13 @@ export function createRetention({ state, parts }) {
state._populationPending = 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._engine = null;
layerState._native = null;
layerState._trafficScene = () => layerState._native?.surface ?? layerState._viewer?.scene;
layerState._trafficScene = () =>
layerState._native?.surface ?? layerState._viewer?.scene;
/** @type {Cesium.PointPrimitiveCollection|null} */
+6 -3
View File
@@ -50,7 +50,7 @@ export function observeTrafficSurface(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)
return { ready: surfaceTilesReady(scene) };
const frame = renderedFrames.get(scene);
@@ -280,7 +280,9 @@ export async function prepareRoadSurfaces(
cache = { surface, heights: new Map() };
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
? Cesium.Math.toDegrees(cameraCarto.longitude)
: 0;
@@ -345,7 +347,8 @@ export async function prepareRoadSurfaces(
if (validHeight(sampled)) metrics.depthHits++;
}
if (
!scene.engine && !scene.globe?.show &&
!scene.engine &&
!scene.globe?.show &&
!validHeight(sampled) &&
scene.sampleHeightSupported
) {
+31 -8
View File
@@ -24,7 +24,9 @@ export function createViewport({ state: layerState, services, parts, source }) {
*/
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;
}
@@ -35,7 +37,9 @@ export function createViewport({ state: layerState, services, parts, source }) {
*/
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;
return {
south: geo.math.toDegrees(rect.south),
@@ -60,18 +64,23 @@ export function createViewport({ state: layerState, services, parts, source }) {
*/
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;
const nadirLat = geo.math.toDegrees(carto.latitude);
const nadirLon = geo.math.toDegrees(carto.longitude);
let hitLat;
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 height = canvas.clientHeight || canvas.height;
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),
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. */
function onCameraChanged(options) {
if (!layerState._enabled || layerState._waitForScene || layerState._restoringViewport) return;
if (
!layerState._enabled ||
layerState._waitForScene ||
layerState._restoringViewport
)
return;
clearTimeout(layerState._fetchTimeout);
if (getCameraAltitude() > ACTIVATION_ALTITUDE) {
clearTimeout(layerState._retryTimer);
@@ -197,10 +211,19 @@ export function createViewport({ state: layerState, services, parts, source }) {
: null;
layerState._fetchTimeout = setTimeout(
() => {
if (!layerState._enabled || layerState._waitForScene || layerState._restoringViewport) return;
if (
!layerState._enabled ||
layerState._waitForScene ||
layerState._restoringViewport
)
return;
const alt = getCameraAltitude();
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();
if (!bounds) return;
const center = footprint ? null : getFetchCenter();
+15 -4
View File
@@ -141,7 +141,10 @@ export function createLifecycle({
state._viewer = null;
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) {
for (const [key, entry] of state._vehicles) {
parts.rendering.addMarker(key, entry);
@@ -203,7 +206,8 @@ export function createLifecycle({
parts.rendering.refreshVisibility();
parts.trails.rebind();
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() {
if (!state._restoringViewport) return;
@@ -217,7 +221,10 @@ export function createLifecycle({
clearInterval(state._maintenanceTimer);
clearTimeout(state._cameraDebounceTimer);
clearTimeout(state._visibilityTimer);
state._maintenanceTimer = state._cameraDebounceTimer = state._visibilityTimer = null;
state._maintenanceTimer =
state._cameraDebounceTimer =
state._visibilityTimer =
null;
parts.selection.removeClickHandler();
unregisterPickOwner('transit');
state._cameraChangedRemove?.();
@@ -238,7 +245,11 @@ export function createLifecycle({
if (state._renderHeld) releaseContinuousRender('transit');
state._renderHeld = false;
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'
)
return;
if (!state._enabled || !parts.renderer?.ready || state._restoringViewport) return;
if (!state._enabled || !parts.renderer?.ready || state._restoringViewport)
return;
refreshViewBounds();
const altitude = getCameraAltitude();
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 restored = [];
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;
globalThis.document = Object.assign(new EventTarget(), { getElementById: () => null });
t.after(() => { globalThis.document = previousDocument; });
let polls = 0, histories = 0, deliver;
globalThis.document = Object.assign(new EventTarget(), {
getElementById: () => null,
});
t.after(() => {
globalThis.document = previousDocument;
});
let polls = 0,
histories = 0,
deliver;
const snapshot = (records) => ({
records, source: 'AIS fixture', complete: true, transportStatus: 'live',
rawRowCount: records.length, observedAtMs: 500, freshness: 'current',
records,
source: 'AIS fixture',
complete: true,
transportStatus: 'live',
rawRowCount: records.length,
observedAtMs: 500,
freshness: 'current',
});
const layer = createVesselLayer({
services: injected,
source: {
getSnapshot: () => ++polls === 1
? snapshot([row('111', 40.6, -73.9)])
: new Promise((resolve) => { deliver = resolve; }),
getSnapshot: () =>
++polls === 1
? snapshot([row('111', 40.6, -73.9)])
: new Promise((resolve) => {
deliver = resolve;
}),
getTrack: async () => {
histories++;
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;
const getHost = () =>
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 shownTime = () => (noFrame ? null : rendering?.getDiagnostics().time ?? lastShown);
const shownTime = () =>
noFrame ? null : (rendering?.getDiagnostics().time ?? lastShown);
const unsubscribeClock = clock?.subscribe(() => {
if (!clock.getState().playing || suspended()) rendering?.cancelPrefetch?.();
notify();
@@ -298,23 +305,32 @@ export function createWeatherLayer({
motion = matchMedia?.('(prefers-reduced-motion: reduce)');
motion?.addEventListener?.('change', 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 } = {}) {
viewer = nextViewer;
sceneEngine = engine;
const options = { viewer, cesium, getHost, onChange: notify };
rendering = engine?.kind === 'navara'
? createNativeRendering(engine, options)
: createRendering(options);
rendering =
engine?.kind === 'navara'
? createNativeRendering(engine, options)
: createRendering(options);
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);
if (!enabled) return;
watchCamera();
if (!manifest) return;
const time = clock && !isLatest() ? clock.selectFor(id, clock.getState().target) :
(followLatest ? manifest.latest : lastShown);
const time =
clock && !isLatest()
? clock.selectFor(id, clock.getState().target)
: followLatest
? manifest.latest
: lastShown;
if (time) void show(time);
else rendering.setHidden(true);
},
@@ -326,7 +342,10 @@ export function createWeatherLayer({
timer = null;
removeCamera?.();
removeCamera = null;
eventTarget?.removeEventListener?.('gev:map-stack-changed', onMapStackChanged);
eventTarget?.removeEventListener?.(
'gev:map-stack-changed',
onMapStackChanged,
);
rendering?.clear();
rendering = null;
hostCollection = null;
@@ -622,7 +641,10 @@ export function createWeatherLayer({
viewer?.scene?.globe?.ellipsoid,
);
const camera = point
? cesium.Cartographic.fromCartesian(point, viewer?.scene?.globe?.ellipsoid)
? cesium.Cartographic.fromCartesian(
point,
viewer?.scene?.globe?.ellipsoid,
)
: null;
const lon = camera ? cesium.Math.toDegrees(camera.longitude) : 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 products = ['Wind', 'Satellite', 'Radar', 'Lightning'];
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 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,
createSurface = createShellSurface,
requestRender = () => viewer.scene.requestRender(),
viewRectangle = () => viewer.camera?.computeViewRectangle?.(viewer.scene.globe?.ellipsoid),
viewRectangle = () =>
viewer.camera?.computeViewRectangle?.(viewer.scene.globe?.ellipsoid),
moveEnd = viewer?.camera?.moveEnd,
}) {
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. */
function computeView() {
const rectangle = detailed
? viewRectangle()
: null;
const rectangle = detailed ? viewRectangle() : null;
const degrees = cesium.Math.toDegrees;
const footprint = rectangle && {
west: degrees(rectangle.west),
+21 -13
View File
@@ -85,7 +85,10 @@ export function createWindLayer({
const sample = (position) => {
if (!position && engine && !viewer) {
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, {
position,
@@ -122,20 +125,25 @@ export function createWindLayer({
attachRenderer(nextViewer, { engine: nextEngine = null } = {}) {
viewer = nextViewer;
engine = nextEngine;
rendering = engine?.kind === 'navara' ? createNativeRendering(engine) : createRendering({
cesium,
container: container ?? nextViewer.container,
getViewer: () => viewer,
getHost: () =>
imageryHost?.() ?? {
collection: viewer.imageryLayers ?? viewer.scene?.imageryLayers,
kind: 'globe',
},
onStatusChange: notify,
});
rendering =
engine?.kind === 'navara'
? createNativeRendering(engine)
: createRendering({
cesium,
container: container ?? nextViewer.container,
getViewer: () => viewer,
getHost: () =>
imageryHost?.() ?? {
collection:
viewer.imageryLayers ?? viewer.scene?.imageryLayers,
kind: 'globe',
},
onStatusChange: notify,
});
rendering.attach();
rendering.setOptions?.({ overlay, paused });
const target = container ?? nextViewer?.container ?? engine?.host.container;
const target =
container ?? nextViewer?.container ?? engine?.host.container;
inspectionMarker = createWindInspectionMarker({
container: target,
viewer,
+10 -6
View File
@@ -44,10 +44,12 @@ export function createWindInspectionMarker({
)
return;
present({ owner, container, visible: false, left: 0, top: 0 });
const occluder = !engine && new cesium.EllipsoidalOccluder(
scene.globe?.ellipsoid || cesium.Ellipsoid.WGS84,
viewer.camera.positionWC,
);
const occluder =
!engine &&
new cesium.EllipsoidalOccluder(
scene.globe?.ellipsoid || cesium.Ellipsoid.WGS84,
viewer.camera.positionWC,
);
const render = () => {
if (!shown()) return;
if (occluder) occluder.cameraPosition = viewer.camera.positionWC;
@@ -60,8 +62,10 @@ export function createWindInspectionMarker({
point.y >= 0 &&
point.x <= canvas.clientWidth &&
point.y <= canvas.clientHeight &&
(engine ? engine.projector.horizonVisible(position) :
scene.mode !== cesium.SceneMode.SCENE3D || occluder.isPointVisible(position));
(engine
? engine.projector.horizonVisible(position)
: scene.mode !== cesium.SceneMode.SCENE3D ||
occluder.isPointVisible(position));
if (!visible) {
// A hidden marker keeps its last place, as the element did.
if (marker.visible) present({ visible: false });
+44 -35
View File
@@ -30,10 +30,7 @@ export function createPackPresentations(
let key,
cards = [];
function present() {
store.publish(
owner,
cards.length ? { key, container, cards } : null,
);
store.publish(owner, cards.length ? { key, container, cards } : null);
}
function addCard(pack) {
if (!cards.length) key = ++panelKeys;
@@ -205,27 +202,35 @@ export function createPackPresentations(
signal.throwIfAborted();
if (image.naturalWidth * image.naturalHeight > 16 * 1024 * 1024)
throw new Error('Image dimensions exceed limit');
handle.display(native => native ? native.image(pack,image,handle) : handle.primitive(
new Cesium.Primitive({
asynchronous: false,
geometryInstances: new Cesium.GeometryInstance({
id: { packId: pack.id },
geometry: new Cesium.RectangleGeometry({
rectangle: Cesium.Rectangle.fromDegrees(...pack.placement.bounds),
height: pack.placement.height,
vertexFormat:
Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat,
}),
}),
appearance: new Cesium.MaterialAppearance({
material: Cesium.Material.fromType('Image', { image }),
materialSupport: Cesium.MaterialAppearance.MaterialSupport.TEXTURED,
flat: true,
faceForward: true,
translucent: true,
}),
}),
));
handle.display((native) =>
native
? native.image(pack, image, handle)
: handle.primitive(
new Cesium.Primitive({
asynchronous: false,
geometryInstances: new Cesium.GeometryInstance({
id: { packId: pack.id },
geometry: new Cesium.RectangleGeometry({
rectangle: Cesium.Rectangle.fromDegrees(
...pack.placement.bounds,
),
height: pack.placement.height,
vertexFormat:
Cesium.MaterialAppearance.MaterialSupport.TEXTURED
.vertexFormat,
}),
}),
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) => {
if (
@@ -243,16 +248,20 @@ export function createPackPresentations(
(item) => item.id === pack.placement.anchorId,
);
if (!anchor) throw new Error('Missing media anchor');
handle.display(native => native ? native.media(pack,anchor,handle) : handle.add({
name: pack.id,
position: xyz([anchor.lon, anchor.lat, anchor.alt]),
point: { pixelSize: 16, color: Cesium.Color.YELLOW },
label: {
text: pack.id,
font: '14px sans-serif',
pixelOffset: new Cesium.Cartesian2(0, -24),
},
}));
handle.display((native) =>
native
? native.media(pack, anchor, handle)
: handle.add({
name: pack.id,
position: xyz([anchor.lon, anchor.lat, anchor.alt]),
point: { pixelSize: 16, color: Cesium.Color.YELLOW },
label: {
text: pack.id,
font: '14px sans-serif',
pixelOffset: new Cesium.Cartesian2(0, -24),
},
}),
);
handle.media({
kind: asset.mimeType.startsWith('video/') ? 'video' : 'audio',
src: handle.url(new Blob([asset.bytes], { type: asset.mimeType })),
+7 -3
View File
@@ -86,9 +86,13 @@ export class ContextControls {
}
}
canRetainScene() {
return ['flights', 'space-missions'].includes(this._contextMode) && !this._contextModeChanging &&
!this._contextRestoreState && !this._clearSelectedLayersPromise &&
!this._contextLayerReactionPromises.size;
return (
['flights', 'space-missions'].includes(this._contextMode) &&
!this._contextModeChanging &&
!this._contextRestoreState &&
!this._clearSelectedLayersPromise &&
!this._contextLayerReactionPromises.size
);
}
captureScene() {
return { mode: this._contextMode, snapshot: this._contextSessionSnapshot };
+16 -5
View File
@@ -176,7 +176,8 @@ export class ShareRestoration {
this._initialShareRestoreTimeout = null;
if (this._disposed) return;
const generation = this._initialShareNavigationGeneration;
const retained = engineSwitch && this._layerStateCoordinator?.retainedTrackingTarget();
const retained =
engineSwitch && this._layerStateCoordinator?.retainedTrackingTarget();
const applyCamera =
Number.isInteger(generation) &&
this.navigation._reassertNavigationHandoff(generation, {
@@ -246,7 +247,15 @@ export class ShareRestoration {
*/
async _runShareRestore(
state,
{ applyCamera, instant = false, retained, retainScene = false, navigationToken, layersPromise, settle },
{
applyCamera,
instant = false,
retained,
retainScene = false,
navigationToken,
layersPromise,
settle,
},
) {
const generation = this._shareRestoreGeneration;
const current = () =>
@@ -259,11 +268,13 @@ export class ShareRestoration {
});
const layers = await (layersPromise || Promise.resolve([]));
if (!current()) return;
if (retainScene && !retained) retained = this._layerStateCoordinator?.retainedTrackingTarget();
if (retainScene && !retained)
retained = this._layerStateCoordinator?.retainedTrackingTarget();
const tracking =
share.camera === 'applied'
? (retained && this._layerStateCoordinator.resumeRetainedTracking(retained)) ||
await this._layerStateCoordinator?.restoreShareTrackingSelection?.()
? (retained &&
this._layerStateCoordinator.resumeRetainedTracking(retained)) ||
(await this._layerStateCoordinator?.restoreShareTrackingSelection?.())
: {
status: 'superseded',
cleared: