Merge current main into ALPR camera candidate

This commit is contained in:
Sameh Khamis committed 2026-09-13 20:08:54 -07:00
commit 7068ee7dc7
51 files changed
+78701 -972

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
@@ -130,6 +130,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:
@@ -143,6 +149,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)',
@@ -184,6 +204,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,46 @@
# CCTV ground heights
Precomputed ground height under every camera in the served CCTV catalog and
under the nine support points of its monitor plane, aligned to work with
Google Photorealistic 3D Tiles. Heights are WGS84 ellipsoidal metres. The
client places a camera and its plane from this file with no runtime sampling.
Runtime output: `cctv_ground_heights.json`
```
{
"schemaVersion": 1,
"provider": "google-3d-tiles",
"heightReference": "WGS84-ellipsoid",
"generatedAt": "<ISO>",
"cameras": {
"<camera id>": {
"poseHash": "p1-…", // hash of the served pose the entry describes
"status": "ok" | "miss",
"mountGroundM": <number>, // ground under the camera position (absent on a miss)
"supports": { // ground under the plane's 3×3 support grid; null where
"bl","bm","br", // a point has no value (absent on a miss)
"ml","mc","mr",
"tl","tm","tr"
},
"misses": [<keys with no value>],
"sampledAt": "<ISO>",
"attempts": <n>
}
}
}
```
Support point positions are defined by `src/data/cctvFootprint.js`
(`planeSupportPoints`), shared with the server join
(`server/providers/cctv/groundHeights.js`). 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 terrain
proxy.
Regenerate against a running app (resumable; only cameras whose pose changed
or whose entry is incomplete are redone):
```
GEV_BASE=http://localhost:4173 node scripts/precompute-cctv-heights.mjs
```
File diff suppressed because it is too large. Load diff