mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Review fixes for #1506. - Ottawa: use the documented camera list and the city's Traffic Camera Images API. The pack is skipped with no request unless OTTAWA_CAMERA_CERTIFICATE is set; the certificate is added only at fetch time inside the pinned /opendata/ path, and each camera is fetched at most once a minute through the shared frame cache. - Québec 511 video is opt-in (CCTV_QUEBEC_VIDEO_ENABLED=1) and loads only after the viewer presses Play; selection, auto-hop, scenes and voice no longer contact Québec 511. Credit splits locations (MTQ, CC BY 4.0) from video (Québec 511). Interrupted play() is no longer reported as an unavailable source; the per-frame listener runs only while a Québec camera is selected and the canvas rect is cached. - Catalog cap: reserved Québec and World slots come only from capacity above the 4,000 shared slots, so a lower CCTV_MAX_SOURCES thins every pack instead of starving the existing ones. dev-fresh and .env.example default to 7,516. - Throttled frames (World, Ottawa): inside the refresh floor with no cached frame the proxy serves the placeholder, never Street View; a failed fetch backs off for a minute; the cache is bounded by bytes. - MJPEG first-frame extraction runs only for sources registered as MJPEG (Taiwan Highway Bureau), caps a frame at 2 MB, reads in linear time, accepts a CRLF preamble and a boundary that already starts with "--", and ends a part without Content-Length at the next delimiter. THB hosts are registered in the redirect pin list. - Tests: World loader policy, Québec loader and opt-in, proxy cooldown without Street View, Ottawa signing, cap reservations at 7,516, 4,000 and 48, overlay Play gate, refresh, visibility and teardown. Guard tests check the 4,000 shared slots again. - Remove the unused standalone Québec preview module and public page. - Docs: DATA_SOURCES, SECURITY, CURRENT-STATE, CCTV_INTEGRATION, README, .env.example, changelog fragment renamed to its full scope.
139 lines
4.5 KiB
JavaScript
139 lines
4.5 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { cameraDisplayCode, fallbackHeadingFromId } from './normalize.js';
|
|
|
|
const QUEBEC_CATALOG_PATH = 'public/quebec-cameras.json';
|
|
export const QUEBEC_CAMERA_SOURCE_COUNT = 1120;
|
|
|
|
/**
|
|
* Whether the operator opted in to the browser-direct Québec 511 video. Off by
|
|
* default: the camera locations are CC BY 4.0, but whether Québec 511's
|
|
* service terms allow another site to embed its clips is unconfirmed, so an
|
|
* unset flag lists the cameras with a link to the 511 viewer page only.
|
|
*/
|
|
export function quebecBrowserVideoEnabled(env = process.env) {
|
|
return String(env.CCTV_QUEBEC_VIDEO_ENABLED || '').trim() === '1';
|
|
}
|
|
|
|
/** Build the direct browser-only MP4 URL for one namespaced MTQ camera id. */
|
|
export function quebec511VideoUrl(cameraId) {
|
|
const match = /^qc-511-(\d+)$/.exec(String(cameraId || ''));
|
|
return match
|
|
? `https://www.quebec511.info/Carte/Fenetres/camera.ashx?id=${match[1]}&format=mp4`
|
|
: '';
|
|
}
|
|
|
|
function safeViewerUrl(value) {
|
|
try {
|
|
const url = new URL(String(value || ''));
|
|
return url.protocol === 'https:' && !url.username && !url.password
|
|
? url.href
|
|
: '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function sourceId(camera) {
|
|
const id = String(camera?.id || '').trim();
|
|
if (camera?.kind === 'traffic' && /^\d+$/.test(id)) return `qc-511-${id}`;
|
|
if (camera?.kind === 'montreal' && /^mtl-\d+(?:-\d+)*$/.test(id))
|
|
return `qc-mtl-${id.slice(4)}`;
|
|
if (camera?.kind === 'webcam' && /^web-\d+$/.test(id))
|
|
return `qc-web-${id.slice(4)}`;
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Read the checked-in, no-key Québec location catalog and adapt it to the
|
|
* regular CCTV source schema. MTQ video stays browser-only; other rows link to
|
|
* their operator page because this dataset has no direct image URL for them.
|
|
*/
|
|
export function loadQuebecSources({ sourceRoot = process.cwd() } = {}) {
|
|
const filename = path.resolve(sourceRoot, QUEBEC_CATALOG_PATH);
|
|
let cameras;
|
|
try {
|
|
const payload = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
cameras = Array.isArray(payload?.cameras) ? payload.cameras : [];
|
|
} catch (error) {
|
|
console.warn('[CCTV] Québec camera catalog unavailable:', error?.message);
|
|
return [];
|
|
}
|
|
|
|
const sources = [];
|
|
const seen = new Set();
|
|
for (const camera of cameras) {
|
|
const id = sourceId(camera);
|
|
const lat = Number(camera.lat);
|
|
const lon = Number(camera.lon);
|
|
if (
|
|
!id ||
|
|
seen.has(id) ||
|
|
!Number.isFinite(lat) ||
|
|
!Number.isFinite(lon) ||
|
|
lat < 44.9 ||
|
|
lat > 62.6 ||
|
|
lon < -79.9 ||
|
|
lon > -57.1
|
|
)
|
|
continue;
|
|
seen.add(id);
|
|
|
|
const name = String(camera.nameEn || camera.name || id).trim();
|
|
const isTraffic = camera.kind === 'traffic';
|
|
const isMontreal = camera.kind === 'montreal';
|
|
const sourceKind = isTraffic
|
|
? 'quebec-511-browser'
|
|
: isMontreal
|
|
? 'quebec-montreal-osm'
|
|
: 'quebec-webcam-link';
|
|
const provider = isTraffic
|
|
? 'Québec 511 / MTQ'
|
|
: isMontreal
|
|
? 'Ville de Montréal / OpenStreetMap'
|
|
: String(camera.operator || 'Québec webcam');
|
|
|
|
sources.push({
|
|
id,
|
|
name: name.slice(0, 180),
|
|
city: String(camera.region || 'Québec'),
|
|
cityId: 'quebec',
|
|
provider,
|
|
lat,
|
|
lon,
|
|
headingDeg: fallbackHeadingFromId(id),
|
|
headingConfidence: 'low',
|
|
pitchDeg: -18,
|
|
fovDeg: 74,
|
|
rangeM: 400,
|
|
mountHeightM: 8,
|
|
groundElevationM: 0,
|
|
feedType: 'image',
|
|
// No frame URL is registered: the server never fetches 511 media. With
|
|
// CCTV_QUEBEC_VIDEO_ENABLED=1 the browser plays the 511 MP4 in the DOM
|
|
// overlay after a play click; otherwise the camera links to 511.
|
|
url: '',
|
|
snapshotUrl: '',
|
|
sourceKind,
|
|
sourceUrl: safeViewerUrl(camera.viewerUrl),
|
|
accuracyM: Number.isFinite(Number(camera.accuracyM))
|
|
? Number(camera.accuracyM)
|
|
: null,
|
|
positionSource: String(camera.positionSource || ''),
|
|
license: isTraffic
|
|
? 'Locations: Données Québec / MTQ, CC BY 4.0. Video: hosted by Québec 511 under its terms of use'
|
|
: isMontreal
|
|
? 'OpenStreetMap contributors, ODbL 1.0'
|
|
: 'Operator terms apply; location provenance shown per camera',
|
|
credit: isTraffic
|
|
? 'Locations: MTQ · CC BY 4.0 · Video: Québec 511'
|
|
: isMontreal
|
|
? 'OpenStreetMap contributors · ODbL'
|
|
: provider,
|
|
code: cameraDisplayCode(name.toUpperCase()),
|
|
});
|
|
}
|
|
|
|
return sources;
|
|
}
|