mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor(transit): isolate native rendering from playback
This commit is contained in:
1 parent
694616d5c2
commit
b8560e5b26
21 files changed
+465
-256
No files matched your search
@@ -828,6 +828,8 @@
|
||||
"src/layers/transit/policy.js",
|
||||
"src/layers/transit/queries.js",
|
||||
"src/layers/transit/rendering.js",
|
||||
"src/layers/transit/render/cesium.js",
|
||||
"src/layers/transit/render/cesium.test.mjs",
|
||||
"src/layers/transit/selection.js",
|
||||
"src/layers/transit/state.js",
|
||||
"src/layers/transit/testing.js",
|
||||
|
||||
@@ -2459,6 +2459,7 @@
|
||||
"src/data/transitPresetStyle.js",
|
||||
"src/layers/transit/controls.js",
|
||||
"src/layers/transit/height.js",
|
||||
"src/layers/transit/render/cesium.js",
|
||||
"src/layers/transit/index.js",
|
||||
"src/layers/transit/ingestion.js",
|
||||
"src/layers/transit/lifecycle.js",
|
||||
@@ -3172,6 +3173,7 @@
|
||||
"src/data/transitPresetStyle.js",
|
||||
"src/layers/transit/controls.js",
|
||||
"src/layers/transit/height.js",
|
||||
"src/layers/transit/render/cesium.js",
|
||||
"src/layers/transit/index.js",
|
||||
"src/layers/transit/ingestion.js",
|
||||
"src/layers/transit/lifecycle.js",
|
||||
@@ -3848,6 +3850,8 @@
|
||||
"src/data/transitProxy.js",
|
||||
"src/layers/transit/controls.js",
|
||||
"src/layers/transit/height.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/transit/render/cesium.js",
|
||||
"src/layers/transit/index.js",
|
||||
"src/layers/transit/ingestion.js",
|
||||
"src/layers/transit/lifecycle.js",
|
||||
@@ -3903,14 +3907,13 @@
|
||||
"transit-layer": {
|
||||
"exports": ["./layers/transit"],
|
||||
"modules": [
|
||||
"src/data/iconOrientation.js",
|
||||
"src/data/render/cesiumProjection.js",
|
||||
"src/data/inputOwnership.js",
|
||||
"src/data/transitFeeds.js",
|
||||
"src/data/transitIcons.js",
|
||||
"src/data/transitPresetStyle.js",
|
||||
"src/layers/transit/controls.js",
|
||||
"src/layers/transit/height.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/transit/index.js",
|
||||
"src/layers/transit/ingestion.js",
|
||||
"src/layers/transit/lifecycle.js",
|
||||
@@ -3921,10 +3924,8 @@
|
||||
"src/layers/transit/state.js",
|
||||
"src/layers/transit/testing.js",
|
||||
"src/layers/transit/viewport.js",
|
||||
"src/data/cameraSensitivity.js",
|
||||
"src/data/contactPlayback.js",
|
||||
"src/data/contactTrailRenderer.js",
|
||||
"src/data/render/cesiumContactTrailRenderer.js",
|
||||
"src/data/transitProxy.js",
|
||||
"src/data/gtfsRealtime.js",
|
||||
"src/layers/transit/trails.js",
|
||||
@@ -3952,8 +3953,7 @@
|
||||
"src/geo/check.js",
|
||||
"src/geo/debug.js",
|
||||
"src/geo/developerError.js",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js"
|
||||
"src/globe/contract.js"
|
||||
],
|
||||
"external": ["cesium", "pbf"]
|
||||
},
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
export function transitQaBase(value) {
|
||||
if (!value) throw new Error('Transit QA requires an explicit QA_BASE_URL');
|
||||
const url = new URL(value);
|
||||
if (
|
||||
['4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(url.port)
|
||||
)
|
||||
throw new Error('Transit QA may not use a reserved port');
|
||||
return url.href.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/** Bound page promises as well as CDP itself; labels identify a stalled section. */
|
||||
export function boundPageEvaluations(page, defaultTimeoutMs = 30000) {
|
||||
const evaluate = page.evaluate.bind(page);
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { transitQaBase } from './qa-transit-browser.mjs';
|
||||
|
||||
/** Prove parked and moving headings in the real scene, without live feeds. */
|
||||
export async function runTransitHeadingRegression(base, check) {
|
||||
base = transitQaBase(base);
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath:
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
args: ['--no-sandbox', '--disable-background-timer-throttling'],
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-background-timer-throttling',
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
],
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await blockPaidQaMaps(page);
|
||||
await page.setViewport({ width: 1280, height: 800 });
|
||||
await page.goto(
|
||||
`${base}/?welcome=0#lat=42.3601&lon=-71.0589&alt=1200&heading=0&pitch=-90`,
|
||||
|
||||
@@ -2,21 +2,26 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { reduceTrailPixels } from '../src/layers/transit/qaMetrics.js';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { transitQaBase } from './qa-transit-browser.mjs';
|
||||
|
||||
const base = process.env.QA_BASE_URL || 'http://localhost:4305';
|
||||
const base = transitQaBase(process.env.QA_BASE_URL);
|
||||
const shots = process.env.QA_SHOTS || 'qa-shots/transit-recovery';
|
||||
await mkdir(shots, { recursive: true });
|
||||
const browser = await puppeteer.launch({
|
||||
headless: process.env.QA_HEADFUL === '1' ? false : true,
|
||||
headless: true,
|
||||
executablePath:
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
args: [
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
],
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await blockPaidQaMaps(page);
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
|
||||
+10
-4
@@ -6,6 +6,7 @@ import {
|
||||
restoreRetainedTransit,
|
||||
sampleTransitPixels,
|
||||
readTransitFleetStyle,
|
||||
transitQaBase,
|
||||
} from './qa-transit-browser.mjs';
|
||||
import {
|
||||
BOSTON,
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
/**
|
||||
* Browser proof for the Transit layer against a running app.
|
||||
*
|
||||
* npm run qa:transit (defaults to http://localhost:4173)
|
||||
* QA_BASE_URL=http://localhost:4305 npm run qa:transit
|
||||
* QA_BASE_URL=http://localhost:4305 node scripts/qa-transit.mjs
|
||||
*
|
||||
* Drives the real layer toggle and the real CONTEXT chooser, watches the real
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
*/
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { createTransitHistory } from '../server/providers/transitHistory.js';
|
||||
import { DETECTION_THEME_MAP } from '../src/overlays/worldOverlayTokens.js';
|
||||
import { transitModeTier } from '../src/data/transitPresetStyle.js';
|
||||
@@ -50,9 +52,9 @@ import {
|
||||
reduceFleet,
|
||||
} from '../src/layers/transit/qaMetrics.js';
|
||||
|
||||
const BASE = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
const BASE = transitQaBase(process.env.QA_BASE_URL);
|
||||
const SHOTS = process.env.QA_SHOTS || 'qa-shots/transit';
|
||||
const TAG = process.env.QA_TAG || 'keyed';
|
||||
const TAG = process.env.QA_TAG || 'keyless';
|
||||
/** Which sections to run: all, or a comma list of cities,playback,disable,context. */
|
||||
const SECTIONS = new Set((process.env.QA_SECTIONS || 'all').split(','));
|
||||
const runs = (section) => SECTIONS.has('all') || SECTIONS.has(section);
|
||||
@@ -260,12 +262,14 @@ historyProbe.clear();
|
||||
await mkdir(SHOTS, { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: process.env.QA_HEADFUL === '1' ? false : true,
|
||||
headless: true,
|
||||
protocolTimeout: 150000,
|
||||
executablePath:
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--use-angle=metal',
|
||||
'--enable-gpu',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--window-size=1920,1080',
|
||||
@@ -273,6 +277,7 @@ const browser = await puppeteer.launch({
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await blockPaidQaMaps(page);
|
||||
boundPageEvaluations(page);
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
const pageErrors = [];
|
||||
@@ -1018,6 +1023,7 @@ try {
|
||||
if (runs('history')) {
|
||||
console.log('\n== retained history reload ==');
|
||||
const recovery = await browser.newPage();
|
||||
await blockPaidQaMaps(recovery);
|
||||
boundPageEvaluations(recovery);
|
||||
try {
|
||||
await recovery.goto(view(BOSTON), { waitUntil: 'domcontentloaded' });
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createTransitLayer } from '../../layers/transit/index.js';
|
||||
import { createTransitCesiumRenderer } from '../../layers/transit/render/cesium.js';
|
||||
import { rendererFor } from './renderers.js';
|
||||
import * as render from '../../renderGovernor.js';
|
||||
import * as sprites from '../../data/spriteOrder.js';
|
||||
import * as picking from '../../data/pickRegistry.js';
|
||||
@@ -22,12 +24,20 @@ import {
|
||||
* @param {{surface: object}} options
|
||||
* @returns {object}
|
||||
*/
|
||||
export function createApplicationTransit({ surface, source }) {
|
||||
export function createApplicationTransit({
|
||||
surface,
|
||||
source,
|
||||
rendererFactory = ({ engine, ...context }) =>
|
||||
engine?.kind === 'navara'
|
||||
? rendererFor('transit', engine, context)
|
||||
: createTransitCesiumRenderer(context),
|
||||
}) {
|
||||
if (!surface?.groundFloor || !surface?.meshFloor)
|
||||
throw new TypeError('Transit needs the application surface services');
|
||||
const { groundFloor, meshFloor } = surface;
|
||||
return createTransitLayer({
|
||||
source,
|
||||
rendererFactory,
|
||||
services: {
|
||||
render,
|
||||
sprites,
|
||||
|
||||
+9
-2
@@ -1,4 +1,5 @@
|
||||
import { createTransitLayer } from '../layers/transit/index.js';
|
||||
import { createTransitLayer as createLayer } from '../layers/transit/index.js';
|
||||
import { createTransitCesiumRenderer } from '../layers/transit/render/cesium.js';
|
||||
import * as render from '../renderGovernor.js';
|
||||
import * as sprites from './spriteOrder.js';
|
||||
import * as picking from './pickRegistry.js';
|
||||
@@ -13,6 +14,13 @@ import {
|
||||
} from './groundFloor.js';
|
||||
import { sampleMeshFloorCells } from './meshFloorSampler.js';
|
||||
|
||||
export function createTransitLayer(options) {
|
||||
return createLayer({
|
||||
rendererFactory: createTransitCesiumRenderer,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
const layer = createTransitLayer({
|
||||
services: {
|
||||
render,
|
||||
@@ -51,6 +59,5 @@ export {
|
||||
transitDetectionMetric,
|
||||
transitVehicleKey,
|
||||
vehicleFixAgeMs,
|
||||
createTransitLayer,
|
||||
} from '../layers/transit/index.js';
|
||||
export default layer;
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
FLOOR_REREAD_ATTEMPTS,
|
||||
FLOOR_REREAD_MS,
|
||||
@@ -18,13 +17,10 @@ export function createHeight({ state, services, parts }) {
|
||||
coarseFloorCoord,
|
||||
GROUND_FLOOR_LIFT_M,
|
||||
} = services.ground;
|
||||
// The application's rendered-mesh sampler. Optional so a layer built on a
|
||||
// bare floor stub still runs; the application always supplies it.
|
||||
const sampleMeshFloorCells = services.mesh?.sampleMeshFloorCells || null;
|
||||
|
||||
/** The camera's subpoint in degrees, for the sampler's proximity gate. */
|
||||
function viewerSubpoint() {
|
||||
const carto = state._viewer?.camera?.positionCartographic;
|
||||
const carto = parts.renderer?.cameraCartographic();
|
||||
if (!carto) return { viewerLat: undefined, viewerLon: undefined };
|
||||
return {
|
||||
viewerLat: (carto.latitude * 180) / Math.PI,
|
||||
@@ -33,7 +29,7 @@ export function createHeight({ state, services, parts }) {
|
||||
}
|
||||
|
||||
function cameraAltitude() {
|
||||
const carto = state._viewer?.camera?.positionCartographic;
|
||||
const carto = parts.renderer?.cameraCartographic();
|
||||
return carto && Number.isFinite(carto.height) ? carto.height : Infinity;
|
||||
}
|
||||
|
||||
@@ -53,7 +49,6 @@ export function createHeight({ state, services, parts }) {
|
||||
* @param {number} lon
|
||||
* @returns {{cell: string, height: number|null, prior: number|null}}
|
||||
*/
|
||||
const terrainPoint = new Cesium.Cartographic();
|
||||
function requestHeight(lat, lon, entry = null) {
|
||||
const coarse = coarseFloorCoord(lat, lon);
|
||||
const cell = `${coarse.lat},${coarse.lon}`;
|
||||
@@ -63,20 +58,9 @@ export function createHeight({ state, services, parts }) {
|
||||
return { cell, height: fixtureFloor, prior: null };
|
||||
// On a terrain globe, use the loaded surface under the marker. This is
|
||||
// synchronous cache work and does not launch a terrain request.
|
||||
const globe = state._viewer?.scene?.globe;
|
||||
if (globe?.show === true) {
|
||||
Cesium.Cartographic.fromDegrees(lon, lat, 0, terrainPoint);
|
||||
const rendered = globe.getHeight?.(terrainPoint);
|
||||
const flat =
|
||||
state._viewer?.terrainProvider instanceof
|
||||
Cesium.EllipsoidTerrainProvider;
|
||||
if (Number.isFinite(rendered) || flat)
|
||||
return {
|
||||
cell,
|
||||
height: (flat ? 0 : rendered) + GROUND_FLOOR_LIFT_M,
|
||||
prior: null,
|
||||
};
|
||||
}
|
||||
const rendered = parts.renderer?.terrainHeight(lat, lon);
|
||||
if (Number.isFinite(rendered))
|
||||
return { cell, height: rendered + GROUND_FLOOR_LIFT_M, prior: null };
|
||||
const floor = cachedGroundFloor(lat, lon);
|
||||
if (Number.isFinite(floor)) {
|
||||
return { cell, height: floor + GROUND_FLOOR_LIFT_M, prior: null };
|
||||
@@ -204,7 +188,7 @@ export function createHeight({ state, services, parts }) {
|
||||
for (let i = 0; i < dem.length; i += 8)
|
||||
warmGroundFloor(dem.slice(i, i + 8));
|
||||
for (let i = 0; i < mesh.length; i += 8)
|
||||
sampleMeshFloorCells?.(state._viewer?.scene, mesh.slice(i, i + 8), {
|
||||
parts.renderer?.sampleMeshFloorCells(mesh.slice(i, i + 8), {
|
||||
excludeObjects: markers,
|
||||
...viewerSubpoint(),
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ import { TRANSIT_POLL_MS } from './policy.js';
|
||||
export function createTransitLayer({
|
||||
services,
|
||||
source = createTransitSource(),
|
||||
rendererFactory,
|
||||
}) {
|
||||
if (!services?.overlays || !services?.render || !services?.sprites) {
|
||||
throw new TypeError(
|
||||
@@ -43,7 +44,7 @@ export function createTransitLayer({
|
||||
}
|
||||
const state = createState({ services });
|
||||
const parts = {};
|
||||
const context = { state, services, parts, source };
|
||||
const context = { state, services, parts, source, rendererFactory };
|
||||
parts.trails = createTrails(context);
|
||||
parts.height = createHeight(context);
|
||||
parts.selection = createSelection(context);
|
||||
|
||||
@@ -48,7 +48,6 @@ export const ORDER_REJECTS_BEFORE_RESET = 3;
|
||||
*/
|
||||
export function createIngestion({ state, services, parts, source }) {
|
||||
const { governorRequestRender } = services.render;
|
||||
const { registerDynamicCredit, transitFeedCredit } = services.credits;
|
||||
|
||||
function feedStatus(feedId) {
|
||||
let status = state._feedStatus.get(feedId);
|
||||
@@ -397,8 +396,7 @@ export function createIngestion({ state, services, parts, source }) {
|
||||
`[Data:Transit] ${feed.id}: ${rejectedFixes} fix(es) implied impossible travel and were not animated`,
|
||||
);
|
||||
}
|
||||
if (state._viewer)
|
||||
registerDynamicCredit(state._viewer, transitFeedCredit(feed));
|
||||
parts.renderer?.addCredit(feed);
|
||||
if (
|
||||
state._selectedKey &&
|
||||
state._vehicles.get(state._selectedKey)?.feedId === feed.id
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { destroyTrack } from '../../data/contactPlayback.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
claimCameraSensitivity,
|
||||
releaseCameraSensitivity,
|
||||
} from '../../data/cameraSensitivity.js';
|
||||
import {
|
||||
CAMERA_PERCENTAGE_CHANGED,
|
||||
TRANSIT_SELECTED_OVERLAY_SOURCE_ID,
|
||||
} from './policy.js';
|
||||
import { TRANSIT_SELECTED_OVERLAY_SOURCE_ID } from './policy.js';
|
||||
import { TRANSIT_ENABLED_FEEDS } from '../../data/transitFeeds.js';
|
||||
|
||||
/**
|
||||
@@ -18,12 +10,13 @@ import { TRANSIT_ENABLED_FEEDS } from '../../data/transitFeeds.js';
|
||||
* @param {object} context
|
||||
* @returns {object}
|
||||
*/
|
||||
export function createLifecycle({ state, services, parts }) {
|
||||
const {
|
||||
registerSpriteCollection,
|
||||
unregisterSpriteCollection,
|
||||
restoreSpriteOrder,
|
||||
} = services.sprites;
|
||||
export function createLifecycle({
|
||||
state,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
rendererFactory,
|
||||
}) {
|
||||
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
||||
const { releaseContinuousRender } = services.render;
|
||||
|
||||
@@ -38,17 +31,13 @@ export function createLifecycle({ state, services, parts }) {
|
||||
* default while Bikeshare was still relying on a fine one was exactly the
|
||||
* bug that produced.
|
||||
*/
|
||||
function borrowCameraSensitivity(viewer) {
|
||||
claimCameraSensitivity(
|
||||
viewer?.camera,
|
||||
'transit',
|
||||
CAMERA_PERCENTAGE_CHANGED,
|
||||
);
|
||||
function borrowCameraSensitivity() {
|
||||
parts.renderer.claimCamera();
|
||||
}
|
||||
|
||||
/** Give the claim up. The camera keeps whatever anyone else still asks for. */
|
||||
function returnCameraSensitivity(viewer) {
|
||||
releaseCameraSensitivity(viewer?.camera, 'transit');
|
||||
function returnCameraSensitivity() {
|
||||
parts.renderer?.releaseCamera();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,20 +113,29 @@ export function createLifecycle({ state, services, parts }) {
|
||||
* Create the point collection. Called once at bootstrap.
|
||||
* @param {Cesium.Viewer} viewer
|
||||
*/
|
||||
init(viewer) {
|
||||
init(viewer, options = {}) {
|
||||
if (parts.renderer)
|
||||
throw new Error('Transit layer is already initialized');
|
||||
if (typeof rendererFactory !== 'function')
|
||||
throw new TypeError('A transit renderer factory is required');
|
||||
const renderer = rendererFactory({
|
||||
state,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
engine: options.engine,
|
||||
});
|
||||
state._viewer = viewer;
|
||||
state._markers = new Cesium.BillboardCollection({
|
||||
blendOption: Cesium.BlendOption.TRANSLUCENT,
|
||||
});
|
||||
viewer.scene.primitives.add(state._markers);
|
||||
registerSpriteCollection('transit', state._markers);
|
||||
state._animatedMarkers = new Cesium.BillboardCollection({
|
||||
blendOption: Cesium.BlendOption.TRANSLUCENT,
|
||||
});
|
||||
viewer.scene.primitives.add(state._animatedMarkers);
|
||||
registerSpriteCollection('transit-motion', state._animatedMarkers);
|
||||
state._animatedMarkers.show = false;
|
||||
state._markers.show = false;
|
||||
parts.renderer = renderer;
|
||||
try {
|
||||
renderer.attach(viewer);
|
||||
[state._markers, state._animatedMarkers] = renderer.createMarkers();
|
||||
} catch (error) {
|
||||
renderer.destroy();
|
||||
parts.renderer = null;
|
||||
state._viewer = null;
|
||||
throw error;
|
||||
}
|
||||
state._enabled = false;
|
||||
state._activeFeeds.clear();
|
||||
state._feedStatus.clear();
|
||||
@@ -159,7 +157,7 @@ export function createLifecycle({ state, services, parts }) {
|
||||
state._rotationPose = null;
|
||||
state._rotationDirty = false;
|
||||
state._overlayHost.setVisible(TRANSIT_SELECTED_OVERLAY_SOURCE_ID, false);
|
||||
restoreSpriteOrder(viewer);
|
||||
parts.renderer.restoreOrder();
|
||||
bindStyleEvents();
|
||||
console.log(
|
||||
`[Data:Transit] Initialized with ${TRANSIT_ENABLED_FEEDS.length} GTFS-RT feeds`,
|
||||
@@ -185,17 +183,19 @@ export function createLifecycle({ state, services, parts }) {
|
||||
parts.selection.installClickHandler(viewer);
|
||||
registerPickOwner('transit', (pickedId) => state._vehicles.has(pickedId));
|
||||
if (!state._cameraChangedAttached) {
|
||||
viewer.camera.changed.addEventListener(parts.viewport.onCameraChanged);
|
||||
state._cameraChangedRemove = parts.renderer.onCameraChanged(
|
||||
parts.viewport.onCameraChanged,
|
||||
);
|
||||
borrowCameraSensitivity(viewer);
|
||||
state._cameraChangedAttached = true;
|
||||
}
|
||||
if (!state._preRenderRemove) {
|
||||
state._preRenderRemove = viewer.scene.preRender.addEventListener(
|
||||
state._preRenderRemove = parts.renderer.onPreRender(
|
||||
parts.rendering.onPreRender,
|
||||
);
|
||||
}
|
||||
parts.viewport.runProximityCheck();
|
||||
restoreSpriteOrder(viewer);
|
||||
parts.renderer.restoreOrder();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -222,9 +222,8 @@ export function createLifecycle({ state, services, parts }) {
|
||||
parts.selection.removeClickHandler();
|
||||
unregisterPickOwner('transit');
|
||||
if (state._cameraChangedAttached) {
|
||||
viewer.camera.changed.removeEventListener(
|
||||
parts.viewport.onCameraChanged,
|
||||
);
|
||||
state._cameraChangedRemove?.();
|
||||
state._cameraChangedRemove = null;
|
||||
returnCameraSensitivity(viewer);
|
||||
state._cameraChangedAttached = false;
|
||||
}
|
||||
@@ -281,16 +280,9 @@ export function createLifecycle({ state, services, parts }) {
|
||||
*/
|
||||
destroy(viewer) {
|
||||
this.disable(viewer);
|
||||
if (state._markers) {
|
||||
unregisterSpriteCollection('transit', state._markers);
|
||||
viewer?.scene?.primitives?.remove(state._markers);
|
||||
state._markers = null;
|
||||
}
|
||||
if (state._animatedMarkers) {
|
||||
unregisterSpriteCollection('transit-motion', state._animatedMarkers);
|
||||
viewer?.scene?.primitives?.remove(state._animatedMarkers);
|
||||
state._animatedMarkers = null;
|
||||
}
|
||||
parts.renderer?.destroy();
|
||||
parts.renderer = null;
|
||||
state._markers = state._animatedMarkers = null;
|
||||
state._overlayHost.clearSource(TRANSIT_SELECTED_OVERLAY_SOURCE_ID);
|
||||
parts.height.clear();
|
||||
unbindStyleEvents();
|
||||
|
||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createTransitLayer } from './index.js';
|
||||
import { createTransitCesiumRenderer } from './render/cesium.js';
|
||||
import {
|
||||
FEED_EVICT_AFTER_MS,
|
||||
FEED_STALE_AFTER_MS,
|
||||
@@ -264,6 +265,7 @@ function harness(
|
||||
/** Every call the floor cycle makes to the rendered-mesh sampler. */
|
||||
const meshCalls = [];
|
||||
const layer = createTransitLayer({
|
||||
rendererFactory: createTransitCesiumRenderer,
|
||||
source,
|
||||
services: {
|
||||
ground,
|
||||
@@ -937,6 +939,7 @@ test('destroy releases every shared registration this layer took', async (t) =>
|
||||
test('two layers built from the factory share no state', async (t) => {
|
||||
const app = harness(t);
|
||||
const second = createTransitLayer({
|
||||
rendererFactory: createTransitCesiumRenderer,
|
||||
services: {
|
||||
ground: groundStub(() => 12),
|
||||
render: {
|
||||
@@ -2922,6 +2925,13 @@ test('mesh refinement reaches all 100 retained DEM cells within five poll budget
|
||||
},
|
||||
},
|
||||
parts: {
|
||||
renderer: {
|
||||
cameraCartographic: () => state._viewer.camera.positionCartographic,
|
||||
terrainHeight: () => undefined,
|
||||
sampleMeshFloorCells(points) {
|
||||
for (const point of points) probed.add(point.cell);
|
||||
},
|
||||
},
|
||||
trails: {
|
||||
prepareEntry(e, collect) {
|
||||
if (collect)
|
||||
@@ -3931,6 +3941,9 @@ test('parked transit reprojects held and reported courses at the bounded camera
|
||||
_rotationDirty: false,
|
||||
};
|
||||
const noop = () => {};
|
||||
const renderer = createTransitCesiumRenderer({ state, services: {} });
|
||||
renderer.attach(state._viewer);
|
||||
t.after(() => renderer.destroy());
|
||||
const rendering = createRendering({
|
||||
state,
|
||||
services: {
|
||||
@@ -3940,7 +3953,11 @@ test('parked transit reprojects held and reported courses at the bounded camera
|
||||
releaseContinuousRender: noop,
|
||||
},
|
||||
},
|
||||
parts: { height: { nearGround: () => false }, trails: { update: noop } },
|
||||
parts: {
|
||||
renderer,
|
||||
height: { nearGround: () => false },
|
||||
trails: { update: noop },
|
||||
},
|
||||
});
|
||||
rendering.onPreRender();
|
||||
const before = [held.marker.rotation, reportedOnly.marker.rotation];
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../../geo/index.js';
|
||||
import {
|
||||
cameraPoseSignature,
|
||||
perspectiveProjectedRotation,
|
||||
} from '../../../data/iconOrientation.js';
|
||||
import {
|
||||
claimCameraSensitivity,
|
||||
releaseCameraSensitivity,
|
||||
} from '../../../data/cameraSensitivity.js';
|
||||
import { createContactTrailRenderer } from '../../../data/render/cesiumContactTrailRenderer.js';
|
||||
import { CAMERA_PERCENTAGE_CHANGED } from '../policy.js';
|
||||
|
||||
/** Native resource owner; feed/playback/selection policy stays in the layer. */
|
||||
export function createTransitCesiumRenderer({ state, services }) {
|
||||
let viewer = null;
|
||||
const banks = [];
|
||||
const removers = new Set();
|
||||
const terrainPoint = new Cesium.Cartographic();
|
||||
const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84);
|
||||
const sphere = new Cesium.BoundingSphere(undefined, 5);
|
||||
const cameraPosition = geo.cartesian3.create();
|
||||
const own = (remove) => {
|
||||
const release = () => {
|
||||
if (removers.delete(release)) remove();
|
||||
};
|
||||
removers.add(release);
|
||||
return release;
|
||||
};
|
||||
return {
|
||||
attach(host) {
|
||||
viewer = host;
|
||||
},
|
||||
get ready() {
|
||||
return Boolean(viewer);
|
||||
},
|
||||
createMarkers() {
|
||||
try {
|
||||
for (const id of ['transit', 'transit-motion']) {
|
||||
const collection = new Cesium.BillboardCollection({
|
||||
blendOption: Cesium.BlendOption.TRANSLUCENT,
|
||||
});
|
||||
banks.push({ id, collection });
|
||||
viewer.scene.primitives.add(collection);
|
||||
services.sprites.registerSpriteCollection(id, collection);
|
||||
collection.show = false;
|
||||
}
|
||||
return banks.map(({ collection }) => collection);
|
||||
} catch (error) {
|
||||
this.destroy();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
restoreOrder: () => services.sprites.restoreSpriteOrder(viewer),
|
||||
cameraCartographic: () => viewer?.camera?.positionCartographic,
|
||||
viewRectangle: () =>
|
||||
viewer?.camera?.computeViewRectangle?.(viewer.scene.globe?.ellipsoid),
|
||||
cameraSignature: () =>
|
||||
viewer?.scene?.camera ? cameraPoseSignature(viewer.scene.camera) : null,
|
||||
rotation: (position, course, previous) =>
|
||||
perspectiveProjectedRotation(viewer.scene, position, course, previous),
|
||||
visibilityTester() {
|
||||
const camera = viewer?.camera;
|
||||
if (!camera) return null;
|
||||
const position =
|
||||
camera.positionWC ||
|
||||
geo.ellipsoid.WGS84.cartographicToCartesian(
|
||||
camera.positionCartographic,
|
||||
cameraPosition,
|
||||
);
|
||||
occluder.cameraPosition = position;
|
||||
const frustum = camera.frustum?.computeCullingVolume(
|
||||
position,
|
||||
camera.directionWC,
|
||||
camera.upWC,
|
||||
);
|
||||
const planes = frustum?.planes;
|
||||
return {
|
||||
hasFrustum: Boolean(frustum),
|
||||
test(point) {
|
||||
sphere.center = point;
|
||||
let inFrustum = true;
|
||||
if (planes) {
|
||||
for (const p of planes) {
|
||||
if (
|
||||
p.x * point.x + p.y * point.y + p.z * point.z + p.w <
|
||||
-sphere.radius
|
||||
) {
|
||||
inFrustum = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (frustum)
|
||||
inFrustum =
|
||||
frustum.computeVisibility(sphere) !== Cesium.Intersect.OUTSIDE;
|
||||
return { horizon: occluder.isPointVisible(point), inFrustum };
|
||||
},
|
||||
};
|
||||
},
|
||||
claimCamera() {
|
||||
claimCameraSensitivity(
|
||||
viewer?.camera,
|
||||
'transit',
|
||||
CAMERA_PERCENTAGE_CHANGED,
|
||||
);
|
||||
},
|
||||
releaseCamera() {
|
||||
releaseCameraSensitivity(viewer?.camera, 'transit');
|
||||
},
|
||||
onCameraChanged: (fn) => own(viewer.camera.changed.addEventListener(fn)),
|
||||
onPreRender: (fn) => own(viewer.scene.preRender.addEventListener(fn)),
|
||||
onPostRender: (fn) => own(viewer.scene.postRender.addEventListener(fn)),
|
||||
terrainHeight(lat, lon) {
|
||||
const globe = viewer?.scene?.globe;
|
||||
if (globe?.show !== true) return undefined;
|
||||
Cesium.Cartographic.fromDegrees(lon, lat, 0, terrainPoint);
|
||||
const rendered = globe.getHeight?.(terrainPoint);
|
||||
return viewer?.terrainProvider instanceof Cesium.EllipsoidTerrainProvider
|
||||
? 0
|
||||
: rendered;
|
||||
},
|
||||
sampleMeshFloorCells: (points, options) =>
|
||||
services.mesh?.sampleMeshFloorCells?.(viewer?.scene, points, options),
|
||||
addCredit(feed) {
|
||||
if (viewer)
|
||||
services.credits.registerDynamicCredit(
|
||||
viewer,
|
||||
services.credits.transitFeedCredit(feed),
|
||||
);
|
||||
},
|
||||
createTrailRenderer: () => createContactTrailRenderer(viewer.scene),
|
||||
bindClick(listener, pointerFree) {
|
||||
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||
handler.setInputAction((click) => {
|
||||
if (!pointerFree()) return;
|
||||
const scene = viewer.scene;
|
||||
const position = Cesium.Cartesian2.clone(click.position);
|
||||
const style = state._stylePreset;
|
||||
const stage = scene.postProcessStages?.getStageByName?.(
|
||||
`godsEyeView_${style}`,
|
||||
);
|
||||
if (
|
||||
stage?.enabled &&
|
||||
(style === 'thermal' || style === 'surveillance')
|
||||
) {
|
||||
const intensity = stage.uniforms.intensity;
|
||||
const canvas = scene.canvas,
|
||||
w = canvas.clientWidth,
|
||||
h = canvas.clientHeight;
|
||||
let u = position.x / w,
|
||||
v = 1 - position.y / h;
|
||||
if (style === 'surveillance') {
|
||||
const x = u * 2 - 1,
|
||||
y = v * 2 - 1,
|
||||
r2 = x * x + y * y;
|
||||
const d = 1 + r2 * intensity * 0.25 + r2 * r2 * intensity * 0.075;
|
||||
u = (x * d + 1) / 2;
|
||||
v = (y * d + 1) / 2;
|
||||
}
|
||||
const grid = 1 + (stage.uniforms.pixelation - 1) * intensity;
|
||||
u +=
|
||||
((Math.floor((u * canvas.width) / grid) * grid) / canvas.width -
|
||||
u) *
|
||||
intensity;
|
||||
v +=
|
||||
((Math.floor((v * canvas.height) / grid) * grid) / canvas.height -
|
||||
v) *
|
||||
intensity;
|
||||
position.x = u * w;
|
||||
position.y = (1 - v) * h;
|
||||
}
|
||||
const picked = scene.pick(position);
|
||||
listener({
|
||||
id: typeof picked?.id === 'string' ? picked.id : null,
|
||||
primitiveId:
|
||||
typeof picked?.primitive?.id === 'string'
|
||||
? picked.primitive.id
|
||||
: null,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
collection:
|
||||
picked?.primitive?._billboardCollection === state._animatedMarkers
|
||||
? 'animated'
|
||||
: picked?.primitive?._billboardCollection === state._markers
|
||||
? 'stationary'
|
||||
: null,
|
||||
});
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
const remove = own(() => handler.destroy());
|
||||
return {
|
||||
destroy: remove,
|
||||
getInputAction: (type) => handler.getInputAction(type),
|
||||
};
|
||||
},
|
||||
frameProbe() {
|
||||
return {
|
||||
canvas: viewer.scene.canvas,
|
||||
gl: viewer.scene.context._gl,
|
||||
uploads: banks.map(({ collection }) => ({
|
||||
target: collection,
|
||||
method: 'update',
|
||||
})),
|
||||
};
|
||||
},
|
||||
destroy() {
|
||||
for (const remove of [...removers]) remove();
|
||||
this.releaseCamera();
|
||||
for (const { id, collection } of banks.splice(0)) {
|
||||
services.sprites.unregisterSpriteCollection(id, collection);
|
||||
viewer?.scene?.primitives?.remove(collection);
|
||||
if (!collection.isDestroyed()) collection.destroy();
|
||||
}
|
||||
viewer = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createTransitCesiumRenderer } from './cesium.js';
|
||||
|
||||
test('failed native bank registration releases both acquired collections', () => {
|
||||
const acquired = [],
|
||||
registered = new Map();
|
||||
const primitives = new Cesium.PrimitiveCollection();
|
||||
const renderer = createTransitCesiumRenderer({
|
||||
state: {},
|
||||
services: {
|
||||
sprites: {
|
||||
registerSpriteCollection(id, collection) {
|
||||
acquired.push(collection);
|
||||
registered.set(id, collection);
|
||||
if (id === 'transit-motion') throw new Error('registration failed');
|
||||
},
|
||||
unregisterSpriteCollection: (id) => registered.delete(id),
|
||||
},
|
||||
},
|
||||
});
|
||||
renderer.attach({ scene: { primitives } });
|
||||
assert.throws(() => renderer.createMarkers(), /registration failed/);
|
||||
assert.equal(primitives.length, 0);
|
||||
assert.equal(registered.size, 0);
|
||||
assert.equal(acquired.length, 2);
|
||||
assert.ok(acquired.every((collection) => collection.isDestroyed()));
|
||||
renderer.destroy();
|
||||
primitives.destroy();
|
||||
});
|
||||
|
||||
test('failed scene insertion also destroys the uninserted collection', (t) => {
|
||||
let acquired;
|
||||
t.mock.method(Cesium.BillboardCollection.prototype, 'destroy', function () {
|
||||
acquired = this;
|
||||
this.isDestroyed = () => true;
|
||||
});
|
||||
const renderer = createTransitCesiumRenderer({
|
||||
state: {},
|
||||
services: { sprites: { unregisterSpriteCollection() {} } },
|
||||
});
|
||||
renderer.attach({
|
||||
scene: {
|
||||
primitives: {
|
||||
add() {
|
||||
throw new Error('insertion failed');
|
||||
},
|
||||
remove() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.throws(() => renderer.createMarkers(), /insertion failed/);
|
||||
assert.ok(acquired?.isDestroyed());
|
||||
renderer.destroy();
|
||||
});
|
||||
@@ -1,9 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { Rgba, rgbaFromCss } from '../shared/color.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
cameraPoseSignature,
|
||||
perspectiveProjectedRotation,
|
||||
} from '../../data/iconOrientation.js';
|
||||
import {
|
||||
updatePlayback,
|
||||
applyDisplayCourse,
|
||||
@@ -33,7 +29,7 @@ import {
|
||||
export const MODE_CESIUM_COLORS = Object.fromEntries(
|
||||
Object.entries(TRANSIT_MODE_COLORS).map(([mode, css]) => [
|
||||
mode,
|
||||
Cesium.Color.fromCssColorString(css),
|
||||
rgbaFromCss(css),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -198,12 +194,7 @@ export function createRendering({ state, services, parts }) {
|
||||
const key = `${transitStyleProfile(style)}:${mode}`;
|
||||
let color = _presetColors.get(key);
|
||||
if (!color) {
|
||||
color = new Cesium.Color(
|
||||
rgba[0] / 255,
|
||||
rgba[1] / 255,
|
||||
rgba[2] / 255,
|
||||
rgba[3],
|
||||
);
|
||||
color = new Rgba(rgba[0] / 255, rgba[1] / 255, rgba[2] / 255, rgba[3]);
|
||||
_presetColors.set(key, color);
|
||||
}
|
||||
return color;
|
||||
@@ -303,11 +294,10 @@ export function createRendering({ state, services, parts }) {
|
||||
* changed somebody's heading.
|
||||
*/
|
||||
function refreshRotations(now) {
|
||||
const scene = state._viewer?.scene;
|
||||
if (!scene?.camera) return;
|
||||
if (!parts.renderer?.ready) return;
|
||||
if (now - state._rotationAt < ROTATION_REFRESH_MS) return;
|
||||
state._rotationAt = now;
|
||||
const pose = cameraPoseSignature(scene.camera);
|
||||
const pose = parts.renderer.cameraSignature();
|
||||
const cameraChanged =
|
||||
state._rotationPose !== pose ||
|
||||
state._rotationRevision !== state._cameraRevision;
|
||||
@@ -345,8 +335,7 @@ export function createRendering({ state, services, parts }) {
|
||||
// camera-basis helper aircraft use is orthographic — fine at altitude,
|
||||
// and wrong by twenty-odd degrees for a street-level contact away from
|
||||
// the centre of an obliquely pitched view, which is most of them.
|
||||
entry.marker.rotation = perspectiveProjectedRotation(
|
||||
scene,
|
||||
entry.marker.rotation = parts.renderer.rotation(
|
||||
entry.marker.position,
|
||||
course,
|
||||
entry.marker.rotation,
|
||||
@@ -424,8 +413,7 @@ export function createRendering({ state, services, parts }) {
|
||||
const selected = state._vehicles.get(state._selectedKey);
|
||||
if (selected && state._visible.has(selected)) {
|
||||
selected.marker.rotation = Number.isFinite(selected.courseDeg)
|
||||
? perspectiveProjectedRotation(
|
||||
state._viewer.scene,
|
||||
? parts.renderer.rotation(
|
||||
selected.marker.position,
|
||||
selected.courseDeg,
|
||||
selected.marker.rotation,
|
||||
@@ -436,9 +424,6 @@ export function createRendering({ state, services, parts }) {
|
||||
syncRenderHold();
|
||||
}
|
||||
|
||||
const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84);
|
||||
const sphere = new Cesium.BoundingSphere(undefined, 5);
|
||||
const cameraPosition = geo.cartesian3.create();
|
||||
const candidatePosition = geo.cartesian3.create();
|
||||
/** Visibility work runs off the frame path, coalesced at four sweeps/second. */
|
||||
function requestVisibility() {
|
||||
@@ -461,22 +446,8 @@ export function createRendering({ state, services, parts }) {
|
||||
}
|
||||
state._visibilityAt = now;
|
||||
state._visibilityDirty = false;
|
||||
const viewer = state._viewer,
|
||||
camera = viewer?.camera;
|
||||
if (!camera) return 0;
|
||||
const position =
|
||||
camera.positionWC ||
|
||||
geo.ellipsoid.WGS84.cartographicToCartesian(
|
||||
camera.positionCartographic,
|
||||
cameraPosition,
|
||||
);
|
||||
occluder.cameraPosition = position;
|
||||
const frustum = camera.frustum?.computeCullingVolume(
|
||||
position,
|
||||
camera.directionWC,
|
||||
camera.upWC,
|
||||
);
|
||||
const planes = frustum?.planes;
|
||||
const visibilityTest = parts.renderer?.visibilityTester();
|
||||
if (!visibilityTest) return 0;
|
||||
let changed = false;
|
||||
for (const entry of state._vehicles.values()) {
|
||||
if (!entry.marker) continue;
|
||||
@@ -487,7 +458,7 @@ export function createRendering({ state, services, parts }) {
|
||||
entry.sample ||= {};
|
||||
updatePlayback(entry, now, performance.now());
|
||||
}
|
||||
sphere.center =
|
||||
const position =
|
||||
state._visible.has(entry) || !entry.sample
|
||||
? entry.marker.position
|
||||
: cartesianFor(
|
||||
@@ -500,24 +471,12 @@ export function createRendering({ state, services, parts }) {
|
||||
// view. The current frustum is authoritative; the horizon still rejects
|
||||
// the far side when the rectangle is missing or stale.
|
||||
const bounds = vehicleNearView(state._viewBounds, entry);
|
||||
const horizon = occluder.isPointVisible(sphere.center);
|
||||
let inFrustum = true;
|
||||
if (planes) {
|
||||
for (let i = 0; i < planes.length; i++) {
|
||||
const p = planes[i],
|
||||
c = sphere.center;
|
||||
if (p.x * c.x + p.y * c.y + p.z * c.z + p.w < -sphere.radius) {
|
||||
inFrustum = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (frustum)
|
||||
inFrustum =
|
||||
frustum.computeVisibility(sphere) !== Cesium.Intersect.OUTSIDE;
|
||||
const visible = (frustum || bounds) && horizon && inFrustum;
|
||||
const { horizon, inFrustum } = visibilityTest.test(position);
|
||||
const visible =
|
||||
(visibilityTest.hasFrustum || bounds) && horizon && inFrustum;
|
||||
const visibility = (entry.visibility ||= {});
|
||||
visibility.bounds = bounds;
|
||||
visibility.boundsApplied = !frustum;
|
||||
visibility.boundsApplied = !visibilityTest.hasFrustum;
|
||||
visibility.occluder = horizon;
|
||||
visibility.frustum = inFrustum;
|
||||
visibility.noSample = noSample;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
SELECTED_CARD_REFRESH_MS,
|
||||
TRANSIT_SELECTED_OVERLAY_SOURCE_ID,
|
||||
@@ -114,79 +113,17 @@ export function createSelection({ state, services, parts }) {
|
||||
|
||||
function installClickHandler(viewer) {
|
||||
if (state._clickHandler) return;
|
||||
state._clickHandler = new Cesium.ScreenSpaceEventHandler(
|
||||
viewer.scene.canvas,
|
||||
);
|
||||
state._clickHandler.setInputAction((click) => {
|
||||
// Ambient selection never competes for a pointer a tool has claimed:
|
||||
// placing a draw vertex on top of a bus must not also select the bus.
|
||||
if (!isPointerFree()) return;
|
||||
const scene = viewer.scene;
|
||||
// Pick buffers precede post-processing. Map a sensor pixel back through
|
||||
// the active shader's barrel distortion and quantization before picking.
|
||||
const position = Cesium.Cartesian2.clone(click.position);
|
||||
const style = state._stylePreset;
|
||||
const stage = scene.postProcessStages?.getStageByName?.(
|
||||
`godsEyeView_${style}`,
|
||||
);
|
||||
if (stage?.enabled && (style === 'thermal' || style === 'surveillance')) {
|
||||
const intensity = stage.uniforms.intensity;
|
||||
const canvas = scene.canvas,
|
||||
w = canvas.clientWidth,
|
||||
h = canvas.clientHeight;
|
||||
let u = position.x / w,
|
||||
v = 1 - position.y / h;
|
||||
if (style === 'surveillance') {
|
||||
const x = u * 2 - 1,
|
||||
y = v * 2 - 1,
|
||||
r2 = x * x + y * y;
|
||||
const d = 1 + r2 * intensity * 0.25 + r2 * r2 * intensity * 0.075;
|
||||
u = (x * d + 1) / 2;
|
||||
v = (y * d + 1) / 2;
|
||||
}
|
||||
const grid = 1 + (stage.uniforms.pixelation - 1) * intensity;
|
||||
u +=
|
||||
((Math.floor((u * canvas.width) / grid) * grid) / canvas.width - u) *
|
||||
intensity;
|
||||
v +=
|
||||
((Math.floor((v * canvas.height) / grid) * grid) / canvas.height -
|
||||
v) *
|
||||
intensity;
|
||||
position.x = u * w;
|
||||
position.y = (1 - v) * h;
|
||||
}
|
||||
const picked = scene.pick(position);
|
||||
state._lastPickForTest = {
|
||||
id: typeof picked?.id === 'string' ? picked.id : null,
|
||||
primitiveId:
|
||||
typeof picked?.primitive?.id === 'string'
|
||||
? picked.primitive.id
|
||||
: null,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
collection:
|
||||
picked?.primitive?._billboardCollection === state._animatedMarkers
|
||||
? 'animated'
|
||||
: picked?.primitive?._billboardCollection === state._markers
|
||||
? 'stationary'
|
||||
: null,
|
||||
};
|
||||
if (picked) {
|
||||
const primitiveId = picked.primitive?.id;
|
||||
if (
|
||||
typeof primitiveId === 'string' &&
|
||||
state._vehicles.has(primitiveId)
|
||||
) {
|
||||
selectVehicle(primitiveId);
|
||||
return;
|
||||
}
|
||||
if (typeof picked.id === 'string' && state._vehicles.has(picked.id)) {
|
||||
selectVehicle(picked.id);
|
||||
return;
|
||||
}
|
||||
state._clickHandler = parts.renderer.bindClick((picked) => {
|
||||
state._lastPickForTest = picked;
|
||||
const key = state._vehicles.has(picked.primitiveId)
|
||||
? picked.primitiveId
|
||||
: picked.id;
|
||||
if (typeof key === 'string' && state._vehicles.has(key)) {
|
||||
selectVehicle(key);
|
||||
return;
|
||||
}
|
||||
if (state._selectedKey) clearSelection();
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
}, isPointerFree);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export function createState({ services }) {
|
||||
state._generation = 0;
|
||||
|
||||
state._cameraChangedAttached = false;
|
||||
state._cameraChangedRemove = null;
|
||||
state._cameraDebounceTimer = null;
|
||||
state._altitudeGateOpen = false;
|
||||
/** Camera sensitivity as we found it, so disable() can hand it back. */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { recordFix, updatePlayback, attachFloorToPlace } from './movement.js';
|
||||
import { seek, setRate, mergeHistory } from '../../data/contactPlayback.js';
|
||||
import { getRegisteredTransitFeed } from '../../data/transitFeeds.js';
|
||||
@@ -97,8 +97,8 @@ export function createTesting({ state, parts }) {
|
||||
entry.displayPaths.set(fixes[i].seq, {
|
||||
toSeq: fixes[i + 1].seq,
|
||||
positions: [
|
||||
Cesium.Cartesian3.fromDegrees(fixes[i].lon, fixes[i].lat, 20),
|
||||
Cesium.Cartesian3.fromDegrees(
|
||||
geo.cartesian3.fromDegrees(fixes[i].lon, fixes[i].lat, 20),
|
||||
geo.cartesian3.fromDegrees(
|
||||
fixes[i + 1].lon,
|
||||
fixes[i + 1].lat,
|
||||
20,
|
||||
@@ -139,8 +139,8 @@ export function createTesting({ state, parts }) {
|
||||
entry.displayPaths.set(fixes[i].seq, {
|
||||
toSeq: fixes[i + 1].seq,
|
||||
positions: [
|
||||
Cesium.Cartesian3.fromDegrees(fixes[i].lon, fixes[i].lat, 20),
|
||||
Cesium.Cartesian3.fromDegrees(
|
||||
geo.cartesian3.fromDegrees(fixes[i].lon, fixes[i].lat, 20),
|
||||
geo.cartesian3.fromDegrees(
|
||||
fixes[i + 1].lon,
|
||||
fixes[i + 1].lat,
|
||||
20,
|
||||
@@ -161,12 +161,12 @@ export function createTesting({ state, parts }) {
|
||||
/** Measure inside Cesium postRender with fixed typed storage; no sample objects per frame. */
|
||||
_measureTransitFramesForTest(durationMs = 12000) {
|
||||
stopProbe?.();
|
||||
const scene = state._viewer.scene;
|
||||
const probe = parts.renderer.frameProbe();
|
||||
const capacity = 20000,
|
||||
stride = 12,
|
||||
data = new Float64Array(capacity * stride);
|
||||
const previousPositions = new Float64Array(state._vehicles.size * 3);
|
||||
const gl = scene.context._gl;
|
||||
const gl = probe.gl;
|
||||
let active = false,
|
||||
upload = 0,
|
||||
cpu = 0,
|
||||
@@ -195,9 +195,9 @@ export function createTesting({ state, parts }) {
|
||||
gl[name] = original;
|
||||
});
|
||||
}
|
||||
for (const collection of [state._markers, state._animatedMarkers]) {
|
||||
const original = collection.update;
|
||||
collection.update = function (frame) {
|
||||
for (const { target, method } of probe.uploads) {
|
||||
const original = target[method];
|
||||
target[method] = function (frame) {
|
||||
active = true;
|
||||
try {
|
||||
return original.call(this, frame);
|
||||
@@ -206,11 +206,11 @@ export function createTesting({ state, parts }) {
|
||||
}
|
||||
};
|
||||
saved.push(() => {
|
||||
collection.update = original;
|
||||
target[method] = original;
|
||||
});
|
||||
}
|
||||
state._preRenderRemove?.();
|
||||
const removePre = scene.preRender.addEventListener(() => {
|
||||
const removePre = parts.renderer.onPreRender(() => {
|
||||
upload = 0;
|
||||
const start = performance.now();
|
||||
parts.rendering.onPreRender();
|
||||
@@ -224,7 +224,7 @@ export function createTesting({ state, parts }) {
|
||||
removePost?.();
|
||||
for (const restore of saved) restore();
|
||||
if (state._enabled)
|
||||
state._preRenderRemove = scene.preRender.addEventListener(
|
||||
state._preRenderRemove = parts.renderer.onPreRender(
|
||||
parts.rendering.onPreRender,
|
||||
);
|
||||
stopProbe = null;
|
||||
@@ -247,12 +247,12 @@ export function createTesting({ state, parts }) {
|
||||
resolve({
|
||||
rows,
|
||||
dpr: devicePixelRatio,
|
||||
width: scene.canvas.clientWidth,
|
||||
height: scene.canvas.clientHeight,
|
||||
width: probe.canvas.clientWidth,
|
||||
height: probe.canvas.clientHeight,
|
||||
});
|
||||
};
|
||||
stopProbe = finish;
|
||||
removePost = scene.postRender.addEventListener(() => {
|
||||
removePost = parts.renderer.onPostRender(() => {
|
||||
const now = performance.now();
|
||||
if (count >= capacity) return;
|
||||
const k = count++ * stride;
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
FIX_FLAGS,
|
||||
mergeHistory,
|
||||
} from '../../data/contactPlayback.js';
|
||||
import { createContactTrailRenderer } from '../../data/contactTrailRenderer.js';
|
||||
import { fixDistanceM } from './movement.js';
|
||||
import { transitStyleProfile } from '../../data/transitPresetStyle.js';
|
||||
|
||||
@@ -33,8 +32,8 @@ export function createTrails({ state, services, parts, source }) {
|
||||
fadeTimer = null,
|
||||
revision = 0;
|
||||
function ensureRenderer() {
|
||||
if (!renderer && state._viewer)
|
||||
renderer = createContactTrailRenderer(state._viewer.scene);
|
||||
if (!renderer && parts.renderer?.ready)
|
||||
renderer = parts.renderer.createTrailRenderer();
|
||||
return renderer;
|
||||
}
|
||||
function style() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
ACTIVATION_ENTER_ALTITUDE_M,
|
||||
@@ -18,23 +17,25 @@ import { transitFeedsInRange } from '../../data/transitFeeds.js';
|
||||
export function createViewport({ state, services, parts }) {
|
||||
const { governorRequestRender } = services.render;
|
||||
|
||||
function getCameraAltitude(viewer = state._viewer) {
|
||||
const carto = viewer?.camera?.positionCartographic;
|
||||
function getCameraAltitude() {
|
||||
const carto = parts.renderer?.cameraCartographic();
|
||||
return carto && Number.isFinite(carto.height) ? carto.height : Infinity;
|
||||
}
|
||||
|
||||
function getCameraCenterLatLon(viewer = state._viewer) {
|
||||
const rect = viewer?.camera?.computeViewRectangle?.(
|
||||
viewer.scene.globe?.ellipsoid,
|
||||
);
|
||||
function getCameraCenterLatLon() {
|
||||
const rect = parts.renderer?.viewRectangle();
|
||||
if (rect) {
|
||||
const center = Cesium.Rectangle.center(rect);
|
||||
const east = rect.east < rect.west ? rect.east + Math.PI * 2 : rect.east;
|
||||
const center = {
|
||||
latitude: (rect.south + rect.north) / 2,
|
||||
longitude: geo.math.negativePiToPi((rect.west + east) / 2),
|
||||
};
|
||||
return {
|
||||
lat: geo.math.toDegrees(center.latitude),
|
||||
lon: geo.math.toDegrees(center.longitude),
|
||||
};
|
||||
}
|
||||
const carto = viewer?.camera?.positionCartographic;
|
||||
const carto = parts.renderer?.cameraCartographic();
|
||||
if (carto) {
|
||||
return {
|
||||
lat: geo.math.toDegrees(carto.latitude),
|
||||
@@ -54,10 +55,7 @@ export function createViewport({ state, services, parts }) {
|
||||
* animating. Cheap, and only on a settled camera — never per frame.
|
||||
*/
|
||||
function refreshViewBounds() {
|
||||
const viewer = state._viewer;
|
||||
const rect = viewer?.camera?.computeViewRectangle?.(
|
||||
viewer.scene.globe?.ellipsoid,
|
||||
);
|
||||
const rect = parts.renderer?.viewRectangle();
|
||||
state._viewBounds = rect
|
||||
? inflateViewBounds({
|
||||
south: geo.math.toDegrees(rect.south),
|
||||
@@ -76,7 +74,7 @@ export function createViewport({ state, services, parts }) {
|
||||
}
|
||||
|
||||
function runProximityCheck() {
|
||||
if (!state._enabled || !state._viewer) return;
|
||||
if (!state._enabled || !parts.renderer?.ready) return;
|
||||
refreshViewBounds();
|
||||
const altitude = getCameraAltitude();
|
||||
state._altitudeGateOpen = altitudeGateOpen(altitude);
|
||||
|
||||
Reference in new issue
Block a user