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