feat(cctv): seven camera packs, semantic labels, terrain-clear monitor planes (#433)

Seven contributed CCTV camera packs (Ontario 511, Fintraffic Finland, DriveBC, TxDOT Texas, Estonia Tallinn + Tarktee, Warendorf, Live Traffic NSW) on a fair per-pack catalog cap; semantic camera labels; monitor planes lifted clear of the terrain from a precomputed, bundled ground-height dataset. Default catalog 800 → 3,446 cameras. Full context in PR #433.

Co-authored-by: sendmebits <[email protected]>
Co-authored-by: avocadoAgent <[email protected]>
Co-authored-by: Alex Harvey <[email protected]>
Co-authored-by: Jason Meredith <[email protected]>
Co-authored-by: Daniel Smith <[email protected]>
Co-authored-by: Vladimir Sobolev <[email protected]>
Co-authored-by: Matthias Schneider <[email protected]>
Co-authored-by: Cyber Knight <[email protected]>
Co-authored-by: NovaSoft Australia <[email protected]>
This commit is contained in:
51 files changed
+78682 -939

No files matched your search

+1
View File
@@ -27,6 +27,7 @@ const layer = createCctvLayer({
},
});
export const calibrationPatchMovesAnchor = layer.calibrationPatchMovesAnchor;
export const migrateRangeScaleForFloor = layer.migrateRangeScaleForFloor;
export const _pushAmbientCardEntriesForTest =
layer._pushAmbientCardEntriesForTest;
export const _setCctvOverlayHostForTest = layer._setCctvOverlayHostForTest;
+697 -185
View File
File diff suppressed because it is too large. Load diff
+137
View File
@@ -0,0 +1,137 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
allocateSourceCap,
resolveCatalogCap,
} from '../../server/providers/cctv/cap.js';
import {
CCTV_MAX_SOURCES_CEILING,
DEFAULT_CCTV_MAX_SOURCES,
} from '../../server/providers/cctv/constants.js';
import { createCctvCatalog } from '../../server/providers/cctv/catalog.js';
const pack = (name, count, prefix = name) => ({
name,
sources: Array.from({ length: count }, (_, i) => ({
id: `${prefix}-${i}`,
rank: i,
})),
});
test('under the cap every pack is served whole, in merge order', () => {
const { sources, packs } = allocateSourceCap(
[pack('a', 3), pack('b', 2)],
10,
);
assert.deepEqual(
sources.map((s) => s.id),
['a-0', 'a-1', 'a-2', 'b-0', 'b-1'],
);
assert.deepEqual(packs, [
{ name: 'a', offered: 3, kept: 3 },
{ name: 'b', offered: 2, kept: 2 },
]);
});
test('over the cap the packs share it round-robin, and the last pack is never starved', () => {
// The old positional slice kept the first N of [a..., b..., c...], which
// deleted pack c entirely; the fair split thins every pack instead.
const { sources, packs } = allocateSourceCap(
[pack('a', 100), pack('b', 100), pack('c', 100)],
9,
);
assert.equal(sources.length, 9);
assert.deepEqual(
packs.map((p) => p.kept),
[3, 3, 3],
);
// Each pack contributes its own highest-priority cameras, in its own order.
assert.deepEqual(
sources.filter((s) => s.id.startsWith('c-')).map((s) => s.rank),
[0, 1, 2],
);
});
test('a duplicate inside one pack takes one slot, not two', () => {
const dup = {
name: 'file',
sources: [{ id: 'x', v: 'old' }, { id: 'x', v: 'new' }, { id: 'y' }],
};
const { sources } = allocateSourceCap([dup], 2);
assert.deepEqual(
sources.map((s) => [s.id, s.v]),
[
['x', 'new'],
['y', undefined],
],
);
});
test('a short pack releases its turns to the others', () => {
const { packs } = allocateSourceCap([pack('a', 2), pack('b', 50)], 10);
assert.deepEqual(
packs.map((p) => p.kept),
[2, 8],
);
});
test('duplicate ids resolve last-pack-wins and count against the winning pack', () => {
const live = {
name: 'live',
sources: [{ id: 'cam-1', label: 'live' }, { id: 'cam-2' }],
};
const override = {
name: 'env',
sources: [{ id: 'cam-1', label: 'override' }],
};
const { sources, packs } = allocateSourceCap([live, override], 10);
assert.deepEqual(
sources.map((s) => [s.id, s.label]),
[
['cam-2', undefined],
['cam-1', 'override'],
],
);
assert.deepEqual(packs, [
{ name: 'live', offered: 1, kept: 1 },
{ name: 'env', offered: 1, kept: 1 },
]);
});
test('resolveCatalogCap bounds the env value and falls back to the default', () => {
assert.equal(resolveCatalogCap(undefined), DEFAULT_CCTV_MAX_SOURCES);
assert.equal(resolveCatalogCap('nonsense'), DEFAULT_CCTV_MAX_SOURCES);
assert.equal(resolveCatalogCap('1'), 8);
assert.equal(resolveCatalogCap('999999'), CCTV_MAX_SOURCES_CEILING);
assert.equal(resolveCatalogCap('120.9'), 120);
});
test('the catalog applies the fair cap to configured packs end to end', async () => {
const saved = { ...process.env };
try {
process.env.CCTV_PREFER_AUSTIN = '0';
process.env.CCTV_SOURCES_FILE = '/nonexistent/cctv.json';
process.env.CCTV_SOURCES_JSON = JSON.stringify(
Array.from({ length: 12 }, (_, i) => ({
id: `env-${i}`,
name: `Env ${i}`,
lat: 30 + i * 0.01,
lon: -97,
url: `https://example.test/${i}.jpg`,
})),
);
process.env.CCTV_MAX_SOURCES = '9';
const getSources = createCctvCatalog({ sourceRoot: '/nonexistent' });
const sources = await getSources();
assert.equal(sources.length, 9);
assert.deepEqual(
sources.slice(0, 3).map((s) => s.id),
['env-0', 'env-1', 'env-2'],
);
} finally {
for (const key of Object.keys(process.env)) {
if (!(key in saved)) delete process.env[key];
}
Object.assign(process.env, saved);
}
});
+61
View File
@@ -0,0 +1,61 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
CAMERA_CODE_MAX_CHARS,
cameraDisplayCode,
normalizeSourceItem,
} from '../../server/providers/cctv/normalize.js';
test('cameraDisplayCode trims long names with an ellipsis and collapses whitespace', () => {
assert.equal(
cameraDisplayCode(' IH 35 at Cesar Chavez '),
'IH 35 at Cesar Chavez',
);
const long = 'City of Windsor – Goyeau Street and Park Street';
const code = cameraDisplayCode(long);
assert.equal(code.length, CAMERA_CODE_MAX_CHARS);
assert.ok(code.endsWith('…'));
assert.equal(cameraDisplayCode(''), '');
});
test('every pack labels by its feed name; explicit codes win; the id is the last resort', () => {
const base = { lat: 1, lon: 1, url: 'https://x.test/a.jpg' };
assert.equal(
normalizeSourceItem({
...base,
id: '354',
name: '5TH ST / CONGRESS AVE',
sourceKind: 'austin-open-data',
}).code,
'5TH ST / CONGRESS AVE',
);
assert.equal(
normalizeSourceItem({
...base,
id: 'on-860',
name: 'Goyeau Street and Park Street',
sourceKind: 'ontario-511-open-data',
}).code,
'GOYEAU STREET AND PARK STRE…',
);
assert.equal(
normalizeSourceItem({
...base,
id: 'nsw-46fa631a',
name: 'Erskine Street looking west',
sourceKind: 'nsw-livetraffic',
code: 'ERSKINE ST (SYDNEY)',
}).code,
'ERSKINE ST (SYDNEY)',
);
assert.equal(
normalizeSourceItem({
...base,
id: 'fi-c1455101',
name: '',
sourceKind: 'fintraffic-open-data',
}).code,
'fi-c1455101',
'a missing name falls back to the id',
);
});
+272
View File
@@ -0,0 +1,272 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createCctvCatalog } from '../../server/providers/cctv/catalog.js';
import {
DEFAULT_AUSTIN_MAX_SOURCES,
DEFAULT_CALTRANS_MAX_SOURCES,
DEFAULT_CCTV_MAX_SOURCES,
CCTV_MAX_SOURCES_CEILING,
DEFAULT_DRIVEBC_MAX_SOURCES,
DEFAULT_TFL_MAX_SOURCES,
DEFAULT_ONTARIO_MAX_SOURCES,
DEFAULT_FINTRAFFIC_MAX_SOURCES,
DEFAULT_TXDOT_MAX_SOURCES,
DEFAULT_TALLINN_MAX_SOURCES,
DEFAULT_TARKTEE_MAX_SOURCES,
DEFAULT_NSW_MAX_SOURCES,
DRIVEBC_WEBCAMS_URL,
} from '../../server/providers/cctv/constants.js';
import {
driveBcImageCredit,
loadDriveBcSourcesFromOpenData,
} from '../../server/providers/cctv/sources.js';
/** Set (or, for `undefined`, delete) environment variables for one test. */
function withEnv(t, env) {
for (const [name, value] of Object.entries(env)) {
const previous = process.env[name];
t.after(() => {
if (previous === undefined) delete process.env[name];
else process.env[name] = previous;
});
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
}
/** Silence the loaders' progress and failure logging. */
function quiet(t) {
t.mock.method(console, 'log', () => {});
t.mock.method(console, 'warn', () => {});
}
/** A DriveBC `/api/webcams/` row carrying the fields the loader reads. */
function driveBcRow(id, lat, lon, overrides = {}) {
return {
id,
name: `Camera ${id}`,
is_on: true,
should_appear: true,
region_name: 'Lower Mainland',
orientation: 'N',
elevation: 12,
location: { type: 'Point', coordinates: [lon, lat] },
links: { imageDisplay: `/images/${id}.jpg?t=1` },
...overrides,
};
}
test('DriveBC loader keeps published cameras and builds frame URLs from the camera id', async (t) => {
quiet(t);
withEnv(t, { CCTV_DRIVEBC_MAX_SOURCES: undefined });
const requested = [];
t.mock.method(globalThis, 'fetch', async (url) => {
requested.push(String(url));
return Response.json([
driveBcRow(13, 49.001268, -122.756658, {
name: 'Douglas (Peace Arch) Border Crossing',
region_name: 'Border Cams',
elevation: 10,
// The payload's own link is ignored; frames come from the pinned host.
links: { imageDisplay: 'https://example.com/13.jpg' },
}),
driveBcRow(682, 49.0579, -123.0569, {
orientation: 'se',
elevation: null,
credit:
'Images courtesy of <a href="https://www.translink.ca/" target="_blank">TransLink</a>',
}),
driveBcRow(1, 49.2, -123.1, { is_on: false }),
driveBcRow(2, 49.2, -123.1, { should_appear: false }),
driveBcRow('3', 49.2, -123.1),
driveBcRow(4, null, -123.1),
driveBcRow(5, 49.2, -123.1, { location: null }),
driveBcRow(6, 1000, -123.1),
driveBcRow(7, 34.05, -118.24),
]);
});
const cameras = await loadDriveBcSourcesFromOpenData();
assert.deepEqual(requested, [DRIVEBC_WEBCAMS_URL]);
// Nearest to the Vancouver anchor first: the pack is always distance-sorted
// so the catalog-wide cap thins it from the far end.
assert.deepEqual(
cameras.map((camera) => camera.id),
['drivebc-682', 'drivebc-13'],
);
const border = cameras.find((camera) => camera.id === 'drivebc-13');
const deltaport = cameras.find((camera) => camera.id === 'drivebc-682');
assert.equal(border.url, 'https://www.drivebc.ca/images/13.jpg');
assert.equal(border.snapshotUrl, border.url);
assert.equal(border.name, 'Douglas (Peace Arch) Border Crossing');
assert.equal(border.city, 'BC Border');
assert.equal(border.provider, 'DriveBC');
assert.equal(border.feedType, 'image');
assert.equal(border.headingDeg, 0);
assert.equal(border.headingConfidence, 'high');
assert.equal(border.groundElevationM, 10);
assert.match(border.license, /Open Government Licence – British Columbia/);
assert.equal(
border.credit,
'',
'a provincial camera carries no partner credit',
);
assert.equal(
deltaport.credit,
'Images courtesy of TransLink',
'a partner-supplied camera names its owner, HTML stripped',
);
assert.equal(deltaport.city, 'Lower Mainland');
assert.equal(
deltaport.headingDeg,
135,
'orientation codes are case-insensitive',
);
assert.equal(
deltaport.groundElevationM,
0,
'a missing elevation falls back to sea level',
);
});
test('DriveBC loader keeps the cameras nearest Vancouver and Victoria under its cap', async (t) => {
quiet(t);
withEnv(t, { CCTV_DRIVEBC_MAX_SOURCES: '8' });
const rows = [
// Fort Nelson rows come first in payload order, so only distance can drop them.
...[58.8, 58.81, 58.82, 58.83].map((lat, i) =>
driveBcRow(100 + i, lat, -122.7),
),
...[49.28, 49.27, 49.26, 49.25].map((lat, i) =>
driveBcRow(200 + i, lat, -123.12),
),
...[48.43, 48.44, 48.45, 48.46].map((lat, i) =>
driveBcRow(300 + i, lat, -123.37),
),
];
t.mock.method(globalThis, 'fetch', async () => Response.json(rows));
const cameras = await loadDriveBcSourcesFromOpenData();
assert.equal(cameras.length, 8);
assert.deepEqual(
cameras
.map((camera) => camera.id)
.filter((id) => /^drivebc-10\d$/.test(id)),
[],
'the northern cameras are the ones dropped',
);
});
test('DriveBC loader fails soft on HTTP errors, unexpected payloads and network errors', async (t) => {
quiet(t);
let respond;
t.mock.method(globalThis, 'fetch', (...args) => respond(...args));
for (respond of [
async () => new Response('unavailable', { status: 503 }),
async () => Response.json({ webcams: [] }),
async () => {
throw new TypeError('fetch failed');
},
]) {
assert.deepEqual(await loadDriveBcSourcesFromOpenData(), []);
}
});
test('CCTV catalog merges DriveBC cameras and CCTV_DRIVEBC_ENABLED=0 skips the request', async (t) => {
quiet(t);
const sourceRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'gev-cctv-catalog-'),
);
t.after(() => fs.rmSync(sourceRoot, { recursive: true, force: true }));
withEnv(t, {
CCTV_SOURCES_FILE: undefined,
CCTV_SOURCES_JSON: undefined,
CCTV_FORCE_AUSTIN: undefined,
CCTV_PREFER_AUSTIN: undefined,
CCTV_MAX_SOURCES: undefined,
CCTV_CALTRANS_DISTRICTS: undefined,
CCTV_TFL_ENABLED: undefined,
CCTV_DRIVEBC_ENABLED: undefined,
CCTV_DRIVEBC_MAX_SOURCES: undefined,
});
const requested = [];
t.mock.method(globalThis, 'fetch', async (url) => {
requested.push(String(url));
// Every other live pack is down; only DriveBC answers.
return String(url) === DRIVEBC_WEBCAMS_URL
? Response.json([driveBcRow(13, 49.001268, -122.756658)])
: new Response('unavailable', { status: 503 });
});
const sources = await createCctvCatalog({ sourceRoot })();
assert.deepEqual(
sources.map((source) => source.id),
['drivebc-13'],
);
assert.equal(sources[0].url, 'https://www.drivebc.ca/images/13.jpg');
process.env.CCTV_DRIVEBC_ENABLED = '0';
requested.length = 0;
assert.deepEqual(await createCctvCatalog({ sourceRoot })(), []);
assert.ok(requested.length > 0, 'the other live packs still load');
assert.equal(
requested.includes(DRIVEBC_WEBCAMS_URL),
false,
'a disabled pack makes no request',
);
});
test('default per-pack camera caps fit inside the default catalog cap', () => {
// Every default pack at its own cap (Warendorf ships 1 curated camera).
const packs =
DEFAULT_AUSTIN_MAX_SOURCES +
DEFAULT_CALTRANS_MAX_SOURCES +
DEFAULT_TFL_MAX_SOURCES +
DEFAULT_ONTARIO_MAX_SOURCES +
DEFAULT_FINTRAFFIC_MAX_SOURCES +
DEFAULT_DRIVEBC_MAX_SOURCES +
DEFAULT_TXDOT_MAX_SOURCES +
DEFAULT_TALLINN_MAX_SOURCES +
DEFAULT_TARKTEE_MAX_SOURCES +
DEFAULT_NSW_MAX_SOURCES +
1;
assert.ok(
packs <= DEFAULT_CCTV_MAX_SOURCES,
`default packs (${packs}) would be thinned by the catalog cap (${DEFAULT_CCTV_MAX_SOURCES})`,
);
assert.ok(
DEFAULT_CCTV_MAX_SOURCES <= CCTV_MAX_SOURCES_CEILING,
'the default cap must sit inside the CCTV_MAX_SOURCES ceiling',
);
});
test('DriveBC credit keeps partner attribution and drops operational notes', () => {
assert.equal(
driveBcImageCredit(
'Images courtesy of <a href="https://x.test/">TransLink</a>',
),
'Images courtesy of TransLink',
);
assert.equal(
driveBcImageCredit('Camera image provided by City of Vancouver'),
'Camera image provided by City of Vancouver',
);
assert.equal(
driveBcImageCredit(
'BC HighwayCam presented in cooperation with AtkinsRéalis.',
),
'BC HighwayCam presented in cooperation with AtkinsRéalis.',
);
assert.equal(
driveBcImageCredit(
'This camera is located in a remote area and relies on solar power. As a result, transmission delays may occur.',
),
'',
);
assert.equal(driveBcImageCredit(null), '');
});
+86
View File
@@ -0,0 +1,86 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
loadTallinnSourcesFromCatalog,
parseTarkteeDatexImages,
parseTarkteeDatexLocations,
} from '../../server/providers/cctv/sources.js';
const SAMPLE_LOCATIONS_XML = `<?xml version="1.0" encoding="UTF-8"?>
<d2LogicalModel xmlns="http://datex2.eu/schema/2/2_0">
<payloadPublication>
<predefinedLocationContainer id="TRAFFIC_CAMERAS" version="0">
<predefinedLocation id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" version="0">
<predefinedLocationName>
<values><value lang="et">Test Site</value></values>
</predefinedLocationName>
<location>
<pointByCoordinates>
<pointCoordinates>
<latitude>59.4370</latitude>
<longitude>24.7530</longitude>
</pointCoordinates>
</pointByCoordinates>
</location>
</predefinedLocation>
</predefinedLocationContainer>
</payloadPublication>
</d2LogicalModel>`;
const SAMPLE_IMAGES_XML = `<?xml version="1.0" encoding="UTF-8"?>
<d2LogicalModel xmlns="http://datex2.eu/schema/2/2_0">
<payloadPublication>
<trafficView id="1">
<linearTrafficView id="1">
<linearPredefinedLocationReference targetClass="PredefinedLocation" id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" version="0"/>
<trafficViewRecord id="1-1">
<urlLink>
<urlLinkAddress>https://tarktee.transpordiamet.ee/images/42/42_202608251542.jpg</urlLinkAddress>
<urlLinkType>image</urlLinkType>
</urlLink>
</trafficViewRecord>
</linearTrafficView>
</trafficView>
</payloadPublication>
</d2LogicalModel>`;
test('Tallinn catalog loads curated ristmikud stills only', () => {
const cameras = loadTallinnSourcesFromCatalog();
assert.ok(
cameras.length >= 200,
`expected a full Tallinn pack, got ${cameras.length}`,
);
for (const camera of cameras.slice(0, 25)) {
assert.match(camera.id, /^tln-/);
assert.equal(camera.cityId, 'tallinn');
assert.equal(camera.feedType, 'image');
assert.equal(camera.sourceKind, 'tallinn-ristmikud');
assert.ok(camera.url.startsWith('https://ristmikud.tallinn.ee/'));
assert.equal(camera.url, camera.snapshotUrl);
assert.ok(Number.isFinite(camera.lat) && Number.isFinite(camera.lon));
}
});
test('Tarktee DATEX parsers join location UUIDs to image URLs', () => {
const locations = parseTarkteeDatexLocations(SAMPLE_LOCATIONS_XML);
const images = parseTarkteeDatexImages(SAMPLE_IMAGES_XML);
assert.equal(locations.size, 1);
assert.equal(images.size, 1);
const id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
assert.equal(locations.get(id)?.name, 'Test Site');
assert.equal(locations.get(id)?.lat, 59.437);
assert.equal(locations.get(id)?.lon, 24.753);
assert.equal(
images.get(id),
'https://tarktee.transpordiamet.ee/images/42/42_202608251542.jpg',
);
});
test('Tarktee DATEX image parser rejects non-official hosts', () => {
const xml = SAMPLE_IMAGES_XML.replace(
'https://tarktee.transpordiamet.ee/images/42/42_202608251542.jpg',
'https://evil.example/images/42/42.jpg',
);
const images = parseTarkteeDatexImages(xml);
assert.equal(images.size, 0);
});
+256
View File
@@ -0,0 +1,256 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { loadFintrafficSourcesFromOpenData } from '../../server/providers/cctv/sources.js';
import {
fintrafficCameraName,
isLikelyFinlandCoordinate,
} from '../../server/providers/cctv/normalize.js';
import {
FINTRAFFIC_GROUND_ELEVATION_M,
FINTRAFFIC_IMAGE_ORIGIN,
FINTRAFFIC_STATIONS_URL,
} from '../../server/providers/cctv/constants.js';
/** One station feature in the shape tie.digitraffic.fi actually returns. */
function station(
id,
{
name = `vt4_${id}`,
status = 'GATHERING',
lon = 24.9384,
lat = 60.1699,
altitude = 0,
presets = [],
} = {},
) {
return {
type: 'Feature',
id,
geometry: { type: 'Point', coordinates: [lon, lat, altitude] },
properties: {
id,
name,
collectionStatus: status,
dataUpdatedTime: '2026-09-13T03:25:36Z',
presets,
state: null,
},
};
}
/** Run the loader against a canned station list, restoring global fetch after. */
async function loadWith(features, { env = {}, capture = {} } = {}) {
const originalFetch = globalThis.fetch;
const originalEnv = {};
for (const [key, value] of Object.entries(env)) {
originalEnv[key] = process.env[key];
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
globalThis.fetch = async (url, options) => {
capture.url = String(url);
capture.options = options;
return new Response(
JSON.stringify({
type: 'FeatureCollection',
dataUpdatedTime: '2026-09-13T05:31:58Z',
features,
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
},
);
};
try {
return await loadFintrafficSourcesFromOpenData();
} finally {
globalThis.fetch = originalFetch;
for (const [key, value] of Object.entries(originalEnv)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
}
test('Fintraffic loader turns every in-collection preset into one camera', async () => {
const cameras = await loadWith([
station('C01503', {
name: 'kt51_Inkoo',
lon: 23.99616,
lat: 60.05374,
presets: [
{ id: 'C0150301', inCollection: true },
{ id: 'C0150302', inCollection: true },
],
}),
]);
assert.equal(cameras.length, 2);
const [first, second] = cameras;
assert.equal(first.id, 'fi-c0150301');
assert.equal(first.name, 'kt51 Inkoo (view 01)');
assert.equal(second.name, 'kt51 Inkoo (view 02)');
assert.equal(first.provider, 'Fintraffic');
assert.equal(first.city, 'Finland');
assert.equal(first.cityId, 'finland');
assert.equal(first.sourceKind, 'fintraffic-open-data');
assert.equal(first.license, 'Fintraffic / digitraffic.fi (CC BY 4.0)');
assert.equal(first.feedType, 'image');
// Presets of one station share the station position...
assert.equal(first.lat, 60.05374);
assert.equal(second.lat, first.lat);
assert.equal(second.lon, first.lon);
// ...so the id-hash fallback heading is what keeps their gizmos apart.
assert.notEqual(first.headingDeg, second.headingDeg);
assert.equal(first.headingConfidence, 'low');
});
test('Fintraffic frame URLs are built on the official image origin', async () => {
const cameras = await loadWith([
station('C01503', { presets: [{ id: 'C0150301', inCollection: true }] }),
]);
assert.equal(cameras[0].url, `${FINTRAFFIC_IMAGE_ORIGIN}C0150301.jpg`);
assert.equal(cameras[0].snapshotUrl, cameras[0].url);
assert.ok(cameras[0].url.startsWith('https://weathercam.digitraffic.fi/'));
});
test('Fintraffic loader drops dead stations, uncollected presets and bad ids', async () => {
const cameras = await loadWith([
// Station off collection: every preset goes, live or not.
station('C01601', {
status: 'REMOVED_TEMPORARILY',
presets: [{ id: 'C0160101', inCollection: true }],
}),
station('C01503', {
presets: [
{ id: 'C0150301', inCollection: true },
{ id: 'C0150302', inCollection: false },
{ id: 'C0150303' },
// Ids that would escape the synthesized frame path, or belong to
// another station, never become a camera.
{ id: '../../etc/passwd', inCollection: true },
{ id: 'C9999901', inCollection: true },
],
}),
// Coordinates nowhere near Finland (swapped lat/lon).
station('C01504', {
lon: 60.1699,
lat: 24.9384,
presets: [{ id: 'C0150401', inCollection: true }],
}),
]);
assert.deepEqual(
cameras.map((camera) => camera.id),
['fi-c0150301'],
);
});
test('Fintraffic ground elevation uses a reported altitude and falls back otherwise', async () => {
const cameras = await loadWith([
station('C08508', {
altitude: 136,
presets: [{ id: 'C0850801', inCollection: true }],
}),
station('C01503', {
altitude: 0,
presets: [{ id: 'C0150301', inCollection: true }],
}),
station('C03506', {
altitude: 99_000,
presets: [{ id: 'C0350601', inCollection: true }],
}),
]);
const byId = new Map(cameras.map((camera) => [camera.id, camera]));
assert.equal(byId.get('fi-c0850801').groundElevationM, 136);
// 0 is "not reported", not sea level.
assert.equal(
byId.get('fi-c0150301').groundElevationM,
FINTRAFFIC_GROUND_ELEVATION_M,
);
// A garbage upstream value cannot fling a camera kilometres up.
assert.equal(byId.get('fi-c0350601').groundElevationM, 1400);
});
test('Fintraffic loader identifies itself to Digitraffic and refuses redirects', async () => {
const capture = {};
await loadWith(
[station('C01503', { presets: [{ id: 'C0150301', inCollection: true }] })],
{ capture },
);
assert.equal(capture.url, FINTRAFFIC_STATIONS_URL);
assert.equal(capture.options.headers['Digitraffic-User'], 'gods-eye-view');
assert.equal(capture.options.headers['Accept-Encoding'], 'gzip');
assert.equal(capture.options.redirect, 'manual');
assert.ok(capture.options.signal instanceof AbortSignal);
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(null, {
status: 302,
headers: { Location: 'https://example.com/stations' },
});
try {
assert.deepEqual(await loadFintrafficSourcesFromOpenData(), []);
} finally {
globalThis.fetch = originalFetch;
}
});
test('Fintraffic pack caps itself and fails to an empty list, never a throw', async () => {
const presets = Array.from({ length: 40 }, (_, index) => ({
id: `C01503${String(index).padStart(2, '0')}`,
inCollection: true,
}));
const capped = await loadWith([station('C01503', { presets })], {
env: { CCTV_FINTRAFFIC_MAX_SOURCES: '8' },
});
assert.equal(capped.length, 8);
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {
throw new Error('network down');
};
try {
assert.deepEqual(await loadFintrafficSourcesFromOpenData(), []);
} finally {
globalThis.fetch = originalFetch;
}
globalThis.fetch = async () => new Response('nope', { status: 503 });
try {
assert.deepEqual(await loadFintrafficSourcesFromOpenData(), []);
} finally {
globalThis.fetch = originalFetch;
}
});
test('fintrafficCameraName unpacks the machine station name and the view number', () => {
assert.equal(
fintrafficCameraName('vt3_Hyvinkää_Noppo', 'C01234', 'C0123402'),
'vt3 Hyvinkää Noppo (view 02)',
);
// Nameless station still gets a usable label.
assert.equal(
fintrafficCameraName('', 'C01234', 'C0123409'),
'Fintraffic C01234 (view 09)',
);
// A preset id that is just the station id has no view suffix to add.
assert.equal(
fintrafficCameraName('vt4_Mäntsälä', 'C01234', 'C01234'),
'vt4 Mäntsälä',
);
});
test('isLikelyFinlandCoordinate spans the catalog extent and rejects the rest', () => {
assert.equal(isLikelyFinlandCoordinate(60.1699, 24.9384), true); // Helsinki
assert.equal(isLikelyFinlandCoordinate(70.088512, 27.0), true); // Utsjoki, the northern extreme
assert.equal(isLikelyFinlandCoordinate(59.856454, 19.618722), true); // the southwestern extreme
assert.equal(isLikelyFinlandCoordinate(51.5074, -0.1278), false); // London
assert.equal(isLikelyFinlandCoordinate(0, 0), false); // null island
assert.equal(isLikelyFinlandCoordinate(NaN, 24.9384), false);
});
+198
View File
@@ -0,0 +1,198 @@
/**
* Pure monitor-plane footprint math shared by the client geometry
* (src/layers/cctv/geometry.js), the server sidecar join
* (server/providers/cctv/groundHeights.js) and the offline precompute
* (scripts/precompute-cctv-heights.mjs). Lives outside every ownership
* package on purpose. No Cesium, no DOM: the same function
* decides where a plane's support points are on the ground for both.
*
* The plane is the pitched far cap of the camera frustum: center at range R
* along the heading (horizontal R·cos(pitch)), half-width R·tan(hFov/2),
* half-height R·tan(vFov/2) with vFov from a 16:9 aspect, tilted by the
* pitch. Support points are a 3×3 grid over that rectangle: rows bottom /
* middle / top, columns left / center / right, named `bl bm br ml mc mr tl tm
* tr`. Their horizontal positions depend on the pose only, never on ground
* height, so an offline job can sample the ground under them for the nominal
* pose and the client can reuse those samples as long as the pose is the same.
*/
const EARTH_RADIUS_M = 6371000;
export const PLANE_VERT_ASPECT = 16 / 9;
export const SUPPORT_KEYS = Object.freeze([
'bl',
'bm',
'br',
'ml',
'mc',
'mr',
'tl',
'tm',
'tr',
]);
const toRad = (deg) => (deg * Math.PI) / 180;
const toDeg = (rad) => (rad * 180) / Math.PI;
/**
* Offset a lat/lon by a distance along a compass heading (spherical earth;
* sub-centimetre at the ≤ 5 km ranges cameras use).
* @param {number} lat @param {number} lon @param {number} headingDeg @param {number} distM
* @returns {{lat:number, lon:number}}
*/
export function projectPoint(lat, lon, headingDeg, distM) {
const b = toRad(headingDeg);
const la = toRad(lat);
const lo = toRad(lon);
const ad = distM / EARTH_RADIUS_M;
const la2 = Math.asin(
Math.sin(la) * Math.cos(ad) + Math.cos(la) * Math.sin(ad) * Math.cos(b),
);
const lo2 =
lo +
Math.atan2(
Math.sin(b) * Math.sin(ad) * Math.cos(la),
Math.cos(ad) - Math.sin(la) * Math.sin(la2),
);
return { lat: toDeg(la2), lon: toDeg(lo2) };
}
/**
* Plane dimensions and offsets for a pose, before any ground is known.
* @param {{pitchDeg:number, fovDeg:number, rangeM:number}} pose
* @returns {{R:number, pitch:number, halfW:number, halfH:number, horiz:number,
* vert:number, upVert:number, upHoriz:number, vFovDeg:number}}
*/
export function planeDimensions(pose) {
const R = Math.max(1, Number(pose.rangeM) || 1);
const pitch = toRad(Math.max(-89, Math.min(89, Number(pose.pitchDeg) || 0)));
const hFov = toRad(Math.max(8, Math.min(160, Number(pose.fovDeg) || 74)));
const halfW = R * Math.tan(hFov / 2);
const vFovRad = 2 * Math.atan(Math.tan(hFov / 2) / PLANE_VERT_ASPECT);
const halfH = R * Math.tan(vFovRad / 2);
return {
R,
pitch,
halfW,
halfH,
horiz: R * Math.cos(pitch),
vert: R * Math.sin(pitch),
// In-plane "up", decomposed into a vertical part and a horizontal part
// along the heading (a downward pitch tilts the plane's top forward).
upVert: Math.cos(pitch) * halfH,
upHoriz: -Math.sin(pitch) * halfH,
vFovDeg: toDeg(vFovRad),
};
}
/**
* Horizontal (lat/lon) positions of the mount and the nine plane support
* points for a pose. Altitudes are not part of this: the caller adds the
* ground it measured under each point.
* @param {{lat:number, lon:number, headingDeg:number, pitchDeg:number,
* fovDeg:number, rangeM:number}} pose
* @returns {{mount:{lat:number,lon:number}, capCenter:{lat:number,lon:number},
* supports: Record<string,{lat:number,lon:number, row:-1|0|1, col:-1|0|1}>}}
*/
export function planeSupportPoints(pose) {
const dims = planeDimensions(pose);
const heading = Number(pose.headingDeg) || 0;
const cap = projectPoint(pose.lat, pose.lon, heading, dims.horiz);
const supports = {};
const rows = { b: -1, m: 0, t: 1 };
// Column letter: l / r, with the centre spelled `m` in the bottom and top
// rows (bm, tm) and `c` in the middle row (mc).
const cols = { l: -1, m: 0, c: 0, r: 1 };
for (const key of SUPPORT_KEYS) {
const row = rows[key[0]];
const col = cols[key[1]];
const across = projectPoint(
cap.lat,
cap.lon,
heading + 90,
col * dims.halfW,
);
const along = projectPoint(
across.lat,
across.lon,
heading,
row * dims.upHoriz,
);
supports[key] = { lat: along.lat, lon: along.lon, row, col };
}
return { mount: { lat: pose.lat, lon: pose.lon }, capCenter: cap, supports };
}
/**
* Altitude of the plane at a support point, given the plane center altitude:
* the rigid rectangle's rows sit ± upVert around the center.
* @param {number} capAltM @param {-1|0|1} row @param {{upVert:number}} dims
* @returns {number}
*/
export function supportAltitude(capAltM, row, dims) {
return capAltM + row * dims.upVert;
}
/**
* The single rigid lift (metres, ≥ 0) that puts every support point at least
* `clearanceM` above the ground measured under it. Support points without a
* measured ground fall back to `fallbackGroundM` (normally the ground at the
* mount), which is what makes the pre-precompute path deterministic instead
* of buried.
* @param {number} capAltM - Unlifted plane center altitude.
* @param {{upVert:number}} dims
* @param {Record<string, number>|null|undefined} groundUnder - support key → ground altitude.
* @param {number} fallbackGroundM
* @param {number} clearanceM
* @returns {{liftM:number, limitingKey:string|null}}
*/
export function requiredPlaneLift(
capAltM,
dims,
groundUnder,
fallbackGroundM,
clearanceM,
) {
let liftM = 0;
let limitingKey = null;
const rows = { b: -1, m: 0, t: 1 };
for (const key of SUPPORT_KEYS) {
const raw = groundUnder ? groundUnder[key] : undefined;
// Strict: a null/absent support must fall back, not read as 0 m.
const ground =
typeof raw === 'number' && Number.isFinite(raw) ? raw : fallbackGroundM;
if (!Number.isFinite(ground)) continue;
const alt = supportAltitude(capAltM, rows[key[0]], dims);
const deficit = ground + clearanceM - alt;
if (deficit > liftM) {
liftM = deficit;
limitingKey = key;
}
}
return { liftM, limitingKey };
}
/**
* Stable hash of the nominal pose a footprint was sampled for, so shipped
* samples are only reused while the pose they describe is unchanged.
* @param {{lat:number, lon:number, headingDeg:number, pitchDeg:number,
* fovDeg:number, rangeM:number, mountHeightM:number}} pose
* @returns {string}
*/
export function poseHash(pose) {
const text = [
Number(pose.lat).toFixed(6),
Number(pose.lon).toFixed(6),
Number(pose.headingDeg).toFixed(1),
Number(pose.pitchDeg).toFixed(1),
Number(pose.fovDeg).toFixed(1),
Number(pose.rangeM).toFixed(1),
Number(pose.mountHeightM).toFixed(1),
].join('|');
// FNV-1a 32-bit, base36: short, stable, dependency-free.
let h = 0x811c9dc5;
for (let i = 0; i < text.length; i += 1) {
h ^= text.charCodeAt(i);
h = Math.imul(h, 0x01000193) >>> 0;
}
return `p1-${h.toString(36)}`;
}
+83
View File
@@ -0,0 +1,83 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
SUPPORT_KEYS,
planeDimensions,
planeSupportPoints,
poseHash,
projectPoint,
requiredPlaneLift,
} from './cctvFootprint.js';
const pose = {
lat: 30.2672,
lon: -97.7431,
headingDeg: 90,
pitchDeg: -24,
fovDeg: 56,
rangeM: 210,
mountHeightM: 12,
};
test('support points form a 3×3 grid across the pitched far cap', () => {
const { mount, capCenter, supports } = planeSupportPoints(pose);
assert.deepEqual(mount, { lat: pose.lat, lon: pose.lon });
assert.deepEqual(Object.keys(supports).sort(), [...SUPPORT_KEYS].sort());
const dims = planeDimensions(pose);
// Cap center sits R·cos(pitch) east of the mount (heading 90).
const east = projectPoint(pose.lat, pose.lon, 90, dims.horiz);
assert.ok(
Math.abs(capCenter.lat - east.lat) < 1e-9 &&
Math.abs(capCenter.lon - east.lon) < 1e-9,
);
// Middle-center is the cap center; left/right columns straddle it across the heading.
assert.ok(Math.abs(supports.mc.lat - capCenter.lat) < 1e-9);
assert.ok(
supports.ml.lat > capCenter.lat && supports.mr.lat < capCenter.lat,
'left is north of centre when looking east',
);
// A downward pitch tilts the top row forward (further along the heading).
assert.ok(
supports.tm.lon > supports.mc.lon && supports.bm.lon < supports.mc.lon,
);
});
test('the rigid lift is the largest clearance deficit over the supports', () => {
const dims = planeDimensions(pose);
const ground = 149;
const capAlt = ground + pose.mountHeightM + dims.vert; // far below ground for -24°
const flat = requiredPlaneLift(capAlt, dims, null, ground, 2);
assert.ok(
Math.abs(flat.liftM - (ground + 2 - (capAlt - dims.upVert))) < 1e-9,
);
assert.equal(flat.limitingKey[0], 'b');
const bump = requiredPlaneLift(capAlt, dims, { tr: ground + 200 }, ground, 2);
assert.equal(bump.limitingKey, 'tr');
assert.ok(bump.liftM > flat.liftM);
// Already-clear planes need no lift at all.
assert.deepEqual(requiredPlaneLift(ground + 500, dims, null, ground, 2), {
liftM: 0,
limitingKey: null,
});
// Unusable measurements fall back to the mount ground rather than zero.
assert.equal(
requiredPlaneLift(capAlt, dims, { bl: null, br: 'x' }, ground, 2).liftM,
flat.liftM,
);
});
test('poseHash is stable for the same pose and changes with any pose field', () => {
const h = poseHash(pose);
assert.equal(poseHash({ ...pose }), h);
assert.match(h, /^p1-[0-9a-z]+$/);
for (const [key, delta] of [
['lat', 0.00001],
['headingDeg', 0.1],
['pitchDeg', 0.1],
['fovDeg', 0.1],
['rangeM', 1],
['mountHeightM', 0.1],
]) {
assert.notEqual(poseHash({ ...pose, [key]: pose[key] + delta }), h, key);
}
});
+81
View File
@@ -0,0 +1,81 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
joinGroundHeights,
loadGroundHeights,
} from '../../server/providers/cctv/groundHeights.js';
import { poseHash } from './cctvFootprint.js';
const source = (over = {}) => ({
id: 'cam-1',
lat: 30.2672,
lon: -97.7431,
headingDeg: 90,
pitchDeg: -24,
fovDeg: 56,
rangeM: 210,
mountHeightM: 12,
...over,
});
test('a shipped entry attaches only while the nominal pose still matches', () => {
const s = source();
const entry = {
status: 'ok',
poseHash: poseHash(s),
mountGroundM: 151.2,
supports: { bl: 150.1, bm: 149.9, br: 155.5, tl: null, tm: 'x' },
};
const [joined] = joinGroundHeights([s], { 'cam-1': entry });
assert.deepEqual(joined.groundHeights, {
poseHash: entry.poseHash,
mountGroundM: 151.2,
supports: { bl: 150.1, bm: 149.9, br: 155.5 },
});
const moved = source({ lat: 30.2673 });
const [notJoined] = joinGroundHeights([moved], { 'cam-1': entry });
assert.equal(
notJoined.groundHeights,
undefined,
'a moved camera loses the shipped value',
);
const [missed] = joinGroundHeights([source()], {
'cam-1': { ...entry, status: 'miss', mountGroundM: null },
});
assert.equal(missed.groundHeights, undefined, 'a miss ships nothing');
assert.deepEqual(
joinGroundHeights([source()], null)[0].groundHeights,
undefined,
);
});
test('the sidecar loads from the source root and tolerates a missing or malformed file', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-heights-'));
fs.mkdirSync(
path.join(dir, 'src', 'data', 'local_data', 'cctv_ground_heights'),
{
recursive: true,
},
);
assert.deepEqual(loadGroundHeights(dir), {}, 'missing file');
const file = path.join(
dir,
'src',
'data',
'local_data',
'cctv_ground_heights',
'cctv_ground_heights.json',
);
fs.writeFileSync(file, '{not json');
assert.deepEqual(loadGroundHeights(dir), {}, 'malformed file');
fs.writeFileSync(
file,
JSON.stringify({ schemaVersion: 1, cameras: { a: { status: 'ok' } } }),
);
assert.deepEqual(loadGroundHeights(dir), { a: { status: 'ok' } });
});
+122 -47
View File
@@ -31,6 +31,13 @@ const PROVIDER_STATIC_REFRESH_MS = Object.freeze({
'austin transportation & public works': 5 * 60 * 1000,
'transport for london': 3 * 60 * 1000,
caltrans: 3 * 60 * 1000,
'ontario 511': 3 * 60 * 1000,
// TxDOT publishes roughly once a minute; 3 min matches the other highway packs.
txdot: 3 * 60 * 1000,
// Digitraffic publishes a new weathercam frame on each station's
// collectionInterval, 600 s for every station sampled; polling faster only
// re-fetches the same JPEG.
fintraffic: 10 * 60 * 1000,
});
/**
@@ -42,7 +49,9 @@ const PROVIDER_STATIC_REFRESH_MS = Object.freeze({
* @returns {{cardLimit:number}}
*/
export function cctvLodBudgets(cameraHeightM) {
const height = Number.isFinite(cameraHeightM) ? Math.max(0, cameraHeightM) : CITY_HEIGHT_M;
const height = Number.isFinite(cameraHeightM)
? Math.max(0, cameraHeightM)
: CITY_HEIGHT_M;
if (height <= STREET_HEIGHT_M) {
return { cardLimit: CCTV_AMBIENT_CARD_MIN };
}
@@ -68,7 +77,11 @@ export const CCTV_CARD_INCUMBENT_FACTOR = 0.8;
* @param {number} [factor]
* @returns {number}
*/
export function incumbentRankKm(distanceKm, isIncumbent, factor = CCTV_CARD_INCUMBENT_FACTOR) {
export function incumbentRankKm(
distanceKm,
isIncumbent,
factor = CCTV_CARD_INCUMBENT_FACTOR,
) {
const km = Number.isFinite(distanceKm) ? Math.max(0, distanceKm) : Infinity;
return isIncumbent ? km * factor : km;
}
@@ -81,8 +94,9 @@ export const CCTV_CARD_SPREAD_PERCENTILE = 0.9;
/** True only when both viewport dimensions are finite positive pixels. */
export function hasFiniteCctvViewport(viewW, viewH) {
return Number.isFinite(viewW) && viewW > 0
&& Number.isFinite(viewH) && viewH > 0;
return (
Number.isFinite(viewW) && viewW > 0 && Number.isFinite(viewH) && viewH > 0
);
}
/**
@@ -117,7 +131,10 @@ export function cctvCandidateSpreadKm(
.filter((distanceKm) => Number.isFinite(distanceKm) && distanceKm >= 0)
.sort((a, b) => a - b);
if (!finite.length) return 0;
const p = Math.min(1, Math.max(0, Number.isFinite(percentile) ? percentile : 0));
const p = Math.min(
1,
Math.max(0, Number.isFinite(percentile) ? percentile : 0),
);
return finite[Math.round(p * (finite.length - 1))];
}
@@ -176,32 +193,47 @@ export const CCTV_CARD_GRID_ROWS = 4;
* @returns {string[]} Winner ids in priority order (cell winners, then
* global-rank fill).
*/
export function distributeCctvCards(candidates, {
budget = CCTV_AMBIENT_CARD_MAX,
viewW = 0,
viewH = 0,
cols = CCTV_CARD_GRID_COLS,
rows = CCTV_CARD_GRID_ROWS,
} = {}) {
export function distributeCctvCards(
candidates,
{
budget = CCTV_AMBIENT_CARD_MAX,
viewW = 0,
viewH = 0,
cols = CCTV_CARD_GRID_COLS,
rows = CCTV_CARD_GRID_ROWS,
} = {},
) {
const cap = Number.isFinite(budget) ? Math.max(0, Math.floor(budget)) : 0;
const valid = (Array.isArray(candidates) ? candidates : [])
.filter((c) => c && typeof c.id === 'string' && c.id
&& Number.isFinite(c.sx) && Number.isFinite(c.sy));
const valid = (Array.isArray(candidates) ? candidates : []).filter(
(c) =>
c &&
typeof c.id === 'string' &&
c.id &&
Number.isFinite(c.sx) &&
Number.isFinite(c.sy),
);
if (!cap || !valid.length) return [];
const validViewport = hasFiniteCctvViewport(viewW, viewH);
const width = validViewport ? viewW : 1;
const height = validViewport ? viewH : 1;
const nCols = Math.max(1, Math.floor(cols));
const nRows = Math.max(1, Math.floor(rows));
const byRank = (a, b) => (Number.isFinite(a.rankKm) ? a.rankKm : Infinity)
- (Number.isFinite(b.rankKm) ? b.rankKm : Infinity)
|| a.id.localeCompare(b.id);
const byRank = (a, b) =>
(Number.isFinite(a.rankKm) ? a.rankKm : Infinity) -
(Number.isFinite(b.rankKm) ? b.rankKm : Infinity) ||
a.id.localeCompare(b.id);
/** @type {Map<number, Array>} occupied cell -> its candidates */
const cells = new Map();
for (const candidate of valid) {
const col = Math.min(nCols - 1, Math.max(0, Math.floor((candidate.sx / width) * nCols)));
const row = Math.min(nRows - 1, Math.max(0, Math.floor((candidate.sy / height) * nRows)));
const col = Math.min(
nCols - 1,
Math.max(0, Math.floor((candidate.sx / width) * nCols)),
);
const row = Math.min(
nRows - 1,
Math.max(0, Math.floor((candidate.sy / height) * nRows)),
);
const key = row * nCols + col;
const bucket = cells.get(key);
if (bucket) bucket.push(candidate);
@@ -249,29 +281,42 @@ export function distributeCctvCards(candidates, {
* @param {number} [options.viewH] - Viewport height (CSS px).
* @returns {{cardIds:string[],budgets:{cardLimit:number}}}
*/
export function selectCctvLod(candidates, { cameraHeightM, incumbentIds, viewW, viewH } = {}) {
export function selectCctvLod(
candidates,
{ cameraHeightM, incumbentIds, viewW, viewH } = {},
) {
const budgets = cctvLodBudgets(cameraHeightM);
const incumbents = incumbentIds instanceof Set ? incumbentIds : new Set(incumbentIds || []);
const incumbents =
incumbentIds instanceof Set ? incumbentIds : new Set(incumbentIds || []);
const screened = hasFiniteCctvViewport(viewW, viewH);
if (!screened) {
// Preserve the pre-refinement nearest-first path byte-for-behavior,
// including its acceptance of any non-empty string ID. The stricter
// valid-viewport rules below must not leak into this compatibility branch.
const inView = (Array.isArray(candidates) ? candidates : [])
.filter((candidate) => candidate && typeof candidate.id === 'string' && candidate.id)
.filter(
(candidate) =>
candidate && typeof candidate.id === 'string' && candidate.id,
)
.map((candidate) => ({
id: candidate.id,
distanceKm: Number.isFinite(candidate.distanceKm) ? Math.max(0, candidate.distanceKm) : Infinity,
rankKm: incumbentRankKm(candidate.distanceKm, incumbents.has(candidate.id)),
distanceKm: Number.isFinite(candidate.distanceKm)
? Math.max(0, candidate.distanceKm)
: Infinity,
rankKm: incumbentRankKm(
candidate.distanceKm,
incumbents.has(candidate.id),
),
inView: candidate.inView === true,
isVideo: candidate.isVideo === true,
}))
.filter((candidate) => candidate.inView)
.sort((a, b) => (
a.rankKm - b.rankKm
|| a.distanceKm - b.distanceKm
|| a.id.localeCompare(b.id)
));
.sort(
(a, b) =>
a.rankKm - b.rankKm ||
a.distanceKm - b.distanceKm ||
a.id.localeCompare(b.id),
);
const cardIds = [];
const seen = new Set();
for (const candidate of inView) {
@@ -284,9 +329,15 @@ export function selectCctvLod(candidates, { cameraHeightM, incumbentIds, viewW,
}
const eligibleById = new Map();
for (const candidate of Array.isArray(candidates) ? candidates : []) {
if (!candidate || typeof candidate.id !== 'string' || !candidate.id.trim()
|| candidate.inView !== true || candidate.isVideo === true
|| !Number.isFinite(candidate.distanceKm)) continue;
if (
!candidate ||
typeof candidate.id !== 'string' ||
!candidate.id.trim() ||
candidate.inView !== true ||
candidate.isVideo === true ||
!Number.isFinite(candidate.distanceKm)
)
continue;
const normalized = {
id: candidate.id,
distanceKm: Math.max(0, candidate.distanceKm),
@@ -295,7 +346,14 @@ export function selectCctvLod(candidates, { cameraHeightM, incumbentIds, viewW,
sy: Number.isFinite(candidate.sy) ? candidate.sy : NaN,
};
const current = eligibleById.get(normalized.id);
if (!current || compareCctvRepresentative(normalized, current, { screened, viewW, viewH }) < 0) {
if (
!current ||
compareCctvRepresentative(normalized, current, {
screened,
viewW,
viewH,
}) < 0
) {
eligibleById.set(normalized.id, normalized);
}
}
@@ -304,7 +362,9 @@ export function selectCctvLod(candidates, { cameraHeightM, incumbentIds, viewW,
// robust distance scale or consume a bounded ambient-card slot.
const stills = [...eligibleById.values()];
const spreadKm = cctvCandidateSpreadKm(stills.map((candidate) => candidate.distanceKm));
const spreadKm = cctvCandidateSpreadKm(
stills.map((candidate) => candidate.distanceKm),
);
for (const candidate of stills) {
const blendedKm = blendCenterRankKm(
candidate.distanceKm,
@@ -313,17 +373,22 @@ export function selectCctvLod(candidates, { cameraHeightM, incumbentIds, viewW,
);
candidate.rankKm = incumbentRankKm(blendedKm, incumbents.has(candidate.id));
}
stills.sort((a, b) => (
a.rankKm - b.rankKm
|| a.distanceKm - b.distanceKm
|| a.id.localeCompare(b.id)
));
stills.sort(
(a, b) =>
a.rankKm - b.rankKm ||
a.distanceKm - b.distanceKm ||
a.id.localeCompare(b.id),
);
let cardIds;
// Item C: screen-distributed fill. Candidates lacking screen anchors are
// dropped by the distribution pass; top up from the ranked pool
// (defensive — cctv.js always projects anchors for in-view candidates).
cardIds = distributeCctvCards(stills, { budget: budgets.cardLimit, viewW, viewH });
cardIds = distributeCctvCards(stills, {
budget: budgets.cardLimit,
viewW,
viewH,
});
if (cardIds.length < budgets.cardLimit) {
const chosen = new Set(cardIds);
for (const candidate of stills) {
@@ -336,10 +401,12 @@ export function selectCctvLod(candidates, { cameraHeightM, incumbentIds, viewW,
}
function compareCctvRepresentative(a, b, { screened, viewW, viewH }) {
if (a.distanceKm !== b.distanceKm) return a.distanceKm < b.distanceKm ? -1 : 1;
if (a.distanceKm !== b.distanceKm)
return a.distanceKm < b.distanceKm ? -1 : 1;
if (screened) {
const centerDelta = screenCenterFraction(a.sx, a.sy, viewW, viewH)
- screenCenterFraction(b.sx, b.sy, viewW, viewH);
const centerDelta =
screenCenterFraction(a.sx, a.sy, viewW, viewH) -
screenCenterFraction(b.sx, b.sy, viewW, viewH);
if (centerDelta) return centerDelta;
}
const ax = Number.isFinite(a.sx) ? a.sx : Infinity;
@@ -414,7 +481,10 @@ export function applyEvictionGrace({
// Under cap pressure the grace-period cards go first, oldest-in-grace
// first (they have had the longest chance to return); ties break on more
// misses, then id for determinism.
graced.sort((a, b) => a.since - b.since || b.misses - a.misses || a.id.localeCompare(b.id));
graced.sort(
(a, b) =>
a.since - b.since || b.misses - a.misses || a.id.localeCompare(b.id),
);
const capacity = Math.max(0, cardLimit - keepIds.length);
const overflow = Math.max(0, graced.length - capacity);
for (let i = 0; i < graced.length; i++) {
@@ -423,7 +493,10 @@ export function applyEvictionGrace({
continue;
}
keepIds.push(graced[i].id);
nextGrace.set(graced[i].id, { misses: graced[i].misses, since: graced[i].since });
nextGrace.set(graced[i].id, {
misses: graced[i].misses,
since: graced[i].since,
});
}
return { keepIds, evictIds, graceState: nextGrace };
@@ -442,6 +515,8 @@ export function staticFrameRefreshMs(camera) {
if (Number.isFinite(explicit) && explicit > 0) {
return Math.max(60_000, Math.min(20 * 60 * 1000, Math.round(explicit)));
}
const provider = String(camera?.provider || '').trim().toLowerCase();
const provider = String(camera?.provider || '')
.trim()
.toLowerCase();
return PROVIDER_STATIC_REFRESH_MS[provider] || DEFAULT_STATIC_REFRESH_MS;
}
+215 -74
View File
@@ -41,7 +41,9 @@ test('cctvLodBudgets scales the card budget from 20 to 40 with view height', ()
assert.equal(CCTV_AMBIENT_CARD_MAX, 40);
assert.deepEqual(cctvLodBudgets(500), { cardLimit: CCTV_AMBIENT_CARD_MIN });
assert.deepEqual(cctvLodBudgets(8_000), { cardLimit: CCTV_AMBIENT_CARD_MID });
assert.deepEqual(cctvLodBudgets(30_000), { cardLimit: CCTV_AMBIENT_CARD_MAX });
assert.deepEqual(cctvLodBudgets(30_000), {
cardLimit: CCTV_AMBIENT_CARD_MAX,
});
});
test('selectCctvLod caps ambient cards at the 40 hard cap regardless of catalog size', () => {
@@ -54,19 +56,30 @@ test('selectCctvLod uses only in-view cameras and orders by viewer distance', ()
const input = candidates(20, { hiddenAt: 0 }).reverse();
const selected = selectCctvLod(input, { cameraHeightM: 500 });
assert.equal(selected.cardIds.includes('cam-000'), false);
assert.deepEqual(selected.cardIds.slice(0, 3), ['cam-001', 'cam-002', 'cam-003']);
assert.deepEqual(selected.cardIds.slice(0, 3), [
'cam-001',
'cam-002',
'cam-003',
]);
});
test('video cameras never consume ambient card slots', () => {
const selected = selectCctvLod(candidates(30, { videoAt: 0 }), { cameraHeightM: 500 });
const selected = selectCctvLod(candidates(30, { videoAt: 0 }), {
cameraHeightM: 500,
});
assert.equal(selected.cardIds.includes('cam-000'), false);
assert.equal(selected.cardIds.length, CCTV_AMBIENT_CARD_MIN);
});
test('selectCctvLod tolerates malformed candidate rows', () => {
const selected = selectCctvLod(
[null, {}, { id: '', inView: true }, { id: 'cam-ok', inView: true, distanceKm: NaN }],
{ cameraHeightM: 500 }
[
null,
{},
{ id: '', inView: true },
{ id: 'cam-ok', inView: true, distanceKm: NaN },
],
{ cameraHeightM: 500 },
);
assert.deepEqual(selected.cardIds, ['cam-ok']);
});
@@ -84,7 +97,9 @@ test('selectCctvLod: a carded camera outranks a slightly-nearer non-carded one',
// incumbent at 5.0 km holds its card against a newcomer at 4.5 km (only
// 10% closer).
const anchors = Array.from({ length: CCTV_AMBIENT_CARD_MIN - 1 }, (_, i) => ({
id: `anchor-${String(i).padStart(2, '0')}`, distanceKm: i / 100, inView: true,
id: `anchor-${String(i).padStart(2, '0')}`,
distanceKm: i / 100,
inView: true,
}));
const contested = [
{ id: 'incumbent', distanceKm: 5.0, inView: true },
@@ -101,7 +116,9 @@ test('selectCctvLod: a carded camera outranks a slightly-nearer non-carded one',
test('selectCctvLod: a meaningfully (>20%) closer camera still displaces an incumbent', () => {
const anchors = Array.from({ length: CCTV_AMBIENT_CARD_MIN - 1 }, (_, i) => ({
id: `anchor-${String(i).padStart(2, '0')}`, distanceKm: i / 100, inView: true,
id: `anchor-${String(i).padStart(2, '0')}`,
distanceKm: i / 100,
inView: true,
}));
const contested = [
{ id: 'incumbent', distanceKm: 5.0, inView: true },
@@ -111,13 +128,19 @@ test('selectCctvLod: a meaningfully (>20%) closer camera still displaces an incu
cameraHeightM: 500,
incumbentIds: new Set(['incumbent']),
});
assert.ok(selected.cardIds.includes('newcomer'), 'meaningfully closer camera wins');
assert.ok(
selected.cardIds.includes('newcomer'),
'meaningfully closer camera wins',
);
assert.equal(selected.cardIds.includes('incumbent'), false);
});
test('selectCctvLod: no incumbents means plain nearest-first (unchanged behavior)', () => {
const plain = selectCctvLod(candidates(20), { cameraHeightM: 500 });
const withEmpty = selectCctvLod(candidates(20), { cameraHeightM: 500, incumbentIds: new Set() });
const withEmpty = selectCctvLod(candidates(20), {
cameraHeightM: 500,
incumbentIds: new Set(),
});
assert.deepEqual(plain.cardIds, withEmpty.cardIds);
});
@@ -129,25 +152,39 @@ test('distributeCctvCards spreads clustered candidates across cells', () => {
// sits alone bottom-right. Nearest-first would take the whole cluster —
// distribution gives every occupied cell its best first.
const clustered = Array.from({ length: 4 }, (_, i) => ({
id: `center-${i}`, sx: 50 + i * 10, sy: 50, rankKm: i,
id: `center-${i}`,
sx: 50 + i * 10,
sy: 50,
rankKm: i,
}));
const lonely = { id: 'periphery', sx: 950, sy: 750, rankKm: 30 };
assert.deepEqual(
distributeCctvCards([...clustered, lonely], { budget: 2, viewW: 1000, viewH: 800 }),
['center-0', 'periphery']
distributeCctvCards([...clustered, lonely], {
budget: 2,
viewW: 1000,
viewH: 800,
}),
['center-0', 'periphery'],
);
});
test('distributeCctvCards: leftover budget falls back to global rank order', () => {
const clustered = Array.from({ length: 4 }, (_, i) => ({
id: `center-${i}`, sx: 50 + i * 10, sy: 50, rankKm: i,
id: `center-${i}`,
sx: 50 + i * 10,
sy: 50,
rankKm: i,
}));
const lonely = { id: 'periphery', sx: 950, sy: 750, rankKm: 30 };
// 2 occupied cells, budget 4: both cell winners, then the cluster's next
// two by rank.
assert.deepEqual(
distributeCctvCards([...clustered, lonely], { budget: 4, viewW: 1000, viewH: 800 }),
['center-0', 'periphery', 'center-1', 'center-2']
distributeCctvCards([...clustered, lonely], {
budget: 4,
viewW: 1000,
viewH: 800,
}),
['center-0', 'periphery', 'center-1', 'center-2'],
);
});
@@ -155,64 +192,101 @@ test('distributeCctvCards: empty cells are skipped, budget respected', () => {
// Everything in ONE cell — the 19 empty cells claim nothing; the single
// occupied cell fills the whole budget in rank order.
const oneCell = Array.from({ length: 5 }, (_, i) => ({
id: `c-${i}`, sx: 20 + i * 5, sy: 20, rankKm: i,
id: `c-${i}`,
sx: 20 + i * 5,
sy: 20,
rankKm: i,
}));
const ids = distributeCctvCards(oneCell, { budget: 3, viewW: 1000, viewH: 800 });
const ids = distributeCctvCards(oneCell, {
budget: 3,
viewW: 1000,
viewH: 800,
});
assert.deepEqual(ids, ['c-0', 'c-1', 'c-2']);
});
test('distributeCctvCards: an under-budget cut keeps the nearest cell winners', () => {
const ids = distributeCctvCards([
{ id: 'far-cell', sx: 900, sy: 700, rankKm: 9 },
{ id: 'near-cell', sx: 100, sy: 100, rankKm: 1 },
], { budget: 1, viewW: 1000, viewH: 800 });
const ids = distributeCctvCards(
[
{ id: 'far-cell', sx: 900, sy: 700, rankKm: 9 },
{ id: 'near-cell', sx: 100, sy: 100, rankKm: 1 },
],
{ budget: 1, viewW: 1000, viewH: 800 },
);
assert.deepEqual(ids, ['near-cell']);
});
test('distributeCctvCards: incumbency (effective distance) is honored inside a cell', () => {
// Same cell: the incumbent at 5.0 km (discounted to 4.0) beats the
// non-carded newcomer at 4.5 km for the cell slot.
const ids = distributeCctvCards([
{ id: 'newcomer', sx: 100, sy: 100, rankKm: incumbentRankKm(4.5, false) },
{ id: 'incumbent', sx: 120, sy: 100, rankKm: incumbentRankKm(5.0, true) },
], { budget: 1, viewW: 1000, viewH: 800 });
const ids = distributeCctvCards(
[
{ id: 'newcomer', sx: 100, sy: 100, rankKm: incumbentRankKm(4.5, false) },
{ id: 'incumbent', sx: 120, sy: 100, rankKm: incumbentRankKm(5.0, true) },
],
{ budget: 1, viewW: 1000, viewH: 800 },
);
assert.deepEqual(ids, ['incumbent']);
});
test('distributeCctvCards: malformed rows dropped; offscreen anchors clamp to edge cells', () => {
const ids = distributeCctvCards([
null,
{ id: '', sx: 10, sy: 10, rankKm: 0 },
{ id: 'nan', sx: NaN, sy: 10, rankKm: 0 },
{ id: 'ok', sx: 10, sy: 10, rankKm: 1 },
// Margin candidates just outside the viewport land in the edge cells.
{ id: 'edge', sx: -20, sy: 850, rankKm: 2 },
], { budget: 5, viewW: 1000, viewH: 800 });
const ids = distributeCctvCards(
[
null,
{ id: '', sx: 10, sy: 10, rankKm: 0 },
{ id: 'nan', sx: NaN, sy: 10, rankKm: 0 },
{ id: 'ok', sx: 10, sy: 10, rankKm: 1 },
// Margin candidates just outside the viewport land in the edge cells.
{ id: 'edge', sx: -20, sy: 850, rankKm: 2 },
],
{ budget: 5, viewW: 1000, viewH: 800 },
);
assert.deepEqual(ids.sort(), ['edge', 'ok']);
assert.deepEqual(distributeCctvCards([], { budget: 5, viewW: 1000, viewH: 800 }), []);
assert.deepEqual(
distributeCctvCards([], { budget: 5, viewW: 1000, viewH: 800 }),
[],
);
});
test('selectCctvLod with viewport dims routes the fill through screen distribution', () => {
// A cluster larger than the street budget sits mid-screen; a periphery
// camera ranks past the budget. Nearest-first drops it — distribution
// keeps it because its cell is otherwise empty.
const clustered = Array.from({ length: CCTV_AMBIENT_CARD_MIN + 2 }, (_, i) => ({
id: `center-${String(i).padStart(2, '0')}`,
distanceKm: i / 10,
const clustered = Array.from(
{ length: CCTV_AMBIENT_CARD_MIN + 2 },
(_, i) => ({
id: `center-${String(i).padStart(2, '0')}`,
distanceKm: i / 10,
inView: true,
sx: 400 + (i % 5) * 8,
sy: 300 + Math.floor(i / 5) * 8,
}),
);
const periphery = {
id: 'periphery',
distanceKm: 9,
inView: true,
sx: 400 + (i % 5) * 8,
sy: 300 + Math.floor(i / 5) * 8,
}));
const periphery = { id: 'periphery', distanceKm: 9, inView: true, sx: 950, sy: 750 };
sx: 950,
sy: 750,
};
const selected = selectCctvLod([...clustered, periphery], {
cameraHeightM: 500, viewW: 1000, viewH: 800,
cameraHeightM: 500,
viewW: 1000,
viewH: 800,
});
assert.equal(selected.cardIds.length, CCTV_AMBIENT_CARD_MIN, 'budget respected');
assert.ok(selected.cardIds.includes('periphery'), 'periphery cell holds a card');
assert.equal(
selected.cardIds.length,
CCTV_AMBIENT_CARD_MIN,
'budget respected',
);
assert.ok(
selected.cardIds.includes('periphery'),
'periphery cell holds a card',
);
// Without screen info the original nearest-first cap applies unchanged.
const plain = selectCctvLod(
[...clustered, periphery].map(({ sx, sy, ...rest }) => rest),
{ cameraHeightM: 500 }
{ cameraHeightM: 500 },
);
assert.equal(plain.cardIds.includes('periphery'), false);
});
@@ -253,11 +327,13 @@ test('non-finite positive viewport dimensions retain exact legacy nearest-first
const legacy = selectCctvLod(input, { cameraHeightM: 500 });
assert.deepEqual(
selectCctvLod(input, { cameraHeightM: 500, viewW: Infinity, viewH: 800 }).cardIds,
selectCctvLod(input, { cameraHeightM: 500, viewW: Infinity, viewH: 800 })
.cardIds,
legacy.cardIds,
);
assert.deepEqual(
selectCctvLod(input, { cameraHeightM: 500, viewW: 1000, viewH: Infinity }).cardIds,
selectCctvLod(input, { cameraHeightM: 500, viewW: 1000, viewH: Infinity })
.cardIds,
legacy.cardIds,
);
@@ -266,7 +342,8 @@ test('non-finite positive viewport dimensions retain exact legacy nearest-first
{ id: 'normal', distanceKm: 2, inView: true },
];
assert.deepEqual(
selectCctvLod(legacyEdgeIds, { cameraHeightM: 500, viewW: 0, viewH: 800 }).cardIds,
selectCctvLod(legacyEdgeIds, { cameraHeightM: 500, viewW: 0, viewH: 800 })
.cardIds,
[' ', 'normal'],
'invalid viewport preserves every non-empty string ID accepted by the legacy path',
);
@@ -294,18 +371,34 @@ test('center weighting deterministically favors center without starving peripher
const { edge, center } = edgeVsCenterPool();
const view = { cameraHeightM: 500, viewW: 1000, viewH: 800 };
const selected = selectCctvLod([...edge, ...center], view);
const reordered = selectCctvLod([...center].reverse().concat([...edge].reverse()), view);
const reordered = selectCctvLod(
[...center].reverse().concat([...edge].reverse()),
view,
);
assert.equal(selected.cardIds.length, CCTV_AMBIENT_CARD_MIN);
assert.equal(selected.cardIds.filter((id) => id.startsWith('center-')).length, 10);
assert.equal(selected.cardIds.filter((id) => id.startsWith('edge-')).length, 10);
assert.deepEqual(reordered.cardIds, selected.cardIds, 'input order cannot affect winners or ordering');
assert.equal(
selected.cardIds.filter((id) => id.startsWith('center-')).length,
10,
);
assert.equal(
selected.cardIds.filter((id) => id.startsWith('edge-')).length,
10,
);
assert.deepEqual(
reordered.cardIds,
selected.cardIds,
'input order cannot affect winners or ordering',
);
const legacy = selectCctvLod(
[...edge, ...center].map(({ sx, sy, ...candidate }) => candidate),
{ cameraHeightM: 500 },
);
assert.equal(legacy.cardIds.filter((id) => id.startsWith('center-')).length, 0);
assert.equal(
legacy.cardIds.filter((id) => id.startsWith('center-')).length,
0,
);
});
test('ineligible video, hidden, and malformed-distance rows cannot alter eligible winners', () => {
@@ -322,13 +415,26 @@ test('ineligible video, hidden, and malformed-distance rows cannot alter eligibl
sx: 500,
sy: 400,
})),
{ id: 'hidden-outlier', distanceKm: 9_000, inView: false, sx: 900, sy: 700 },
{
id: 'hidden-outlier',
distanceKm: 9_000,
inView: false,
sx: 900,
sy: 700,
},
{ id: 'bad-distance', distanceKm: NaN, inView: true, sx: 900, sy: 700 },
{ id: '', distanceKm: 0, inView: true, sx: 500, sy: 400 },
];
assert.deepEqual(selectCctvLod([...eligible, ...noise], view).cardIds, baseline);
assert.deepEqual(selectCctvLod([...noise].reverse().concat([...eligible].reverse()), view).cardIds, baseline);
assert.deepEqual(
selectCctvLod([...eligible, ...noise], view).cardIds,
baseline,
);
assert.deepEqual(
selectCctvLod([...noise].reverse().concat([...eligible].reverse()), view)
.cardIds,
baseline,
);
});
test('malformed-distance rows remain slot-neutral when valid candidates leave spare capacity', () => {
@@ -339,7 +445,9 @@ test('malformed-distance rows remain slot-neutral when valid candidates leave sp
];
assert.deepEqual(selectCctvLod(candidates, view).cardIds, ['valid']);
assert.deepEqual(selectCctvLod([...candidates].reverse(), view).cardIds, ['valid']);
assert.deepEqual(selectCctvLod([...candidates].reverse(), view).cardIds, [
'valid',
]);
});
test('duplicate IDs are slot-neutral and choose a deterministic representative', () => {
@@ -353,7 +461,10 @@ test('duplicate IDs are slot-neutral and choose a deterministic representative',
{ ...center[0], distanceKm: NaN, sx: 500, sy: 400 },
];
assert.deepEqual(selectCctvLod([...eligible, ...duplicates], view).cardIds, baseline);
assert.deepEqual(
selectCctvLod([...eligible, ...duplicates], view).cardIds,
baseline,
);
assert.deepEqual(
selectCctvLod([...duplicates, ...eligible].reverse(), view).cardIds,
baseline,
@@ -362,13 +473,16 @@ test('duplicate IDs are slot-neutral and choose a deterministic representative',
});
test('center weighting has deterministic ID ties and preserves invalid-anchor eligibility', () => {
const anchors = Array.from({ length: CCTV_AMBIENT_CARD_MIN - 1 }, (_, index) => ({
id: `anchor-${String(index).padStart(2, '0')}`,
distanceKm: index / 100,
inView: true,
sx: 500,
sy: 400,
}));
const anchors = Array.from(
{ length: CCTV_AMBIENT_CARD_MIN - 1 },
(_, index) => ({
id: `anchor-${String(index).padStart(2, '0')}`,
distanceKm: index / 100,
inView: true,
sx: 500,
sy: 400,
}),
);
const tied = [
{ id: 'tie-b', distanceKm: 10, inView: true, sx: 500, sy: 400 },
{ id: 'tie-a', distanceKm: 10, inView: true, sx: 500, sy: 400 },
@@ -380,13 +494,26 @@ test('center weighting has deterministic ID ties and preserves invalid-anchor el
viewH: 800,
});
assert.ok(selected.cardIds.includes('tie-a'));
assert.equal(selected.cardIds.includes('tie-b'), false, 'ID breaks an exact contested tie');
assert.equal(selected.cardIds.includes('invalid-anchor'), false, 'invalid anchor does not displace valid winners');
assert.equal(
selected.cardIds.includes('tie-b'),
false,
'ID breaks an exact contested tie',
);
assert.equal(
selected.cardIds.includes('invalid-anchor'),
false,
'invalid anchor does not displace valid winners',
);
const onlyInvalid = selectCctvLod([
{ id: 'invalid-only', distanceKm: 1, inView: true, sx: NaN, sy: NaN },
], { cameraHeightM: 500, viewW: 1000, viewH: 800 });
assert.deepEqual(onlyInvalid.cardIds, ['invalid-only'], 'defensive top-up preserves eligibility');
const onlyInvalid = selectCctvLod(
[{ id: 'invalid-only', distanceKm: 1, inView: true, sx: NaN, sy: NaN }],
{ cameraHeightM: 500, viewW: 1000, viewH: 800 },
);
assert.deepEqual(
onlyInvalid.cardIds,
['invalid-only'],
'defensive top-up preserves eligibility',
);
});
test('applyEvictionGrace keeps a fallen-out card and clears grace when it returns', () => {
@@ -480,7 +607,11 @@ test('applyEvictionGrace never exceeds the hard cap; graced cards go first, olde
// cam-c is newly selected and enters immediately despite grace pressure.
assert.ok(result.keepIds.includes('cam-c'));
assert.ok(result.keepIds.includes('cam-new'), 'newest grace entry survives');
assert.deepEqual(result.evictIds, ['cam-old'], 'oldest grace entry evicted for the cap');
assert.deepEqual(
result.evictIds,
['cam-old'],
'oldest grace entry evicted for the cap',
);
});
test('applyEvictionGrace with a full selection keeps no graced cards', () => {
@@ -497,9 +628,19 @@ test('applyEvictionGrace with a full selection keeps no graced cards', () => {
});
test('staticFrameRefreshMs follows known pack cadences and bounds explicit values', () => {
assert.equal(staticFrameRefreshMs({ provider: 'Austin Transportation & Public Works' }), 300_000);
assert.equal(staticFrameRefreshMs({ provider: 'Transport for London' }), 180_000);
assert.equal(
staticFrameRefreshMs({ provider: 'Austin Transportation & Public Works' }),
300_000,
);
assert.equal(
staticFrameRefreshMs({ provider: 'Transport for London' }),
180_000,
);
assert.equal(staticFrameRefreshMs({ provider: 'Caltrans' }), 180_000);
assert.equal(staticFrameRefreshMs({ provider: 'Ontario 511' }), 180_000);
assert.equal(staticFrameRefreshMs({ provider: 'TxDOT' }), 180_000);
// Digitraffic publishes one weathercam frame per 600 s collection interval.
assert.equal(staticFrameRefreshMs({ provider: 'Fintraffic' }), 600_000);
assert.equal(staticFrameRefreshMs({ frameRefreshMs: 5_000 }), 60_000);
assert.equal(staticFrameRefreshMs({ frameRefreshMs: 3_000_000 }), 1_200_000);
assert.equal(staticFrameRefreshMs({ provider: 'Unknown Provider' }), 300_000);
+196
View File
@@ -0,0 +1,196 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
loadNswSourcesFromOpenData,
nswCameraLabel,
nswCameraToSource,
} from '../../server/providers/cctv/sources.js';
import {
cctvUpstreamUserAgent,
fetchCctvImageFromUpstream,
} from '../../server/providers/cctv/media.js';
import {
NSW_CAMERAS_URL,
NSW_IMAGE_USER_AGENT,
} from '../../server/providers/cctv/constants.js';
const feature = (overrides = {}, props = {}) => ({
type: 'Feature',
id: '5_ways_miranda',
geometry: { type: 'Point', coordinates: [151.1036, -34.0337] },
properties: {
region: 'SYD_SOUTH',
title: '5 Ways (Miranda)',
view: '5 Ways at The Boulevarde looking west towards Sutherland.',
direction: 'W',
href: 'https://webcams.transport.nsw.gov.au/livetraffic-webcams/cameras/5_ways_miranda.jpeg',
...props,
},
...overrides,
});
test('NSW label prefers the view sentence and falls back to the title for works notices', () => {
assert.equal(
nswCameraLabel({ title: '5 Ways (Miranda)', view: '5 Ways looking west.' }),
'5 Ways looking west.',
);
assert.equal(
nswCameraLabel({ title: 'Victoria Pass', view: 'x'.repeat(200) }),
'Victoria Pass',
);
assert.equal(nswCameraLabel({ title: 'T', view: 'line one\nline two' }), 'T');
assert.equal(nswCameraLabel({}), '');
});
test('NSW feature maps to a source with a compass heading and a pinned image host', () => {
const source = nswCameraToSource(feature({}, { direction: 'N-E' }));
assert.equal(source.id, 'nsw-5_ways_miranda');
assert.equal(source.city, 'SYD SOUTH');
assert.equal(source.provider, 'Live Traffic NSW');
assert.equal(source.lat, -34.0337);
assert.equal(source.lon, 151.1036);
assert.equal(source.headingDeg, 45);
assert.equal(source.headingConfidence, 'high');
assert.equal(source.sourceKind, 'nsw-livetraffic');
assert.match(source.license, /CC BY 4.0/);
assert.equal(source.url, source.snapshotUrl);
});
test('NSW mapping rejects off-host images and unusable geometry', () => {
assert.equal(
nswCameraToSource(feature({}, { href: 'https://evil.example/x.jpeg' })),
null,
);
assert.equal(nswCameraToSource(feature({ geometry: null })), null);
assert.equal(nswCameraToSource(feature({ id: '' })), null);
const noBearing = nswCameraToSource(feature({}, { direction: '' }));
assert.equal(noBearing.headingConfidence, 'low');
assert.ok(Number.isFinite(noBearing.headingDeg));
});
test('NSW loader reads the keyless feed and caps around Sydney', async (t) => {
t.mock.method(console, 'log', () => {});
const requested = [];
t.mock.method(globalThis, 'fetch', async (url) => {
requested.push(String(url));
return Response.json({
type: 'FeatureCollection',
features: [
feature(),
feature({ id: 'other' }, { href: 'https://evil.example/x.jpeg' }),
],
});
});
const cameras = await loadNswSourcesFromOpenData();
assert.deepEqual(requested, [NSW_CAMERAS_URL]);
assert.deepEqual(
cameras.map((camera) => camera.id),
['nsw-5_ways_miranda'],
);
});
test('the browser User-Agent applies to the NSW image host only', async () => {
assert.equal(
cctvUpstreamUserAgent(
'https://webcams.transport.nsw.gov.au/livetraffic-webcams/cameras/x.jpeg',
),
NSW_IMAGE_USER_AGENT,
);
assert.equal(
cctvUpstreamUserAgent(
'https://webcams.transport.nsw.gov.au.evil.test/x.jpeg',
),
'gods-eye-view-cctv-proxy/1.0',
);
assert.equal(
cctvUpstreamUserAgent(
'https://example.test/webcams.transport.nsw.gov.au/x.jpeg',
),
'gods-eye-view-cctv-proxy/1.0',
);
assert.equal(
cctvUpstreamUserAgent('not a url'),
'gods-eye-view-cctv-proxy/1.0',
);
const seen = [];
await fetchCctvImageFromUpstream(
'https://cctv.austinmobility.io/image/1.jpg',
{
timeoutMs: 100,
fetchImpl: async (url, init) => {
seen.push(init.headers['User-Agent']);
return new Response(Buffer.from([0xff, 0xd8, 0xff]), {
headers: { 'Content-Type': 'image/jpeg' },
});
},
},
);
assert.deepEqual(seen, ['gods-eye-view-cctv-proxy/1.0']);
});
test('the frame path follows redirects within the host only', async () => {
const jpeg = () =>
new Response(Buffer.from([0xff, 0xd8, 0xff]), {
headers: { 'Content-Type': 'image/jpeg' },
});
const redirect = (location) =>
new Response(null, { status: 302, headers: { location } });
const start = 'https://cctv.austinmobility.io/image/1.jpg';
const sameHost = [];
const followed = await fetchCctvImageFromUpstream(start, {
timeoutMs: 100,
fetchImpl: async (url, init) => {
assert.equal(init.redirect, 'manual', 'the fetch itself never follows');
sameHost.push(url);
return sameHost.length === 1 ? redirect('/image/moved/1.jpg') : jpeg();
},
});
assert.equal(followed?.ok, true);
assert.deepEqual(sameHost, [
start,
'https://cctv.austinmobility.io/image/moved/1.jpg',
]);
const offHost = [];
const refused = await fetchCctvImageFromUpstream(start, {
timeoutMs: 100,
fetchImpl: async (url) => {
offHost.push(url);
return redirect('https://evil.example/1.jpg');
},
});
assert.equal(refused, null);
assert.deepEqual(offHost, [start], 'the off-host target is never requested');
// Same host on another port, a plaintext downgrade, and a protocol-relative
// other host are all a different origin and are refused.
for (const target of [
'https://cctv.austinmobility.io:8443/image/1.jpg',
'http://cctv.austinmobility.io/image/1.jpg',
'//evil.example/1.jpg',
]) {
const seen = [];
const result = await fetchCctvImageFromUpstream(start, {
timeoutMs: 100,
fetchImpl: async (url) => {
seen.push(url);
return redirect(target);
},
});
assert.equal(result, null, target);
assert.deepEqual(seen, [start], target);
}
let hops = 0;
const looped = await fetchCctvImageFromUpstream(start, {
timeoutMs: 100,
fetchImpl: async () => {
hops += 1;
return redirect('/image/again.jpg');
},
});
assert.equal(looped, null);
assert.equal(hops, 3, 'at most two redirect hops are followed');
});
+328
View File
@@ -0,0 +1,328 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
loadTxdotSourcesFromOpenData,
normalizeTxdotDistrictPayload,
} from '../../server/providers/cctv/sources.js';
import { fetchTxdotSnapshot } from '../../server/providers/cctv/media.js';
import { TXDOT_CCTV_STATUS_URL } from '../../server/providers/cctv/constants.js';
/** Minimal valid JPEG head: SOI marker plus a byte of payload. */
const JPEG_BYTES = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00]);
const JPEG_B64 = JPEG_BYTES.toString('base64');
/** One "Device Online" record in the shape GetCctvStatusListByDistrict returns. */
function cameraRow(overrides = {}) {
return {
icd_Id: 'FM-734 @ US-290 EB',
name: 'FM-734 @ US-290 EB',
latitude: 30.34396,
longitude: -97.57988,
statusDescription: 'Device Online',
hasSnapshot: true,
dirDescription: 'North',
equipLoc: { roadway: 'FM-734', direction: 'North' },
...overrides,
};
}
const districtPayload = (rows) => ({
roadwayCctvStatuses: { 'FM-734': rows },
});
function quiet(t) {
t.mock.method(console, 'log', () => {});
t.mock.method(console, 'warn', () => {});
}
test('TxDOT catalog keeps only online cameras with finite coordinates', () => {
const cameras = normalizeTxdotDistrictPayload(
districtPayload([
cameraRow({ icd_Id: 'a', name: 'a' }),
cameraRow({
icd_Id: 'b',
name: 'b',
statusDescription: 'Device Offline',
}),
cameraRow({ icd_Id: 'c', name: 'c', statusDescription: 'Device Error' }),
cameraRow({ icd_Id: 'd', name: 'd', latitude: null }),
cameraRow({ icd_Id: 'e', name: 'e', longitude: 'not-a-number' }),
cameraRow({ icd_Id: 'f', name: 'f', latitude: 0, longitude: 0 }),
cameraRow({
icd_Id: 'g',
name: 'g',
latitude: '30.1',
longitude: '-97.7',
}),
cameraRow({ icd_Id: 'h', name: 'h', hasSnapshot: false }),
]),
'AUS',
);
assert.equal(cameras.length, 1);
assert.equal(cameras[0].name, 'a');
assert.equal(cameras[0].provider, 'TxDOT');
assert.equal(cameras[0].feedType, 'image');
assert.equal(cameras[0].sourceKind, 'txdot-its');
assert.equal(cameras[0].license, 'Public TxDOT traffic camera data');
assert.equal(
cameras[0].url,
'https://its.txdot.gov/its/DistrictIts/GetCctvSnapshotByIcdId?icdId=a&districtCode=AUS',
);
assert.equal(
cameras[0].id,
`txdot-aus-${Buffer.from('a').toString('base64url')}`,
);
});
test('TxDOT ids stay distinct for distinct device keys, whatever they hash or slug to', () => {
const ids = normalizeTxdotDistrictPayload(
districtPayload([
// These two share a 32-bit FNV hash.
cameraRow({ icd_Id: 'costarring', name: 'costarring' }),
cameraRow({ icd_Id: 'liquid', name: 'liquid' }),
// These two share a lowercase slug and a hash.
cameraRow({ icd_Id: 'abcDEfaBCDEFAbcdEFABCdEfAbcdeFaB', name: 'x' }),
cameraRow({ icd_Id: 'ABcDeFABCDefaBCDeFAbCdeFabcdefAB', name: 'y' }),
]),
'AUS',
).map((camera) => camera.id);
assert.equal(new Set(ids).size, 4);
assert.equal(
Buffer.from(ids[0].replace(/^txdot-aus-/, ''), 'base64url').toString(
'utf8',
),
'costarring',
'the device key is recoverable from the id',
);
});
test('TxDOT catalog dedupes a camera listed under two roadways', () => {
const cameras = normalizeTxdotDistrictPayload(
{
roadwayCctvStatuses: {
'IH-35': [cameraRow({ icd_Id: 'shared', name: 'IH-35 @ SH-71' })],
'SH-71': [cameraRow({ icd_Id: 'shared', name: 'IH-35 @ SH-71' })],
},
},
'AUS',
);
assert.equal(cameras.length, 1);
});
test('TxDOT heading comes from an explicit travel token, never the roadway direction', () => {
const [eastbound, westbound, plain] = normalizeTxdotDistrictPayload(
districtPayload([
cameraRow({ icd_Id: 'eb', name: 'FM-734 @ US-290 EB' }),
cameraRow({ icd_Id: 'wb', name: 'FM-734 @ US-290 WB' }),
cameraRow({ icd_Id: 'plain', name: 'FM-734 @ Bellingham Dr' }),
]),
'AUS',
);
assert.equal(eastbound.headingDeg, 90);
assert.equal(eastbound.headingConfidence, 'high');
assert.equal(westbound.headingDeg, 270);
assert.equal(westbound.headingConfidence, 'high');
assert.equal(plain.headingConfidence, 'low');
assert.ok(Number.isFinite(plain.headingDeg));
});
test('TxDOT heading refuses bare cardinals in Texas route names', () => {
const cameras = normalizeTxdotDistrictPayload(
districtPayload([
cameraRow({ icd_Id: 'lamar', name: 'N Lamar Blvd @ Rundberg Ln' }),
cameraRow({ icd_Id: 'west', name: 'West Ave @ 6th St' }),
]),
'AUS',
);
for (const camera of cameras) {
assert.equal(
camera.headingConfidence,
'low',
`${camera.name} must not claim a heading`,
);
}
});
test('TxDOT camera ids are district-scoped and stable across runs', () => {
const build = (district) =>
normalizeTxdotDistrictPayload(districtPayload([cameraRow()]), district)[0]
.id;
assert.equal(build('AUS'), build('AUS'));
assert.ok(build('AUS').startsWith('txdot-aus-'));
assert.ok(build('HOU').startsWith('txdot-hou-'));
assert.notEqual(build('AUS'), build('HOU'));
});
test('TxDOT ground elevation uses a per-district prior', () => {
const at = (district) =>
normalizeTxdotDistrictPayload(districtPayload([cameraRow()]), district)[0]
.groundElevationM;
assert.equal(at('HOU'), 15);
assert.equal(at('ELP'), 1140);
assert.equal(at('AUS'), 149);
assert.equal(at('SAT'), 198);
});
test('TxDOT catalog tolerates a malformed payload', () => {
assert.deepEqual(normalizeTxdotDistrictPayload(null, 'AUS'), []);
assert.deepEqual(normalizeTxdotDistrictPayload({}, 'AUS'), []);
assert.deepEqual(
normalizeTxdotDistrictPayload({ roadwayCctvStatuses: [] }, 'AUS'),
[],
);
assert.deepEqual(
normalizeTxdotDistrictPayload(
{ roadwayCctvStatuses: { 'IH-35': null } },
'AUS',
),
[],
);
});
test('TxDOT loader fetches each configured district and ignores unknown codes', async (t) => {
quiet(t);
const saved = process.env.CCTV_TXDOT_DISTRICTS;
process.env.CCTV_TXDOT_DISTRICTS = 'aus, SAT, NOPE, aus';
const requested = [];
t.mock.method(globalThis, 'fetch', async (url) => {
requested.push(String(url));
const district = new URL(String(url)).searchParams.get('districtCode');
return Response.json(
districtPayload([
cameraRow({ icd_Id: `${district}-1`, name: `${district}-1` }),
]),
);
});
try {
const cameras = await loadTxdotSourcesFromOpenData();
assert.deepEqual(requested, [
TXDOT_CCTV_STATUS_URL('AUS'),
TXDOT_CCTV_STATUS_URL('SAT'),
]);
assert.deepEqual(cameras.map((camera) => camera.cityId).sort(), [
'tx-aus',
'tx-sat',
]);
} finally {
if (saved === undefined) delete process.env.CCTV_TXDOT_DISTRICTS;
else process.env.CCTV_TXDOT_DISTRICTS = saved;
}
});
test('TxDOT snapshot decodes JSON only from the official origin and only as JPEG', async () => {
const jsonResponse = (body) =>
new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
const official =
'https://its.txdot.gov/its/DistrictIts/GetCctvSnapshotByIcdId?icdId=x&districtCode=AUS';
const frame = await fetchTxdotSnapshot(official, {
timeoutMs: 100,
fetchImpl: async () => jsonResponse({ snippet: JPEG_B64 }),
});
assert.equal(frame?.ok, true);
assert.equal(frame?.contentType, 'image/jpeg');
assert.deepEqual(frame?.body, JPEG_BYTES);
// Off-origin with the right path, and right origin with the wrong path,
// are each refused on their own.
for (const bad of [
'https://evil.example/its/DistrictIts/GetCctvSnapshotByIcdId?icdId=x',
'https://its.txdot.gov/its/DistrictIts/Other?icdId=x',
]) {
assert.equal(
await fetchTxdotSnapshot(bad, {
timeoutMs: 100,
fetchImpl: async () => jsonResponse({ snippet: JPEG_B64 }),
}),
null,
bad,
);
}
// A redirect is never followed, so the pin holds for the request made.
let calls = 0;
assert.equal(
await fetchTxdotSnapshot(official, {
timeoutMs: 100,
fetchImpl: async (_url, init) => {
calls += 1;
assert.equal(init.redirect, 'manual');
return new Response(null, {
status: 302,
headers: { location: 'https://evil.example/x.json' },
});
},
}),
null,
);
assert.equal(calls, 1);
// Junk base64 and oversize bodies are refused before decoding.
assert.equal(
await fetchTxdotSnapshot(official, {
timeoutMs: 100,
fetchImpl: async () => jsonResponse({ snippet: '/9!@#j/4AA=' }),
}),
null,
);
// Malformed padding is refused even though Buffer.from() would decode it.
assert.equal(
await fetchTxdotSnapshot(official, {
timeoutMs: 100,
fetchImpl: async () => jsonResponse({ snippet: '/9j/4AA==' }),
}),
null,
);
// A real JPEG larger than the frame cap is refused after decoding...
const bigJpeg = Buffer.concat([JPEG_BYTES, Buffer.alloc(59, 0x11)]);
assert.equal(bigJpeg.length, 64);
assert.equal(
await fetchTxdotSnapshot(official, {
timeoutMs: 100,
maxBytes: 8,
fetchImpl: async () =>
jsonResponse({ snippet: bigJpeg.toString('base64') }),
}),
null,
);
assert.equal(
(
await fetchTxdotSnapshot(official, {
timeoutMs: 100,
maxBytes: 64,
fetchImpl: async () =>
jsonResponse({ snippet: bigJpeg.toString('base64') }),
})
)?.body.length,
64,
'the same frame passes when it fits the cap',
);
// ...and an envelope that overflows the streaming cap never gets parsed.
const hugeJpeg = Buffer.concat([JPEG_BYTES, Buffer.alloc(8192, 0x11)]);
assert.equal(
await fetchTxdotSnapshot(official, {
timeoutMs: 100,
maxBytes: 8,
fetchImpl: async () =>
jsonResponse({ snippet: hugeJpeg.toString('base64') }),
}),
null,
);
const notJpeg = Buffer.from('<html>nope</html>').toString('base64');
for (const body of [
null,
{ snippet: null },
{ snippet: '' },
{ snippet: notJpeg },
]) {
assert.equal(
await fetchTxdotSnapshot(official, {
timeoutMs: 100,
fetchImpl: async () => jsonResponse(body),
}),
null,
);
}
});
+81
View File
@@ -0,0 +1,81 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { loadWarendorfSourcesFromCatalog } from '../../server/providers/cctv/sources.js';
test('Warendorf catalog registers the Marktplatz webcam on official hosts only', (t) => {
t.mock.method(console, 'log', () => {});
const cameras = loadWarendorfSourcesFromCatalog();
assert.deepEqual(
cameras.map((camera) => camera.id),
['warendorf-marktplatz-rathaus'],
);
for (const camera of cameras) {
assert.ok(
/^https?:\/\/(webcam\.warendorf\.de|www\.kreis-warendorf\.de)\//.test(
camera.url,
),
camera.url,
);
assert.equal(camera.snapshotUrl, camera.url);
assert.equal(camera.sourceKind, 'municipal-webcam');
assert.equal(camera.poseSource, 'curated');
assert.match(camera.license, /^Public municipal webcam data/);
}
});
test('Warendorf loader tolerates a missing catalog file', (t) => {
t.mock.method(console, 'warn', () => {});
assert.deepEqual(
loadWarendorfSourcesFromCatalog({ sourceRoot: '/nonexistent' }),
[],
);
});
test('Warendorf loader skips malformed rows without throwing', (t) => {
t.mock.method(console, 'log', () => {});
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-warendorf-'));
fs.mkdirSync(path.join(dir, 'config'));
fs.writeFileSync(
path.join(dir, 'config', 'cctv_sources.warendorf.json'),
JSON.stringify([
{
id: { toString: null },
url: 'https://www.kreis-warendorf.de/a.jpg',
lat: 51.9,
lon: 7.9,
},
{
id: 'ok',
url: 'https://www.kreis-warendorf.de/a.jpg',
lat: 51.9,
lon: 7.9,
},
{
id: 'text-coords',
url: 'https://www.kreis-warendorf.de/a.jpg',
lat: '51.9',
lon: '7.9',
},
{
id: 'null-island',
url: 'https://www.kreis-warendorf.de/a.jpg',
lat: 0,
lon: 0,
},
{
id: 'off-host',
url: 'https://evil.example/a.jpg',
lat: 51.9,
lon: 7.9,
},
]),
);
const cameras = loadWarendorfSourcesFromCatalog({ sourceRoot: dir });
assert.deepEqual(
cameras.map((camera) => camera.id),
['ok'],
);
});
+55 -1
View File
@@ -7,7 +7,7 @@ import * as Cesium from 'cesium';
* docs/pre-ship-audit-2026-07-01.md): every third-party data layer this app can
* display carries its own license and required attribution — ODbL (OSM
* datacenters/dams, adsb.lol, Overpass roads), CC BY-NC-SA (TeleGeography
* cables), NASA FIRMS, CelesTrak, USGS, City of Austin, GBFS operators, OpenSky.
* cables), NASA FIRMS, CelesTrak, USGS, City of Austin, Fintraffic (CC BY 4.0),
* The MIT code license does NOT cover this data.
*
* These credits are registered ONCE at init as STATIC credits with
@@ -122,6 +122,12 @@ export const DATA_CREDITS = [
'CCTV cameras &amp; frames: City of Austin, TX — ' +
'<a href="https://data.austintexas.gov" target="_blank" rel="noopener">data.austintexas.gov</a>',
},
{
key: 'txdot-cctv',
html:
'CCTV cameras &amp; frames (Texas): ' +
'<a href="https://its.txdot.gov/" target="_blank" rel="noopener">Texas Department of Transportation</a> (courtesy)',
},
{
key: 'caltrans-cctv',
html:
@@ -135,6 +141,20 @@ export const DATA_CREDITS = [
'<a href="https://tfl.gov.uk/info-for/open-data-users/" target="_blank" rel="noopener">Powered by TfL Open Data</a>. ' +
'Contains OS data © Crown copyright and database rights.',
},
{
key: 'ontario-511-cctv',
html:
'CCTV cameras &amp; frames (Ontario): ' +
'<a href="https://511on.ca/" target="_blank" rel="noopener">Ontario 511</a> ' +
'(<a href="https://www.ontario.ca/page/open-government-licence-ontario" target="_blank" rel="noopener">Open Government Licence - Ontario</a>)',
},
{
key: 'fintraffic-cctv',
html:
'CCTV cameras &amp; frames (Finland): Fintraffic / ' +
'<a href="https://www.digitraffic.fi/en/" target="_blank" rel="noopener">digitraffic.fi</a>, ' +
'license CC BY 4.0',
},
{
key: 'gbfs',
html: 'Bikeshare availability: GBFS operator feeds (e.g. Austin BCycle)',
@@ -176,6 +196,40 @@ export const DATA_CREDITS = [
'(<a href="https://earthdata.nasa.gov/firms" target="_blank" rel="noopener">earthdata.nasa.gov/firms</a>), ' +
'part of NASA’s Earth Observing System Data and Information System (EOSDIS)',
},
{
key: 'drivebc-cctv',
html:
'CCTV cameras &amp; frames (British Columbia): ' +
'<a href="https://www.drivebc.ca/" target="_blank" rel="noopener">DriveBC</a>. ' +
'Contains information licensed under the ' +
'<a href="https://www2.gov.bc.ca/gov/content/data/open-data/open-government-licence-bc" target="_blank" rel="noopener">Open Government Licence – British Columbia</a>. ' +
'Some cameras are supplied by partners (TransLink, the City of Vancouver, the City of Surrey, Parks Canada and others); each names its provider in the CCTV panel.',
},
{
key: 'tallinn-cctv',
html:
'CCTV cameras &amp; frames (Tallinn): City of Tallinn — ' +
'<a href="https://ristmikud.tallinn.ee/" target="_blank" rel="noopener">ristmikud.tallinn.ee</a> (courtesy)',
},
{
key: 'tarktee-cctv',
html:
'CCTV cameras &amp; frames (Estonia road weather): Transpordiamet / Tarktee — ' +
'<a href="https://tarktee.transpordiamet.ee/" target="_blank" rel="noopener">tarktee.transpordiamet.ee</a> (courtesy)',
},
{
key: 'warendorf-cctv',
html:
'Webcam (Warendorf): <a href="https://www.warendorf.de/" target="_blank" rel="noopener">Stadt Warendorf</a> (courtesy); ' +
'camera poses derived from OpenStreetMap geometry, © OpenStreetMap contributors (ODbL)',
},
{
key: 'nsw-cctv',
html:
'CCTV cameras &amp; frames (New South Wales): ' +
'<a href="https://www.livetraffic.com/" target="_blank" rel="noopener">Live Traffic NSW</a> — Transport for NSW ' +
'(<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener">CC BY 4.0</a>)',
},
{
key: 'telegeography',
html:
@@ -0,0 +1,58 @@
# CCTV ground heights
Precomputed ground height under every camera in the served CCTV catalog and
under the nine support points of its monitor plane, so the client can place a
camera and its plane with no runtime sampling. Shared placement data: any
consumer that renders these cameras on the same photorealistic surface can
reuse it.
Source surface: Google Photorealistic 3D Tiles, sampled at maximum detail with
CesiumJS `scene.sampleHeightMostDetailed` from a running God's Eye View
(scripts/precompute-cctv-heights.mjs). Heights are WGS84 **ellipsoidal**
metres; no geoid conversion is needed or applied. This is placement data
derived from the rendered surface, not imagery.
Runtime output: `cctv_ground_heights.json`
```
{
"schemaVersion": 1,
"provider": "google-photorealistic-3d",
"heightReference": "WGS84-ellipsoid",
"generatedAt": "<ISO>",
"cameras": {
"<camera id>": {
"poseHash": "p1-…", // hash of the served pose the samples describe
"status": "ok" | "miss",
"mountGroundM": <number>, // ground under the camera position (absent on a miss)
"supports": { // ground under the plane's 3×3 support grid; a value is
// null where that point had no sample (absent on a miss)
"bl","bm","br", // bottom row (left, middle, right)
"ml","mc","mr", // middle row
"tl","tm","tr" // top row
},
"misses": [<keys with no sample: "mount" and/or support keys>],
"sampledAt": "<ISO>",
"attempts": <n>
}
}
}
```
Support point positions are defined by `src/data/cctvFootprint.js`
(`planeSupportPoints`), which the client, the server join
(`server/providers/cctv/groundHeights.js`) and the precompute all share. An
entry is used only while the camera's served pose still hashes to
`poseHash`; a camera whose feed moved it, or whose pose a user edited, falls
back to runtime placement from the Re:Earth DEM.
Regenerate (resumable; only cameras whose pose changed or whose entry is
incomplete are re-sampled):
```
GEV_BASE=http://localhost:4173 node scripts/precompute-cctv-heights.mjs
```
Coverage at generation (2026-09-14): 3,445 of 3,446 cameras `ok` (six of them
with one or more null supports), 34,444 sampled heights, 43 minutes on a real
GPU. Regenerate whenever a pack changes its poses or a new pack lands.
File diff suppressed because it is too large. Load diff