mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 12:23:59 +00:00
Format the files this branch wrote or broke with Prettier
This commit is contained in:
1 parent
227e3c2051
commit
8d7f055572
90 files changed
+1408
-640
No files matched your search
@@ -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
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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' ||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Vendored
+2
-1
@@ -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;
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
@@ -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?.();
|
||||
|
||||
@@ -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). */
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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} */
|
||||
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user