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

+121 -9
View File
@@ -306,6 +306,109 @@ export function isLikelyAustinCoordinate(lat, lon) {
return lat >= 30.02 && lat <= 30.58 && lon >= -98.12 && lon <= -97.4;
}
/**
* Bounding-box sanity check: is this coordinate plausibly on the Finnish road
* network? Generous around the observed catalog extent (59.86..70.09 N,
* 19.62..31.28 E) so a real new station is never dropped, tight enough that a
* swapped lat/lon or a null island record is.
*
* @param {number} lat
* @param {number} lon
* @returns {boolean}
*/
/** Longest unselected camera label the HUD shows before it gets noisy. */
export const CAMERA_CODE_MAX_CHARS = 28;
/**
* Short display code for the unselected camera label ("CAM-<code>"): the
* feed's own name for the camera ("5TH ST / CONGRESS AVE", "TRAFALGAR
* SQUARE"), trimmed to CAMERA_CODE_MAX_CHARS. A pack may pass an explicit
* `code` (TxDOT's device key, NSW's title); the id is the last resort.
*
* @param {string} text
* @returns {string}
*/
export function cameraDisplayCode(text) {
const clean = String(text || '')
.replace(/\s+/g, ' ')
.trim();
if (clean.length <= CAMERA_CODE_MAX_CHARS) return clean;
return `${clean.slice(0, CAMERA_CODE_MAX_CHARS - 1).trimEnd()}…`;
}
/** Finite, in range, and not the null island that Number(null) produces. */
export function isPlausibleLatLon(lat, lon) {
return (
Number.isFinite(lat) &&
Number.isFinite(lon) &&
Math.abs(lat) <= 90 &&
Math.abs(lon) <= 180 &&
!(lat === 0 && lon === 0)
);
}
/** British Columbia bounding box (with the neighbouring border crossings). */
export function isLikelyBcCoordinate(lat, lon) {
return (
isPlausibleLatLon(lat, lon) &&
lat >= 48 &&
lat <= 60.5 &&
lon >= -139.5 &&
lon <= -114
);
}
/** Texas bounding box. */
export function isLikelyTexasCoordinate(lat, lon) {
return (
isPlausibleLatLon(lat, lon) &&
lat >= 25.5 &&
lat <= 36.7 &&
lon >= -107 &&
lon <= -93.4
);
}
/** New South Wales bounding box (incl. the ACT and Lord Howe Island). */
export function isLikelyNswCoordinate(lat, lon) {
return (
isPlausibleLatLon(lat, lon) &&
lat >= -38 &&
lat <= -28 &&
lon >= 140.9 &&
lon <= 159.2
);
}
export function isLikelyFinlandCoordinate(lat, lon) {
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return false;
return lat >= 59.5 && lat <= 70.5 && lon >= 19 && lon <= 32;
}
/**
* Human label for one Fintraffic preset (camera view).
*
* Station names are machine-shaped road codes ("vt3_Hyvinkää_Noppo"); the
* underscores become spaces. A preset id is always its station id plus a
* two-digit view number, so the remainder distinguishes the several views that
* share one station position. The station list endpoint carries no
* presentationName ("Helsinkiin"); that lives only on the per-station detail
* endpoint, which would cost one request per station.
*
* @param {string} stationName - Raw `properties.name`.
* @param {string} stationId - Raw `properties.id` (e.g. "C01503").
* @param {string} presetId - Raw preset id (e.g. "C0150301").
* @returns {string}
*/
export function fintrafficCameraName(stationName, stationId, presetId) {
const base =
String(stationName || '')
.replace(/_/g, ' ')
.trim() || `Fintraffic ${stationId}`;
const view = String(presetId || '').slice(String(stationId || '').length);
return view ? `${base} (view ${view})` : base;
}
/**
* Derive a deterministic fallback heading from a camera ID hash.
*
@@ -353,14 +456,15 @@ export function prioritizeSources(cameras, maxCount, anchors) {
const anchorList = (Array.isArray(anchors) ? anchors : []).filter(
(a) => Number.isFinite(a?.lat) && Number.isFinite(a?.lon),
);
if (
!Number.isFinite(maxCount) ||
maxCount <= 0 ||
list.length <= maxCount ||
!anchorList.length
) {
return list;
}
if (!anchorList.length) return list;
// Always sort when anchors exist, even when the pack fits its own cap: the
// catalog-wide cap (cap.js) thins a pack from the END of this order, so
// "nearest to an anchor first" has to hold whether or not the pack was
// trimmed here.
const cap =
Number.isFinite(maxCount) && maxCount > 0
? Math.min(maxCount, list.length)
: list.length;
const scored = list.map((camera, idx) => {
const lat = Number(camera?.lat);
@@ -379,7 +483,7 @@ export function prioritizeSources(cameras, maxCount, anchors) {
return a.idx - b.idx;
});
return scored.slice(0, maxCount).map((entry) => entry.camera);
return scored.slice(0, cap).map((entry) => entry.camera);
}
/**
@@ -410,6 +514,14 @@ export function normalizeSourceItem(item) {
url: typeof item.url === 'string' ? item.url : '',
snapshotUrl: typeof item.snapshotUrl === 'string' ? item.snapshotUrl : '',
license: String(item.license || item.licenseNote || ''),
// Per-camera attribution for feeds a partner supplies inside a pack
// (DriveBC: TransLink, city cameras). Shown beside the provider.
credit: String(item.credit || '').trim(),
// Unselected-label code: the pack's explicit short name, else the feed's
// name, else the id.
code: cameraDisplayCode(
item.code || String(item.name || '').toUpperCase() || item.id || '',
),
sourceKind: String(item.sourceKind || item.kind || 'configured'),
// Optional CAL badge input (cctv-v2 design §3b/§9.2, additive-only per the
// global constraints — nothing else in this file changes): hand-authored