mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 12:23:59 +00:00
Keep traffic vehicles and road flow across globe switches
This commit is contained in:
1 parent
4e09559337
commit
75df3389f3
20 files changed
+586
-109
No files matched your search
@@ -3679,6 +3679,7 @@
|
||||
"src/layers/shared/render/navaraStems.js",
|
||||
"src/layers/submarineCables/render/navara.js",
|
||||
"src/layers/directions/render/navara.js",
|
||||
"src/layers/traffic/render/navara.js",
|
||||
"src/layers/shared/render/navaraStory.js",
|
||||
"src/layers/bhoteKoshi/render/navara.js",
|
||||
"src/layers/weather/render/navara.js",
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { trafficFixtureResponse } from './traffic-fixtures.mjs';
|
||||
import puppeteer from 'puppeteer';
|
||||
import sharp from 'sharp';
|
||||
import assert from 'node:assert/strict';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
|
||||
const base = new URL(process.argv[2]);
|
||||
if (
|
||||
!['localhost', '127.0.0.1'].includes(base.hostname) ||
|
||||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||
base.port,
|
||||
)
|
||||
)
|
||||
throw new Error('Supply an unreserved local keyless preview URL');
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
base.hash =
|
||||
'v=2&lat=30.2670&lon=-97.7440&alt=1800&heading=0&pitch=-90&roll=0&map=sentinel2&l=';
|
||||
const b = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--use-angle=metal', '--enable-gpu'],
|
||||
});
|
||||
try {
|
||||
const p = await b.newPage();
|
||||
await blockPaidQaMaps(p);
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
errors.push(e.message);
|
||||
console.log('error', e.message.split('?')[0]);
|
||||
});
|
||||
|
||||
const keyless = process.argv.includes('--simulation');
|
||||
const reads = new Map();
|
||||
await p.setRequestInterception(true);
|
||||
p.on('request', async (req) => {
|
||||
const u = new URL(req.url());
|
||||
if (
|
||||
u.hostname === 'tiles.openfreemap.org' ||
|
||||
u.pathname.startsWith('/api/tomtom/')
|
||||
) {
|
||||
reads.set(u.href, (reads.get(u.href) || 0) + 1);
|
||||
if (keyless && u.pathname === '/api/tomtom/status')
|
||||
return req.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: '{"hasKey":false}',
|
||||
});
|
||||
const response = trafficFixtureResponse(req);
|
||||
if (response) return req.respond(response);
|
||||
throw Error('unrecognized traffic URL: ' + u.pathname);
|
||||
}
|
||||
return req.continue();
|
||||
});
|
||||
p.on('console', (msg) => {
|
||||
if (msg.type() === 'error' && /Shader|WebGLProgram/i.test(msg.text()))
|
||||
errors.push(msg.text());
|
||||
});
|
||||
await p.goto(base.href);
|
||||
const ready = async (kind) => {
|
||||
await p.waitForFunction(
|
||||
(k) =>
|
||||
window.__godsEyeView?.engine.kind === k &&
|
||||
!document.querySelector('#globe-engine')?.disabled &&
|
||||
!document.querySelector('.engine-switch-frame') &&
|
||||
document.querySelector('#loading-screen.hidden'),
|
||||
{ timeout: 90000 },
|
||||
kind,
|
||||
);
|
||||
await p.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
};
|
||||
await ready('navara');
|
||||
await p.evaluate(async () => {
|
||||
const { tileset, dataManager } = window.__godsEyeView;
|
||||
if (tileset || window.__GOOGLE_MAPS_API_KEY__)
|
||||
throw Error('Keyless preview required');
|
||||
window.qaTraffic = dataManager.layers.get('traffic').module;
|
||||
window.qaTraffic.setParams({ jamViz: 'both' });
|
||||
await dataManager.setEnabled('traffic', true, { origin: 'user' });
|
||||
});
|
||||
const waitTraffic = async () => {
|
||||
try {
|
||||
await p.waitForFunction(() => window.qaTraffic.getStats().count >= 10, {
|
||||
timeout: 40000,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'no traffic',
|
||||
await p.evaluate(() => window.qaTraffic.getStats()),
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
await waitTraffic();
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
const ids = await p.evaluate(() =>
|
||||
window.qaTraffic.getDetectableObjects().map((p) => p.id),
|
||||
);
|
||||
const first = await p.evaluate(
|
||||
() => window.qaTraffic.getStats().motion.rebuilds,
|
||||
);
|
||||
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await p.select('#globe-engine', kind);
|
||||
await ready(kind);
|
||||
}
|
||||
await waitTraffic();
|
||||
const check = await p.evaluate(() => {
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const mod = dataManager.layers.get('traffic').module;
|
||||
return {
|
||||
same: mod === window.qaTraffic,
|
||||
...mod.getStats(),
|
||||
params: mod.getParams(),
|
||||
ids: mod.getDetectableObjects().map((p) => p.id),
|
||||
};
|
||||
});
|
||||
console.log(
|
||||
kind,
|
||||
check.mode,
|
||||
check.count,
|
||||
check.flowBuckets,
|
||||
check.heatLines,
|
||||
check.motion,
|
||||
);
|
||||
assert.equal(check.same, true);
|
||||
assert.equal(check.params.jamViz, 'both');
|
||||
assert.equal(check.error, null);
|
||||
assert.equal(
|
||||
check.motion.rebuilds,
|
||||
first,
|
||||
'traffic population was cleared',
|
||||
);
|
||||
assert.ok(
|
||||
check.ids.some((id) => ids.includes(id)),
|
||||
'original moving vehicles survived',
|
||||
);
|
||||
if (!keyless)
|
||||
assert.ok(check.heatLines > 0, 'congestion corridors are published');
|
||||
if (keyless) assert.equal(check.flowBuckets.sim, check.count);
|
||||
else assert.ok(check.flowBuckets.slow + check.flowBuckets.jam > 0);
|
||||
}
|
||||
if (keyless)
|
||||
assert.ok(![...reads.keys()].some((u) => u.includes('/tomtom/flow/')));
|
||||
const pixels = async () =>
|
||||
sharp(await p.screenshot())
|
||||
.extract({ left: 360, top: 255, width: 330, height: 260 })
|
||||
.removeAlpha()
|
||||
.raw()
|
||||
.toBuffer();
|
||||
// Source counts can be ready before the incoming renderer has drawn terrain.
|
||||
// Let the camera and the newly attached road heights settle before comparison.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2200));
|
||||
const shown = await pixels();
|
||||
await p.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.setEnabled('traffic', false, {
|
||||
origin: 'user',
|
||||
}),
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
const hidden = await pixels();
|
||||
let visible = 0;
|
||||
for (let i = 0; i < shown.length; i += 3) {
|
||||
if (
|
||||
shown[i] +
|
||||
shown[i + 1] +
|
||||
shown[i + 2] -
|
||||
(hidden[i] + hidden[i + 1] + hidden[i + 2]) >
|
||||
110
|
||||
)
|
||||
visible++;
|
||||
}
|
||||
console.log('changed traffic pixels', visible);
|
||||
assert.ok(visible > 15, 'changing the layer changes the rendered streets');
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(
|
||||
'PASS retained traffic population and flow, real geography and stable identities across both globes',
|
||||
);
|
||||
} finally {
|
||||
await b.close();
|
||||
}
|
||||
@@ -241,7 +241,9 @@ export function createApplicationCatalog({
|
||||
],
|
||||
[
|
||||
'traffic',
|
||||
() => createApplicationTraffic({ source: sources.traffic, surface }),
|
||||
() => sessionModel('traffic', () => createApplicationTraffic({
|
||||
source: sources.traffic, surface, nativeRenderer: engine => rendererFor('traffic',engine),
|
||||
})),
|
||||
],
|
||||
['cctv', () => createApplicationCctv({ surface, source: sources.cctv })],
|
||||
[
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ export function createApplicationData({
|
||||
throw new TypeError('An application layer catalog is required');
|
||||
for (const layer of catalog.layers)
|
||||
layer.attachMapStackController?.(mapStackController);
|
||||
const attached = dataManager.bindCatalog(viewer, engine, catalog);
|
||||
const attached = dataManager.bindCatalog(viewer, engine, catalog, { waitForCameraRestore: layerSession?.restoringView === true });
|
||||
for (const layer of catalog.layers) layer.attachDataManager?.(dataManager);
|
||||
|
||||
if (allowQaRegistration) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import * as ground from '../../data/groundFloor.js';
|
||||
import * as render from '../../renderGovernor.js';
|
||||
|
||||
/** Construct one layer using the application scene owners and a supplied source. */
|
||||
export function createApplicationTraffic({ source, surface }) {
|
||||
export function createApplicationTraffic({ source, surface, nativeRenderer }) {
|
||||
return createTrafficLayer({
|
||||
source,
|
||||
services: { credits, render, ground: surface?.groundFloor || ground },
|
||||
services: { credits, render, nativeRenderer, ground: surface?.groundFloor || ground },
|
||||
});
|
||||
}
|
||||
@@ -56,6 +56,7 @@ test('the Navara mode registers migrated renderers once', async () => {
|
||||
'alpr-cameras',
|
||||
'satellites',
|
||||
'rocket-launches',
|
||||
'traffic',
|
||||
'cctv',
|
||||
'radio',
|
||||
'transit',
|
||||
|
||||
@@ -126,6 +126,10 @@ export async function registerNavaraLayerRenderers() {
|
||||
const { drawStorySource } = await import('../layers/shared/render/navaraStory.js');
|
||||
return (engine, { data }) => drawStorySource(engine, navaraViews.get(engine), data);
|
||||
},
|
||||
traffic: async () => {
|
||||
const { createTrafficNavaraRenderer } = await import('../layers/traffic/render/navara.js');
|
||||
return engine => createTrafficNavaraRenderer(engine, navaraViews.get(engine));
|
||||
},
|
||||
directions: async () => {
|
||||
const { createDirectionsNavaraRenderer } = await import('../layers/directions/render/navara.js');
|
||||
return (engine) => createDirectionsNavaraRenderer(engine, navaraViews.get(engine));
|
||||
|
||||
@@ -142,7 +142,7 @@ export class LayerLifecycle {
|
||||
}
|
||||
|
||||
/** Bind the next catalog only after the preceding scene released its entries. */
|
||||
bindCatalog(viewer, engine, catalog) {
|
||||
bindCatalog(viewer, engine, catalog, options = {}) {
|
||||
if (this._registrationsFinalized) throw new Error('Scene already attached');
|
||||
this.viewer = viewer;
|
||||
this.engine = engine ?? engineFromViewer(viewer) ?? null;
|
||||
@@ -154,7 +154,7 @@ export class LayerLifecycle {
|
||||
else {
|
||||
if (entry.module !== layer) throw new Error(`Layer owner changed: ${layer.id}`);
|
||||
if (entry.initialized) {
|
||||
const ready = layer.attachRenderer(viewer, this._moduleContext());
|
||||
const ready = layer.attachRenderer(viewer, { ...this._moduleContext(), ...options });
|
||||
if (ready?.then) attachments.push(ready);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ export function createAnimation({
|
||||
if (needed) {
|
||||
layerState._lastAnimTime = 0;
|
||||
services.render.holdContinuousRender('traffic');
|
||||
layerState._viewer?.scene?.requestRender?.();
|
||||
if (layerState._engine) layerState._engine.render.request('traffic');
|
||||
else layerState._viewer?.scene?.requestRender?.();
|
||||
} else services.render.releaseContinuousRender('traffic');
|
||||
}
|
||||
|
||||
@@ -318,6 +319,9 @@ export function createAnimation({
|
||||
|
||||
// Jam heat-lines throb (~1.6 s period) — one shared material uniform for
|
||||
// the whole jam batch, no geometry rebuild. Null outside live heatline mode.
|
||||
if (layerState._native && layerState._heatJamPrim) {
|
||||
layerState._heatJamPrim.setAlpha(HEAT_JAM_BASE_ALPHA + HEAT_JAM_PULSE_ALPHA * Math.sin(now / 260));
|
||||
}
|
||||
if (layerState._heatJamPrim?.appearance) {
|
||||
layerState._heatJamPrim.appearance.material.uniforms.color.alpha =
|
||||
HEAT_JAM_BASE_ALPHA + HEAT_JAM_PULSE_ALPHA * Math.sin(now / 260);
|
||||
@@ -349,7 +353,7 @@ export function createAnimation({
|
||||
/** Remove all point primitives and reset dot/road arrays and counters. */
|
||||
|
||||
function clearDots() {
|
||||
services.credits?.hideOsmCredit?.(layerState._viewer, 'traffic');
|
||||
services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic');
|
||||
parts.retention.clear();
|
||||
if (layerState._dots.length) layerState._motion.rebuilds++;
|
||||
if (layerState._pointCollection) layerState._pointCollection.removeAll();
|
||||
|
||||
+14
-10
@@ -70,7 +70,7 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
layerState._flowStatusKnown = true;
|
||||
if (layerState._liveMode) {
|
||||
console.log('[Data:Traffic] TomTom key present — live flow mode');
|
||||
registerDynamicCredit(layerState._viewer, TOMTOM_CREDIT);
|
||||
registerDynamicCredit(layerState._engine ?? layerState._viewer, TOMTOM_CREDIT);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -217,6 +217,10 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
) {
|
||||
if (generation !== layerState._loadGeneration || signal?.aborted)
|
||||
return false;
|
||||
if (layerState._waitForScene) {
|
||||
await layerState._waitForScene;
|
||||
if (generation !== layerState._loadGeneration || signal?.aborted) return false;
|
||||
}
|
||||
const revision = (layerState._surfaceRevision || 0) + 1;
|
||||
layerState._surfaceRevision = revision;
|
||||
const current = () =>
|
||||
@@ -237,6 +241,8 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
layerState._retainedPreview = true;
|
||||
return true;
|
||||
}
|
||||
if (roads.length && !majorPreview && !label.includes('refined'))
|
||||
layerState._surfaceInput = { roads, bounds: clamped };
|
||||
const preparedRoads =
|
||||
layerState._preparedRoads || (layerState._preparedRoads = new Map());
|
||||
const refining = label.includes('refined');
|
||||
@@ -253,7 +259,8 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
parts.rendering.renderRoadsForAltitude([], altitude, label, trace);
|
||||
return true;
|
||||
}
|
||||
const scene = layerState._viewer.scene;
|
||||
const scene = layerState._trafficScene?.() ?? layerState._viewer?.scene;
|
||||
if (!scene) return false;
|
||||
// The enable-time estimate can predate the first local mesh (especially
|
||||
// after an intercontinental jump). Once previews have measured roads, use
|
||||
// that local evidence to select the detail view at its real elevation.
|
||||
@@ -325,7 +332,7 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
const selected = parts.rendering
|
||||
.visibleRoadsForAltitude(roads, altitude)
|
||||
.filter((road) => {
|
||||
const camera = layerState._viewer.camera?.positionWC;
|
||||
const camera = (layerState._engine?.camera.livePosition ?? layerState._viewer?.camera?.positionWC);
|
||||
const localHeight = Math.max(
|
||||
0,
|
||||
altitude - (layerState._viewHeightEstimate || 0),
|
||||
@@ -349,10 +356,7 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
for (const point of road.waypoints) {
|
||||
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||
scene,
|
||||
point,
|
||||
);
|
||||
const screen = scene.project ? scene.project(point, {}) : Cesium.SceneTransforms.worldToWindowCoordinates(scene, point);
|
||||
if (!screen) continue;
|
||||
minX = Math.min(minX, screen.x);
|
||||
maxX = Math.max(maxX, screen.x);
|
||||
@@ -382,7 +386,7 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
roadDotBudget(altitude),
|
||||
);
|
||||
const admitted = selected.filter((r, i) => budgets[i] > 0);
|
||||
const camera = layerState._viewer.camera?.positionWC;
|
||||
const camera = (layerState._engine?.camera.livePosition ?? layerState._viewer?.camera?.positionWC);
|
||||
if (camera)
|
||||
admitted.sort(
|
||||
(a, b) =>
|
||||
@@ -478,7 +482,7 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
};
|
||||
let prepared = await prepareRoadSurfaces(
|
||||
unresolved,
|
||||
layerState._viewer.scene,
|
||||
scene,
|
||||
services.ground,
|
||||
[layerState._pointCollection],
|
||||
signal,
|
||||
@@ -500,7 +504,7 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
if (!current()) return false;
|
||||
prepared = await prepareRoadSurfaces(
|
||||
prepared.pending,
|
||||
layerState._viewer.scene,
|
||||
scene,
|
||||
services.ground,
|
||||
[layerState._pointCollection],
|
||||
signal,
|
||||
|
||||
@@ -69,16 +69,18 @@ export function createReticleFootprint() {
|
||||
const hits = Array.from({ length: 25 }, () => geo.cartesian3.create());
|
||||
const carto = geo.cartographic.create();
|
||||
const ray = new Cesium.Ray();
|
||||
return (viewer, altitude) => {
|
||||
const { camera, scene } = viewer;
|
||||
if (!camera.getPickRay) return null;
|
||||
const nadir = camera.positionCartographic;
|
||||
return (viewer, altitude, engine) => {
|
||||
const camera = engine?.camera ?? viewer?.camera;
|
||||
if (!camera?.getPickRay && !camera?.pickRay) return null;
|
||||
const pose = engine?.camera.pose();
|
||||
const nadir = pose?.cartographic ?? camera.positionCartographic;
|
||||
const canvas = engine?.host.canvas ?? viewer.scene.canvas;
|
||||
const near = {
|
||||
lat: geo.math.toDegrees(nadir.latitude),
|
||||
lon: geo.math.toDegrees(nadir.longitude),
|
||||
};
|
||||
const width = scene.canvas.clientWidth || scene.canvas.width,
|
||||
height = scene.canvas.clientHeight || scene.canvas.height;
|
||||
const width = canvas.clientWidth || canvas.width,
|
||||
height = canvas.clientHeight || canvas.height;
|
||||
const side = Math.min(width, height),
|
||||
left = (width - side) / 2,
|
||||
top = (height - side) / 2;
|
||||
@@ -98,8 +100,9 @@ export function createReticleFootprint() {
|
||||
dlon -= 360 * Math.round(dlon / 360);
|
||||
dx = dlon * 111.195 * Math.cos(nadir.latitude);
|
||||
} else {
|
||||
camera.getPickRay(p, ray);
|
||||
const d = ray.direction;
|
||||
const picked = engine ? camera.pickRay(p) : camera.getPickRay(p, ray) || ray;
|
||||
if (!picked) continue;
|
||||
const d = picked.direction;
|
||||
dx =
|
||||
-Math.sin(nadir.longitude) * d.x + Math.cos(nadir.longitude) * d.y;
|
||||
dy =
|
||||
@@ -155,8 +158,8 @@ export function createReticleFootprint() {
|
||||
nadir.latitude,
|
||||
nadir.longitude,
|
||||
nadir.height / 10000,
|
||||
camera.heading,
|
||||
camera.pitch,
|
||||
pose?.heading ?? camera.heading,
|
||||
pose?.pitch ?? camera.pitch,
|
||||
]
|
||||
.map((v) => Number(v || 0).toFixed(7))
|
||||
.join(','),
|
||||
|
||||
@@ -270,12 +270,12 @@ export function createIngestion({
|
||||
layerState._viewHeightEstimate = 0;
|
||||
const centerLat = (clamped.south + clamped.north) / 2;
|
||||
const centerLon = (clamped.west + clamped.east) / 2;
|
||||
const scene = layerState._viewer?.scene;
|
||||
const scene = layerState._trafficScene?.() ?? layerState._viewer?.scene;
|
||||
const location = geo.cartographic.fromDegrees(centerLon, centerLat);
|
||||
let estimated = services.ground?.cachedGroundFloor?.(centerLat, centerLon);
|
||||
if (!Number.isFinite(estimated)) {
|
||||
try {
|
||||
estimated = scene?.globe?.show
|
||||
estimated = scene?.engine ? scene.sampleHeight(location) : scene?.globe?.show
|
||||
? scene.globe.getHeight?.(location)
|
||||
: scene?.sampleHeightSupported
|
||||
? scene.sampleHeight(location, [layerState._pointCollection])
|
||||
|
||||
+123
-59
@@ -14,6 +14,51 @@ export function createLifecycle({
|
||||
}) {
|
||||
const { resetFlowTileCache } = source;
|
||||
|
||||
function readyForSurfaceWork() {
|
||||
const resolve = layerState._sceneReady;
|
||||
layerState._sceneReady = layerState._waitForScene = null;
|
||||
resolve?.();
|
||||
}
|
||||
|
||||
function bindScene() {
|
||||
const viewer = layerState._viewer, engine = layerState._engine;
|
||||
const camera = engine?.camera ?? viewer.camera;
|
||||
const frames = engine?.frames ?? viewer.scene;
|
||||
if (viewer) layerState._surfaceFrameRemover = observeTrafficSurface(viewer.scene);
|
||||
layerState._pointCollection.show = true;
|
||||
layerState._lastAnimTime = 0;
|
||||
parts.animation.syncRenderDemand();
|
||||
layerState._preRenderRemover = frames.preRender.addEventListener(() => {
|
||||
parts.animation.animate();
|
||||
layerState._native?.points.flush();
|
||||
});
|
||||
if (TRAFFIC_TIMING_ENABLED) {
|
||||
parts.timing.clearTrafficTimingEntries();
|
||||
layerState._trafficTimingCurrentAnchor = null;
|
||||
layerState._trafficTimingPostRenderRemovers = new Set();
|
||||
layerState._trafficTimingMoveEndRemover = camera.moveEnd.addEventListener(parts.timing.markTrafficTimingMoveEnd);
|
||||
}
|
||||
layerState._cameraRemover = camera.changed.addEventListener(parts.viewport.onCameraChanged);
|
||||
layerState._arrivalRemover = camera.moveEnd.addEventListener(() => parts.viewport.onCameraChanged({immediate:true}));
|
||||
claimCameraSensitivity(engine ?? camera,'traffic',0.05);
|
||||
}
|
||||
|
||||
function unbindScene() {
|
||||
const camera = layerState._engine?.camera ?? layerState._viewer?.camera;
|
||||
layerState._surfaceFrameRemover?.();
|
||||
layerState._preRenderRemover?.();
|
||||
layerState._trafficTimingMoveEndRemover?.();
|
||||
for (const remove of layerState._trafficTimingPostRenderRemovers || []) remove();
|
||||
layerState._trafficTimingPostRenderRemovers = null;
|
||||
layerState._trafficTimingCurrentAnchor = null;
|
||||
layerState._cameraRemover?.();
|
||||
// Some legacy camera events do not return a remover.
|
||||
if (!layerState._cameraRemover) camera?.changed?.removeEventListener?.(parts.viewport.onCameraChanged);
|
||||
layerState._arrivalRemover?.();
|
||||
layerState._surfaceFrameRemover = layerState._preRenderRemover = layerState._cameraRemover = layerState._arrivalRemover = layerState._trafficTimingMoveEndRemover = null;
|
||||
releaseCameraSensitivity(layerState._engine ?? camera,'traffic');
|
||||
}
|
||||
|
||||
const methods = {
|
||||
/**
|
||||
* One-time initialisation. Creates the PointPrimitiveCollection and adds it
|
||||
@@ -22,14 +67,8 @@ export function createLifecycle({
|
||||
*
|
||||
* @param {Cesium.Viewer} viewer - The Cesium viewer instance.
|
||||
*/
|
||||
init(viewer) {
|
||||
layerState._viewer = viewer;
|
||||
layerState._pointCollection = new Cesium.PointPrimitiveCollection({
|
||||
blendOption: Cesium.BlendOption.TRANSLUCENT,
|
||||
});
|
||||
// Add permanently — toggle with .show to avoid destroy-on-remove errors
|
||||
viewer.scene.primitives.add(layerState._pointCollection);
|
||||
layerState._pointCollection.show = false;
|
||||
init(viewer, options) {
|
||||
this.attachRenderer(viewer, options);
|
||||
layerState._dots = [];
|
||||
layerState._roads = [];
|
||||
layerState._count = 0;
|
||||
@@ -60,9 +99,8 @@ export function createLifecycle({
|
||||
layerState._stylePreset =
|
||||
document?.documentElement?.dataset?.gevStyle || 'normal';
|
||||
if (!layerState._styleListenerBound) {
|
||||
window.addEventListener('gev:style-change', (e) =>
|
||||
parts.style.setStylePreset(e?.detail?.style),
|
||||
);
|
||||
layerState._styleListener = (e) => parts.style.setStylePreset(e?.detail?.style);
|
||||
window.addEventListener('gev:style-change', layerState._styleListener);
|
||||
layerState._styleListenerBound = true;
|
||||
}
|
||||
}
|
||||
@@ -70,6 +108,66 @@ export function createLifecycle({
|
||||
console.log('[Data:Traffic] Initialized');
|
||||
},
|
||||
|
||||
attachRenderer(viewer, {engine=null,waitForCameraRestore=false}={}) {
|
||||
layerState._restoringViewport = waitForCameraRestore;
|
||||
layerState._viewer = viewer;
|
||||
layerState._engine = engine;
|
||||
layerState._native = !viewer && engine ? services.nativeRenderer(engine) : null;
|
||||
layerState._heatSupported = null;
|
||||
const points = layerState._native?.points ?? new Cesium.PointPrimitiveCollection({blendOption: Cesium.BlendOption.TRANSLUCENT});
|
||||
if (viewer) viewer.scene.primitives.add(points);
|
||||
layerState._pointCollection = points;
|
||||
for(const dot of layerState._dots) dot.point = points.add(dot.point);
|
||||
points.show = layerState._enabled;
|
||||
if (!waitForCameraRestore) readyForSurfaceWork();
|
||||
if (layerState._enabled) {
|
||||
bindScene();
|
||||
parts.rendering.rebuildHeatLines(layerState._roads);
|
||||
if (layerState._dots.length && layerState._roads.some(r=>!r.directFlow))
|
||||
services.credits?.showOsmCredit?.(engine ?? viewer,'traffic',{openMapTiles:true});
|
||||
if (layerState._liveMode) services.credits?.registerDynamicCredit?.(engine ?? viewer, services.credits.TOMTOM_CREDIT);
|
||||
parts.viewport.onCameraChanged({immediate:true});
|
||||
}
|
||||
},
|
||||
resumeViewport() {
|
||||
if (!layerState._restoringViewport) return;
|
||||
layerState._restoringViewport = false;
|
||||
readyForSurfaceWork();
|
||||
if (!layerState._enabled) return;
|
||||
if (layerState._populationPending) parts.retention.resume();
|
||||
const previous = layerState._surfaceInput;
|
||||
if (previous && !layerState._fetching && layerState._lastBounds === previous.bounds) {
|
||||
void parts.flow.applyFlowThenRender(previous.roads, previous.bounds, layerState._loadGeneration,
|
||||
parts.viewport.getCameraAltitude(), 'Handoff refined').catch(error => {
|
||||
if (error?.name !== 'AbortError') console.warn('[Data:Traffic] Surface refresh failed', error);
|
||||
});
|
||||
}
|
||||
parts.viewport.onCameraChanged({immediate:true});
|
||||
},
|
||||
detachRenderer() {
|
||||
if(!layerState._pointCollection)return;
|
||||
unbindScene();
|
||||
clearTimeout(layerState._fetchTimeout);
|
||||
layerState._surfaceRefineRemove?.();
|
||||
layerState._surfaceRefineRemove = null;
|
||||
layerState._surfaceRevision = (layerState._surfaceRevision || 0)+1;
|
||||
layerState._preparedRoads?.clear();
|
||||
parts.rendering.removeHeatLines();
|
||||
services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer,'traffic');
|
||||
for(const dot of layerState._dots) {
|
||||
const p=dot.point;
|
||||
dot.point={position:Cesium.Cartesian3.clone(p.position),pixelSize:p.pixelSize,color:p.color,show:p.show,
|
||||
outlineColor:p.outlineColor,outlineWidth:p.outlineWidth,scaleByDistance:p.scaleByDistance,
|
||||
translucencyByDistance:p.translucencyByDistance,disableDepthTestDistance:0};
|
||||
}
|
||||
layerState._viewer?.scene.primitives.remove(layerState._pointCollection);
|
||||
layerState._native?.destroy();
|
||||
if(layerState._renderHeld)services.render.releaseContinuousRender('traffic');
|
||||
layerState._renderHeld=false;
|
||||
layerState._pointCollection=layerState._viewer=layerState._engine=layerState._native=null;
|
||||
layerState._waitForScene ??= new Promise(resolve=>{layerState._sceneReady=resolve;});
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable the traffic layer. Shows the point collection, subscribes to the
|
||||
* preRender animation loop and camera-change events, and kicks off an
|
||||
@@ -79,35 +177,10 @@ export function createLifecycle({
|
||||
*/
|
||||
enable(viewer) {
|
||||
if (layerState._enabled) return;
|
||||
if (viewer) layerState._viewer = viewer;
|
||||
layerState._enabled = true;
|
||||
layerState._surfaceFrameRemover = observeTrafficSurface(viewer.scene);
|
||||
if (layerState._roadMode !== 'tomtom') source.prefetch?.();
|
||||
parts.animation.syncRenderDemand();
|
||||
layerState._lastAnimTime = 0;
|
||||
layerState._pointCollection.show = true;
|
||||
|
||||
layerState._preRenderRemover = viewer.scene.preRender.addEventListener(
|
||||
parts.animation.animate,
|
||||
);
|
||||
if (TRAFFIC_TIMING_ENABLED) {
|
||||
parts.timing.clearTrafficTimingEntries();
|
||||
layerState._trafficTimingCurrentAnchor = null;
|
||||
layerState._trafficTimingPostRenderRemovers = new Set();
|
||||
layerState._trafficTimingMoveEndRemover =
|
||||
viewer.camera.moveEnd.addEventListener(
|
||||
parts.timing.markTrafficTimingMoveEnd,
|
||||
);
|
||||
}
|
||||
|
||||
// Share the 5% movement threshold with other camera-driven layers.
|
||||
viewer.camera.changed.addEventListener(parts.viewport.onCameraChanged);
|
||||
// Always inspect the final view, even when the last flight step is below
|
||||
// camera.changed's movement threshold.
|
||||
layerState._arrivalRemover = viewer.camera.moveEnd.addEventListener(() =>
|
||||
parts.viewport.onCameraChanged({ immediate: true }),
|
||||
);
|
||||
claimCameraSensitivity(viewer.camera, 'traffic', 0.05);
|
||||
|
||||
bindScene();
|
||||
// Enabling is explicit intent, not a camera gesture that needs settling.
|
||||
parts.viewport.onCameraChanged({ immediate: true });
|
||||
|
||||
@@ -142,7 +215,8 @@ export function createLifecycle({
|
||||
*/
|
||||
disable(viewer) {
|
||||
layerState._enabled = false;
|
||||
services.credits?.hideOsmCredit?.(layerState._viewer, 'traffic');
|
||||
layerState._surfaceInput = null;
|
||||
services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic');
|
||||
parts.animation.syncRenderDemand();
|
||||
clearTimeout(layerState._fetchTimeout);
|
||||
clearInterval(layerState._enableKickTimer);
|
||||
@@ -164,25 +238,7 @@ export function createLifecycle({
|
||||
layerState._flowStale = false;
|
||||
layerState._flowOldestTileAt = null;
|
||||
|
||||
layerState._surfaceFrameRemover?.();
|
||||
layerState._surfaceFrameRemover = null;
|
||||
if (layerState._preRenderRemover) {
|
||||
layerState._preRenderRemover();
|
||||
layerState._preRenderRemover = null;
|
||||
}
|
||||
if (TRAFFIC_TIMING_ENABLED) {
|
||||
layerState._trafficTimingMoveEndRemover?.();
|
||||
layerState._trafficTimingMoveEndRemover = null;
|
||||
for (const remove of layerState._trafficTimingPostRenderRemovers || [])
|
||||
remove();
|
||||
layerState._trafficTimingPostRenderRemovers = null;
|
||||
layerState._trafficTimingCurrentAnchor = null;
|
||||
}
|
||||
|
||||
viewer.camera.changed.removeEventListener(parts.viewport.onCameraChanged);
|
||||
layerState._arrivalRemover?.();
|
||||
layerState._arrivalRemover = null;
|
||||
releaseCameraSensitivity(viewer.camera, 'traffic');
|
||||
unbindScene();
|
||||
if (layerState._pointCollection) layerState._pointCollection.show = false;
|
||||
},
|
||||
|
||||
@@ -195,11 +251,19 @@ export function createLifecycle({
|
||||
destroy(viewer) {
|
||||
this.disable(viewer);
|
||||
if (layerState._pointCollection) {
|
||||
viewer.scene.primitives.remove(layerState._pointCollection);
|
||||
layerState._viewer?.scene.primitives.remove(layerState._pointCollection);
|
||||
layerState._native?.destroy();
|
||||
layerState._native = null;
|
||||
layerState._pointCollection = null;
|
||||
}
|
||||
parts.rendering.removeHeatLines();
|
||||
layerState._tileCache.clear();
|
||||
if (layerState._styleListenerBound && typeof window !== 'undefined') {
|
||||
window.removeEventListener('gev:style-change', layerState._styleListener);
|
||||
layerState._styleListenerBound = false;
|
||||
}
|
||||
layerState._sceneReady?.();
|
||||
layerState._waitForScene = layerState._sceneReady = null;
|
||||
resetFlowTileCache();
|
||||
layerState._count = 0;
|
||||
layerState._lastUpdate = null;
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import * as geo from '../../../geo/index.js';
|
||||
import { MAX_DOTS } from '../policy.js';
|
||||
|
||||
// Point handles keep the same mutable fields the shared traffic motion loop uses.
|
||||
// The GPU gets two packed draws, including the preset's optional colored outlines.
|
||||
function createTrafficPoints(engine) {
|
||||
const points = new Set(),
|
||||
renderSets = [];
|
||||
for (const [role, order] of [
|
||||
['outline', 0],
|
||||
['fill', 1],
|
||||
]) {
|
||||
renderSets.push(
|
||||
engine.movers.createSet({
|
||||
id: 'traffic-' + role,
|
||||
kind: 'point',
|
||||
order,
|
||||
capacity: MAX_DOTS,
|
||||
depthTest: 'normal',
|
||||
pickable: false,
|
||||
point: { sizePx: 1, outlinePx: 0 },
|
||||
}),
|
||||
);
|
||||
}
|
||||
const color = [0, 0, 0, 0];
|
||||
let empty = true;
|
||||
const range = (p, d) => {
|
||||
if (!p) return 1;
|
||||
// The engine's distance scalar interpolates in squared range.
|
||||
const x = Math.min(
|
||||
1,
|
||||
Math.max(
|
||||
0,
|
||||
(d * d - p.near * p.near) / (p.far * p.far - p.near * p.near),
|
||||
),
|
||||
);
|
||||
return p.nearValue + (p.farValue - p.nearValue) * Math.pow(x, 0.2);
|
||||
};
|
||||
return {
|
||||
show: true,
|
||||
add(spec) {
|
||||
const pos = geo.cartesian3.clone(spec.position);
|
||||
const p = {
|
||||
...spec,
|
||||
show: spec.show ?? true,
|
||||
get position() {
|
||||
return pos;
|
||||
},
|
||||
set position(v) {
|
||||
geo.cartesian3.clone(v, pos);
|
||||
},
|
||||
};
|
||||
points.add(p);
|
||||
return p;
|
||||
},
|
||||
remove(point) {
|
||||
return points.delete(point);
|
||||
},
|
||||
removeAll() {
|
||||
points.clear();
|
||||
this.flush();
|
||||
},
|
||||
flush() {
|
||||
if (!points.size && empty) return;
|
||||
empty = points.size === 0;
|
||||
const center = engine.camera.livePosition;
|
||||
const sizes = [0, 0];
|
||||
if (this.show && center)
|
||||
for (const p of points) {
|
||||
if (
|
||||
!p.show ||
|
||||
p.pixelSize <= 0 ||
|
||||
!engine.projector.horizonVisible(p.position)
|
||||
)
|
||||
continue;
|
||||
const distance = geo.cartesian3.distance(p.position, center),
|
||||
scale = range(p.scaleByDistance, distance),
|
||||
alpha = range(p.translucencyByDistance, distance);
|
||||
if (alpha <= 0) continue;
|
||||
for (
|
||||
let i = p.outlineWidth > 0 && p.outlineColor ? 0 : 1;
|
||||
i < 2;
|
||||
i++
|
||||
) {
|
||||
const c = i ? p.color : p.outlineColor;
|
||||
color[0] = c.red;
|
||||
color[1] = c.green;
|
||||
color[2] = c.blue;
|
||||
color[3] = c.alpha * alpha;
|
||||
renderSets[i].set(sizes[i]++, {
|
||||
position: p.position,
|
||||
scale: Math.max(
|
||||
0,
|
||||
(p.pixelSize + (i ? 0 : 2 * p.outlineWidth)) * scale,
|
||||
),
|
||||
color,
|
||||
show: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 2; i++) {
|
||||
renderSets[i].setCount(sizes[i]);
|
||||
renderSets[i].commit();
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
for (const set of renderSets) set.destroy();
|
||||
points.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
export function createTrafficNavaraRenderer(engine, native) {
|
||||
const { view } = native;
|
||||
const points = createTrafficPoints(engine);
|
||||
const surface = {
|
||||
engine,
|
||||
canvas: engine.host.canvas,
|
||||
postRender: engine.frames.postRender,
|
||||
get sampleHeightSupported() {
|
||||
return engine.surface.supported;
|
||||
},
|
||||
sampleHeight: (p) =>
|
||||
engine.maps.photorealActive()
|
||||
? engine.surface.sampleHeight(p)
|
||||
: engine.surface.terrainHeight(p),
|
||||
project: (world, out) => engine.projector.worldToWindow(world, out),
|
||||
get ready() {
|
||||
return engine.surface.tilesLoaded();
|
||||
},
|
||||
get surfaceKey() {
|
||||
return engine.maps.photorealActive() ? 'tiles' : 'terrain';
|
||||
},
|
||||
};
|
||||
const makeHeat = (roads, tint, width, alpha) => {
|
||||
if (!roads.length) return null;
|
||||
const source = view.addSource({
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: roads.map((r) => ({
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: { type: 'LineString', coordinates: r.coords },
|
||||
})),
|
||||
},
|
||||
});
|
||||
const css =
|
||||
(Math.round(tint.red * 255) << 16) |
|
||||
(Math.round(tint.green * 255) << 8) |
|
||||
Math.round(tint.blue * 255);
|
||||
let layer;
|
||||
try {
|
||||
layer = view.addLayer({
|
||||
type: 'vector',
|
||||
source,
|
||||
polyline: {
|
||||
color: css,
|
||||
width,
|
||||
opacity: alpha,
|
||||
clampToGround: true,
|
||||
transparent: true,
|
||||
lit: false,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
source.delete();
|
||||
throw e;
|
||||
}
|
||||
let current = alpha;
|
||||
layer.on('featureUpdated', ({ evaluator }) =>
|
||||
evaluator.evaluate(() => ({ opacity: current })),
|
||||
);
|
||||
return {
|
||||
setAlpha(a) {
|
||||
if (Math.abs(a - current) > 0.015) {
|
||||
current = a;
|
||||
layer.forceUpdate();
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
layer.delete();
|
||||
source.delete();
|
||||
},
|
||||
};
|
||||
};
|
||||
return { points, surface, makeHeat, destroy: () => points.destroy() };
|
||||
}
|
||||
@@ -37,6 +37,11 @@ export function createRendering({
|
||||
/** Remove both heat-line ground primitives from the scene. */
|
||||
|
||||
function removeHeatLines() {
|
||||
if (layerState._native) {
|
||||
layerState._heatJamPrim?.destroy();
|
||||
layerState._heatSlowPrim?.destroy();
|
||||
layerState._heatJamPrim = layerState._heatSlowPrim = null;
|
||||
}
|
||||
if (layerState._heatJamPrim) {
|
||||
layerState._viewer?.scene.groundPrimitives.remove(
|
||||
layerState._heatJamPrim,
|
||||
@@ -68,11 +73,12 @@ export function createRendering({
|
||||
function rebuildHeatLines(roads) {
|
||||
removeHeatLines();
|
||||
if (
|
||||
!layerState._viewer ||
|
||||
(!layerState._viewer && !layerState._native) ||
|
||||
!layerState._liveMode ||
|
||||
!parts.style.heatlineOn()
|
||||
)
|
||||
return;
|
||||
if (layerState._native) layerState._heatSupported = true;
|
||||
if (layerState._heatSupported === null) {
|
||||
layerState._heatSupported = Cesium.GroundPolylinePrimitive.isSupported(
|
||||
layerState._viewer.scene,
|
||||
@@ -122,6 +128,14 @@ export function createRendering({
|
||||
? new Cesium.Color(0.7, 0.7, 0.7, HEAT_SLOW_COLOR.alpha)
|
||||
: HEAT_SLOW_COLOR;
|
||||
|
||||
if (layerState._native) {
|
||||
const roadsFor = bucket => kept.filter(item=>item.bucket===bucket).map(item=>item.road);
|
||||
layerState._heatJamPrim = layerState._native.makeHeat(roadsFor('jam'), jamLineColor, HEAT_LINE_JAM_WIDTH, HEAT_JAM_BASE_ALPHA);
|
||||
layerState._heatSlowPrim = layerState._native.makeHeat(roadsFor('slow'), slowLineColor, HEAT_LINE_SLOW_WIDTH, slowLineColor.alpha);
|
||||
layerState._heatLineCount = kept.length;
|
||||
parts.animation.syncRenderDemand();
|
||||
return;
|
||||
}
|
||||
const jamInstances = instancesFor('jam', HEAT_LINE_JAM_WIDTH);
|
||||
if (jamInstances.length) {
|
||||
layerState._heatJamPrim = layerState._viewer.scene.groundPrimitives.add(
|
||||
@@ -202,7 +216,7 @@ export function createRendering({
|
||||
// nadir view: oblique pitches put the loaded roads many km away even at
|
||||
// low altitude. Probe three roads and stretch the curves accordingly.
|
||||
let areaDist = altitude;
|
||||
if (layerState._viewer && filteredRoads.length) {
|
||||
if ((layerState._viewer || layerState._engine) && filteredRoads.length) {
|
||||
const probes = [
|
||||
filteredRoads[0],
|
||||
filteredRoads[Math.floor(filteredRoads.length / 2)],
|
||||
@@ -214,7 +228,7 @@ export function createRendering({
|
||||
areaDist = Math.max(
|
||||
areaDist,
|
||||
Cesium.Cartesian3.distance(
|
||||
layerState._viewer.camera.positionWC,
|
||||
layerState._engine?.camera.livePosition ?? layerState._viewer.camera.positionWC,
|
||||
wp,
|
||||
),
|
||||
);
|
||||
@@ -233,10 +247,10 @@ export function createRendering({
|
||||
: null;
|
||||
parts.retention.reconcile(filteredRoads, altitude, replace);
|
||||
if (layerState._dots.length && filteredRoads.some((r) => !r.directFlow))
|
||||
services.credits?.showOsmCredit?.(layerState._viewer, 'traffic', {
|
||||
services.credits?.showOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic', {
|
||||
openMapTiles: true,
|
||||
});
|
||||
else services.credits?.hideOsmCredit?.(layerState._viewer, 'traffic');
|
||||
else services.credits?.hideOsmCredit?.(layerState._engine ?? layerState._viewer, 'traffic');
|
||||
parts.model.recolorDotsInPlace(label);
|
||||
|
||||
const renderMetrics = state
|
||||
|
||||
@@ -17,6 +17,7 @@ export function createRetention({ state, parts }) {
|
||||
const screenTo = new Cesium.Cartesian2();
|
||||
const eye = new Cesium.Cartesian3();
|
||||
function projectSafely(scene, point, screen) {
|
||||
if (scene.project) return Boolean(scene.project(point, screen));
|
||||
if (scene.camera.viewMatrix) {
|
||||
Cesium.Matrix4.multiplyByPoint(scene.camera.viewMatrix, point, eye);
|
||||
if (eye.z >= -(scene.camera.frustum?.near || 0)) return false;
|
||||
@@ -50,7 +51,7 @@ export function createRetention({ state, parts }) {
|
||||
};
|
||||
function pump() {
|
||||
timer = null;
|
||||
if (!state._enabled) return;
|
||||
if (!state._enabled || !state._pointCollection) return;
|
||||
let budget = DOT_CHANGE_BUDGET;
|
||||
let pending = false;
|
||||
const now = Date.now();
|
||||
@@ -191,9 +192,9 @@ export function createRetention({ state, parts }) {
|
||||
}
|
||||
function easeHeights(dt) {
|
||||
const blend = Math.min(1, dt * 12);
|
||||
const scene = state._viewer?.scene;
|
||||
const scene = state._trafficScene?.() ?? state._viewer?.scene;
|
||||
const canProject =
|
||||
scene?.canvas?.clientHeight && scene.camera && scene.frameState;
|
||||
scene?.canvas?.clientHeight && (scene.project || (scene.camera && scene.frameState));
|
||||
state._heightPending = false;
|
||||
for (let r = 0; r < state._roads.length; r++) {
|
||||
const road = state._roads[r];
|
||||
@@ -276,5 +277,5 @@ export function createRetention({ state, parts }) {
|
||||
state._populationPending = false;
|
||||
state._heightPending = false;
|
||||
}
|
||||
return { reconcile, easeHeights, finishHeights, clear, refreshCounts };
|
||||
return { reconcile, easeHeights, finishHeights, clear, refreshCounts, resume: schedule };
|
||||
}
|
||||
@@ -17,6 +17,10 @@ export function createState({ services }) {
|
||||
/** @type {Cesium.Viewer|null} */
|
||||
|
||||
layerState._viewer = null;
|
||||
layerState._engine = null;
|
||||
layerState._native = null;
|
||||
layerState._trafficScene = () => layerState._native?.surface ?? layerState._viewer?.scene;
|
||||
|
||||
|
||||
/** @type {Cesium.PointPrimitiveCollection|null} */
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export function observeTrafficSurface(scene) {
|
||||
}
|
||||
|
||||
function currentRenderedFrame(scene) {
|
||||
if (scene.engine) return {ready:scene.ready};
|
||||
if (!scene.postRender?.addEventListener || !scene.camera?.viewMatrix)
|
||||
return { ready: surfaceTilesReady(scene) };
|
||||
const frame = renderedFrames.get(scene);
|
||||
@@ -204,6 +205,7 @@ const surfaceId = (object) => {
|
||||
*/
|
||||
const surfaceKeys = new WeakMap();
|
||||
export function trafficSurfaceKey(scene) {
|
||||
if (scene.engine) return scene.surfaceKey;
|
||||
let state = surfaceKeys.get(scene);
|
||||
if (!state) {
|
||||
state = { globe: null, terrain: null, tiles: [], key: '' };
|
||||
@@ -278,7 +280,7 @@ export async function prepareRoadSurfaces(
|
||||
cache = { surface, heights: new Map() };
|
||||
sceneCaches.set(scene, cache);
|
||||
}
|
||||
const cameraCarto = scene.camera?.positionCartographic;
|
||||
const cameraCarto = scene.engine?.camera.pose().cartographic ?? scene.camera?.positionCartographic;
|
||||
const cameraLon = cameraCarto
|
||||
? Cesium.Math.toDegrees(cameraCarto.longitude)
|
||||
: 0;
|
||||
@@ -333,7 +335,8 @@ export async function prepareRoadSurfaces(
|
||||
let sampled;
|
||||
// sampleHeight reads the locally rendered mesh, not unresolved/offscreen
|
||||
// tiles. Global tilesLoaded can stay false while this street is usable.
|
||||
if (scene.globe?.show) sampled = scene.globe.getHeight?.(carto);
|
||||
if (scene.engine) sampled = scene.sampleHeight(carto);
|
||||
else if (scene.globe?.show) sampled = scene.globe.getHeight?.(carto);
|
||||
else {
|
||||
const estimate = Cesium.Cartographic.fromCartesian(
|
||||
road.waypoints[i],
|
||||
@@ -342,7 +345,7 @@ export async function prepareRoadSurfaces(
|
||||
if (validHeight(sampled)) metrics.depthHits++;
|
||||
}
|
||||
if (
|
||||
!scene.globe?.show &&
|
||||
!scene.engine && !scene.globe?.show &&
|
||||
!validHeight(sampled) &&
|
||||
scene.sampleHeightSupported
|
||||
) {
|
||||
|
||||
@@ -24,7 +24,7 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
*/
|
||||
|
||||
function getCameraAltitude() {
|
||||
const carto = layerState._viewer.camera.positionCartographic;
|
||||
const carto = layerState._engine?.camera.pose().cartographic ?? layerState._viewer?.camera.positionCartographic;
|
||||
return carto ? carto.height : Infinity;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
*/
|
||||
|
||||
function getViewBounds() {
|
||||
const rect = layerState._viewer.camera.computeViewRectangle();
|
||||
const rect = layerState._engine?.camera.viewRectangle() ?? layerState._viewer?.camera.computeViewRectangle();
|
||||
if (!rect) return null;
|
||||
return {
|
||||
south: geo.math.toDegrees(rect.south),
|
||||
@@ -60,18 +60,18 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
*/
|
||||
|
||||
function getFetchCenter() {
|
||||
const carto = layerState._viewer.camera.positionCartographic;
|
||||
const carto = layerState._engine?.camera.pose().cartographic ?? layerState._viewer?.camera.positionCartographic;
|
||||
if (!carto) return null;
|
||||
const nadirLat = geo.math.toDegrees(carto.latitude);
|
||||
const nadirLon = geo.math.toDegrees(carto.longitude);
|
||||
|
||||
let hitLat;
|
||||
let hitLon;
|
||||
const canvas = layerState._viewer.scene.canvas;
|
||||
const canvas = layerState._engine?.host.canvas ?? layerState._viewer.scene.canvas;
|
||||
const width = canvas.clientWidth || canvas.width;
|
||||
const height = canvas.clientHeight || canvas.height;
|
||||
if (width > 0 && height > 0) {
|
||||
const hit = layerState._viewer.camera.pickEllipsoid(
|
||||
const hit = (layerState._engine?.camera ?? layerState._viewer.camera).pickEllipsoid(
|
||||
geo.cartesian2.create(width / 2, height / 2),
|
||||
Cesium.Ellipsoid.WGS84,
|
||||
);
|
||||
@@ -174,7 +174,7 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
|
||||
/** Debounce camera events and sample the actual settled view, including final flight steps. */
|
||||
function onCameraChanged(options) {
|
||||
if (!layerState._enabled) return;
|
||||
if (!layerState._enabled || layerState._waitForScene || layerState._restoringViewport) return;
|
||||
clearTimeout(layerState._fetchTimeout);
|
||||
if (getCameraAltitude() > ACTIVATION_ALTITUDE) {
|
||||
clearTimeout(layerState._retryTimer);
|
||||
@@ -197,10 +197,10 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
: null;
|
||||
layerState._fetchTimeout = setTimeout(
|
||||
() => {
|
||||
if (!layerState._enabled) return;
|
||||
if (!layerState._enabled || layerState._waitForScene || layerState._restoringViewport) return;
|
||||
const alt = getCameraAltitude();
|
||||
if (alt > ACTIVATION_ALTITUDE) return onCameraChanged();
|
||||
const footprint = reticleFootprint(layerState._viewer, alt);
|
||||
const footprint = reticleFootprint(layerState._viewer, alt, layerState._engine);
|
||||
const bounds = footprint || getViewBounds();
|
||||
if (!bounds) return;
|
||||
const center = footprint ? null : getFetchCenter();
|
||||
|
||||
@@ -24,6 +24,7 @@ window.addEventListener('pagehide', (event) => {
|
||||
|
||||
async function start(next, engineSwitch = false, cockpit = null) {
|
||||
kind = next;
|
||||
layerSession.restoringView = engineSwitch;
|
||||
application = createStandaloneApplication({
|
||||
googleApiKey: import.meta.env.GOOGLE_MAPS_API_KEY,
|
||||
cesiumToken: import.meta.env.CESIUM_ION_TOKEN,
|
||||
@@ -37,6 +38,8 @@ async function start(next, engineSwitch = false, cockpit = null) {
|
||||
if (engineSwitch) {
|
||||
const style = controls.styleManager;
|
||||
await style.initialRestorePromise;
|
||||
layerSession.restoringView = false;
|
||||
for (const {module} of layerSession.manager.layers.values()) module.resumeViewport?.();
|
||||
if (layerSession.handoffLayers) layerSession.manager.layers.get('directions')?.module.resumeCamera?.();
|
||||
// Only resume the aircraft that actually survived the handoff. A history
|
||||
// entry may select a different subject, and a stale track may disappear.
|
||||
|
||||
Reference in new issue
Block a user