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