Merge remote-tracking branch 'upstream/main' into fix/drone-airspace

# Conflicts:
#	src/i18n/locales/uk.json
This commit is contained in:
Matt Van Horn committed 2026-10-30 00:14:42 -07:00
commit 4a74420e7b
159 files changed
+18844 -1999

No files matched your search

+2
View File
@@ -47,6 +47,8 @@ export const prioritizeActiveCctvGeometryRecord =
export const processGeometryBatch = layer.processGeometryBatch;
export const hideCctvRecordVisuals = layer.hideCctvRecordVisuals;
export const refreshCoverageStyles = layer.refreshCoverageStyles;
export const refreshHorizonCulling = layer.refreshHorizonCulling;
export const _viewRecordIdsForTest = layer._viewRecordIdsForTest;
export const clearProbeClampOnDeactivation =
layer.clearProbeClampOnDeactivation;
export const cctvRecordNeedsActivation = layer.cctvRecordNeedsActivation;
+3 -2
View File
@@ -1309,11 +1309,12 @@ test('CCTV null-active coverage, auto-hop, cycling, and panel targets stay hones
assert.ok(renderer, '_renderCctvState is missing');
assert.match(
renderer[0],
/else if \(!activeId\)[\s\S]*?selectedIndex = -1/,
// Nothing active clears the pick; only the outside-view note may show.
/else if \(!activeId\)[\s\S]*?selectedIndex =[^;]*\? 0 : -1;/,
);
assert.match(
renderer[0],
/_cctvFocusBtn\.disabled\s*=\s*!enabled\s*\|\|\s*cameras\.length === 0\s*\|\|\s*!activeId/,
/_cctvFocusBtn\.disabled\s*=\s*!enabled\s*\|\|\s*catalogCount === 0\s*\|\|\s*!activeId/,
);
} finally {
cctvLayer.setParams({ autoHop: false });
+251
View File
@@ -0,0 +1,251 @@
// src/data/cctvViewThinning.test.mjs — the CCTV view pass: only cameras on or
// near the screen show, a zoomed-out view keeps one camera per world-anchored
// cell, and the layer's UI state never rebuilds every camera for a listener
// that does not read the list.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import cctvLayer, {
_setCctvCoverageStateForTest,
_viewRecordIdsForTest,
refreshHorizonCulling,
} from './cctv.js';
const WIDTH = 800;
const HEIGHT = 600;
/**
* Records at the given [lon, lat] degrees near the origin, each drawn at the
* screen point `screenAt` gives it (default: the middle of the screen).
*/
function recordsAt(
lonLats,
screenAt = () => ({ x: WIDTH / 2, y: HEIGHT / 2 }),
) {
const screenByPosition = new Map();
const records = lonLats.map(([lon, lat], index) => {
const position = Cesium.Cartesian3.fromDegrees(lon, lat, 10);
screenByPosition.set(position, screenAt(index));
return {
camera: {
id: `cam-${index}`,
name: `Camera ${index}`,
city: 'Test',
lat,
lon,
headingDeg: 0,
pitchDeg: -18,
fovDeg: 44,
rangeM: 145,
mountHeightM: 8,
},
position,
billboard: { position, show: true },
coverageEntities: [],
projection: null,
};
});
return { records, screenByPosition };
}
function viewerAt(heightM, screenByPosition, shiftX = 0) {
return {
isDestroyed: () => false,
camera: {
positionWC: Cesium.Cartesian3.fromDegrees(0, 0, heightM),
positionCartographic: { latitude: 0, longitude: 0, height: heightM },
frustum: { fovy: Math.PI / 3 },
},
scene: {
canvas: { clientWidth: WIDTH, clientHeight: HEIGHT },
cartesianToCanvasCoordinates: (position) => {
const screen = screenByPosition.get(position);
return screen && { x: screen.x + shiftX, y: screen.y };
},
},
};
}
function shownIds(records) {
return records
.filter((record) => record.billboard.show)
.map((record) => record.camera.id)
.sort();
}
// Three cameras within ~110 m, and one ~22 km away.
const CLUSTER = [
[0, 0],
[0.001, 0],
[0.0005, 0.0005],
[0.2, 0],
];
test('zoomed out, cameras sharing a ground cell show one icon', () => {
const { records, screenByPosition } = recordsAt(CLUSTER);
_setCctvCoverageStateForTest({
viewer: viewerAt(20_000, screenByPosition),
records,
});
assert.equal(
refreshHorizonCulling(),
true,
'the first pass changes the view',
);
const shown = shownIds(records);
assert.equal(shown.length, 2, 'one for the cluster, one for the far camera');
assert.ok(shown.includes('cam-3'), 'a camera alone in its cell shows');
assert.deepEqual(_viewRecordIdsForTest().sort(), shown);
});
test('a pan keeps the same winners, and an unchanged view keeps its list', () => {
const { records, screenByPosition } = recordsAt(CLUSTER, (index) => ({
x: 100 + index * 7,
y: 300,
}));
_setCctvCoverageStateForTest({
viewer: viewerAt(20_000, screenByPosition),
records,
});
refreshHorizonCulling();
const before = shownIds(records);
const list = _viewRecordIdsForTest();
for (const shiftX of [3, 11, 20, 37]) {
_setCctvCoverageStateForTest({
viewer: viewerAt(20_000, screenByPosition, shiftX),
records,
});
refreshHorizonCulling();
assert.deepEqual(shownIds(records), before, `pan by ${shiftX} px`);
}
assert.equal(refreshHorizonCulling(), false, 'the same view changes nothing');
assert.deepEqual(_viewRecordIdsForTest(), list);
});
test('a small zoom within one cell size keeps the same winners', () => {
const { records, screenByPosition } = recordsAt(CLUSTER);
const shownAt = (heightM) => {
_setCctvCoverageStateForTest({
viewer: viewerAt(heightM, screenByPosition),
records,
});
refreshHorizonCulling();
return shownIds(records);
};
// Cells come in power-of-two metre steps: about 1.5 km and 1.6 km cells at
// these heights fall in the same step, so the same cameras win.
assert.deepEqual(shownAt(20_500), shownAt(20_000));
});
test('a healthy camera wins its cell over an unknown one', () => {
const { records, screenByPosition } = recordsAt(CLUSTER.slice(0, 2));
// Whichever camera the id order would pick, the healthy one must win.
for (const id of ['cam-0', 'cam-1']) {
_setCctvCoverageStateForTest({
viewer: viewerAt(20_000, screenByPosition),
records,
healthById: new Map([[id, { status: 'ok' }]]),
});
refreshHorizonCulling();
assert.deepEqual(shownIds(records), [id]);
}
});
test('the active camera always shows, even in a crowded cell', () => {
const { records, screenByPosition } = recordsAt(CLUSTER.slice(0, 3));
for (const active of records) {
_setCctvCoverageStateForTest({
viewer: viewerAt(20_000, screenByPosition),
records,
activeCameraId: active.camera.id,
});
refreshHorizonCulling();
assert.ok(active.billboard.show, `${active.camera.id} shows while active`);
}
});
test('close up, every camera near the screen shows and far-off ones hide', () => {
const { records, screenByPosition } = recordsAt(
[
[0, 0],
[0.0001, 0],
[0.0002, 0],
],
(index) =>
index === 2 ? { x: WIDTH * 3, y: HEIGHT / 2 } : { x: 100, y: 100 },
);
_setCctvCoverageStateForTest({
viewer: viewerAt(300, screenByPosition),
records,
});
refreshHorizonCulling();
assert.deepEqual(shownIds(records), ['cam-0', 'cam-1']);
});
test('a disabled layer skips the view pass', () => {
const { records, screenByPosition } = recordsAt(CLUSTER);
_setCctvCoverageStateForTest({
viewer: viewerAt(20_000, screenByPosition),
records,
enabled: false,
});
assert.equal(refreshHorizonCulling(), false);
assert.deepEqual(_viewRecordIdsForTest(), []);
});
test('UI state builds the full camera list only when it is read', () => {
const { records, screenByPosition } = recordsAt(CLUSTER.slice(0, 2));
_setCctvCoverageStateForTest({
viewer: viewerAt(300, screenByPosition),
records,
});
refreshHorizonCulling();
let built = 0;
for (const record of records) {
const camera = record.camera;
record.camera = new Proxy(camera, {
get(target, key) {
if (key === 'calibration') built += 1;
return target[key];
},
});
}
const first = cctvLayer.getUIState();
assert.equal(built, 0, 'listeners that skip `cameras` cost nothing');
assert.deepEqual(
first.cameraIndex.map((camera) => camera.id),
['cam-0', 'cam-1'],
);
assert.equal(
cctvLayer.getUIState().cameraIndex,
first.cameraIndex,
'the index keeps its identity while the view does',
);
assert.equal(first.cameras.length, 2);
assert.ok(built > 0, 'reading `cameras` builds it');
assert.equal(first.cameras, first.cameras, 'and builds it once');
});
test('the camera picker lists the view, with the active camera first', () => {
const { records, screenByPosition } = recordsAt(
[
[0, 0],
[0.0001, 0],
[0.0002, 0],
],
(index) => (index === 1 ? { x: WIDTH * 3, y: 0 } : { x: 100, y: 100 }),
);
_setCctvCoverageStateForTest({
viewer: viewerAt(300, screenByPosition),
records,
activeCameraId: 'cam-1',
});
refreshHorizonCulling();
const state = cctvLayer.getUIState();
assert.deepEqual(
state.cameraIndex.map((camera) => camera.id),
['cam-1', 'cam-0', 'cam-2'],
'the off-screen active camera leads, then what is in view',
);
assert.equal(state.cameras.length, 3, 'the full list is still every camera');
});
+29
View File
@@ -875,6 +875,13 @@ test('route numbers follow the page language and never break from their unit', (
);
const list = directionsStepList({ route });
assert.equal(list.items[1].lead, '240\u00a0м');
// Japanese unit words follow the number: 1時間5分, but 1.5 km.
setLocale('ja');
assert.equal(routeDistanceText(1500), '1.5\u00a0km');
assert.equal(routeDurationText(39), '39秒');
assert.equal(routeDurationText(3900), '1時間5分');
assert.equal(routeDurationText(7200), '2時間');
});
test('ramp and lane wording stays localized after choosing the maneuver form', (t) => {
@@ -892,6 +899,12 @@ test('ramp and lane wording stays localized after choosing the maneuver form', (
'Toma la salida bruscamente a la derecha',
'Toma el carril izquierdo',
],
[
'ja',
'ランプを斜め左に進んでI-95に入る',
'出口を急角度で右に出る',
'左車線を通行',
],
[
'zh-CN',
'向左前方驶入匝道,进入 I-95',
@@ -919,3 +932,19 @@ test('ramp and lane wording stays localized after choosing the maneuver form', (
);
}
});
test('the reroute tooltip keeps German mode nouns capitalized', (t) => {
t.after(() => setLocale('en'));
const title = (id) =>
directionsRowControls(idle).chips.find((chip) => chip.id === id).title;
assert.equal(title('mode-foot'), 'Walk — reroute for walk');
setLocale('de');
assert.equal(
title('mode-foot'),
'Zu Fuß – Route im Modus „Zu Fuß“ neu berechnen',
);
assert.equal(
title('mode-bike'),
'Fahrrad – Route im Modus „Fahrrad“ neu berechnen',
);
});
+1 -1
View File
@@ -20,7 +20,7 @@
* jam keeps a size bump so queues read as fat beads.
* - `crt` (retro): saturated primaries that survive 10-level posterization
* plus an all-bucket size boost to out-shout the pixel grid.
* - `normal` (normal/anime/snow/unknown): every lookup returns null/0 —
* - `normal` (normal/anime/unknown): every lookup returns null/0 —
* the shipped palette and sizing apply untouched.
*
* Sim/uncovered dots (bucket null/'sim') are NEVER restyled under any
+1 -1
View File
@@ -13,7 +13,7 @@ const luma = ([r, g, b]) => (0.299 * r + 0.587 * g + 0.114 * b) / 255;
const BUCKETS = ['free', 'slow', 'jam'];
const MONO_STYLES = ['surveillance', 'thermal', 'noir'];
const NORMAL_STYLES = ['normal', 'anime', 'snow', 'does-not-exist', '', undefined, null];
const NORMAL_STYLES = ['normal', 'anime', 'does-not-exist', '', undefined, null];
test('profile mapping: NVG/FLIR/noir are mono, retro is crt, everything else normal', () => {
for (const s of MONO_STYLES) assert.equal(trafficStyleProfile(s), 'mono', s);
+1 -1
View File
@@ -18,7 +18,7 @@
* black sprite with a light ring is what a hot object looks like there.
* - `crt` (retro): saturated per-mode colours that survive posterisation,
* a size boost to out-shout the pixel grid, and a thinner halo.
* - `normal` (normal/anime/snow/unknown): every lookup returns the shipped
* - `normal` (normal/anime/unknown): every lookup returns the shipped
* value — mode colour, shipped size, hairline edge only.
*
* Brackets are a different matter. The detection canvas composites ABOVE the
-1
View File
@@ -17,7 +17,6 @@ test('profile mapping: NVG/FLIR/noir and cockpit nvg are mono, retro is crt, eve
for (const plain of [
'normal',
'anime',
'snow',
'minecraft',
'',
null,