feat(cctv): WSDOT highway camera source pack (Washington State)

Adds Washington State's highway cameras as a keyless source pack, from the
Esri JSON layer WSDOT's own Travel Center map consumes
(data.wsdot.wa.gov/travelcenter/Cameras.json, ~1,700 point features).
Closes #624.

- Geometry arrives in EPSG:3857; converted to WGS84 on load and bounded to
  a Washington bounding box.
- Only frames on WSDOT's own images.wsdot.wa.gov host are registered — the
  catalog also lists ~70 partner cameras (Oregon DOT, City of Seattle,
  lodges, airports) whose own terms apply; those are skipped rather than
  proxied. 1,630 cameras survive the filters against the live feed.
- CompassDirection letters (N/E/S/W) give a high-confidence heading; the
  majority `B` ("both directions") rows carry no single facing and use the
  shared id-hash fallback at low confidence, with the usual two pose
  personalities.
- The feed body carries Windows-1252 bytes (en-dashes in titles) with no
  charset declaration, so it is decoded explicitly before parsing.
- WSDOT's layer metadata marks the feed "low volume": the 15-minute catalog
  cache and per-request frame fetches respect that, frames are never stored
  or redistributed, and the WSDOT credit is registered in the attribution
  popover. A keyed WSDOT Traveler Information API serves the same population
  for higher-volume use and is deliberately not required.
- Defaults: 250 cameras nearest Seattle and Spokane; CCTV_WSDOT_MAX_SOURCES
  changes the cap, CCTV_WSDOT_ENABLED=0 turns the pack off.
- Tests: Mercator conversion against a real layer feature, official-host and
  malformed-row filtering, compass mapping and both pose personalities,
  cp1252 decoding, env cap, upstream-failure fallback.
This commit is contained in:
bassem chagra committed 2026-09-25 12:32:36 +02:00
1 parent b210ab0fe4
commit ee3e3c7a41
6 files changed
+337

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
DEFAULT_WSDOT_MAX_SOURCES,
WSDOT_CAMERAS_URL,
} from '../../server/providers/cctv/constants.js';
import {
loadWsdotSourcesFromOpenData,
webMercatorToWgs84,
} 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', () => {});
}
/** Web Mercator metres for a WGS84 coordinate (inverse of the loader's math). */
function mercator(lat, lon) {
const R = 6378137;
return {
x: (lon * Math.PI * R) / 180,
y: R * Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360)),
};
}
/** A `Cameras.json` point feature carrying the fields the loader reads. */
function wsdotFeature(id, lat, lon, overrides = {}) {
const { attributes = {}, geometry } = overrides;
return {
attributes: {
CameraID: id,
CameraTitle: `Camera ${id}`,
CompassDirection: 'N',
ImageURL: `https://images.wsdot.wa.gov/nw/${id}.jpg`,
...attributes,
},
geometry: geometry === undefined ? mercator(lat, lon) : geometry,
};
}
/** Serialize a payload for the mocked fetch as raw Windows-1252 bytes. */
function cp1252Response(payload) {
const text = JSON.stringify(payload);
const bytes = new Uint8Array(text.length);
for (let i = 0; i < text.length; i += 1) {
const code = text.charCodeAt(i);
// U+2013 EN DASH is 0x96 in Windows-1252 — the byte the live feed ships.
bytes[i] = code === 0x2013 ? 0x96 : code;
}
return new Response(bytes, {
headers: { 'Content-Type': 'application/json' },
});
}
test('webMercatorToWgs84 recovers WGS84 degrees from layer geometry', () => {
// Real feature from the live layer: I-5 at the Interstate Bridge.
const { lat, lon } = webMercatorToWgs84(-13656007.102, 5719738.545);
assert.ok(Math.abs(lat - 45.62) < 0.05, `lat ${lat}`);
assert.ok(Math.abs(lon - -122.675) < 0.05, `lon ${lon}`);
});
test('WSDOT loader keeps official-host cameras and maps compass directions', async (t) => {
quiet(t);
withEnv(t, { CCTV_WSDOT_MAX_SOURCES: undefined });
const requested = [];
t.mock.method(globalThis, 'fetch', async (url) => {
requested.push(String(url));
return cp1252Response({
features: [
wsdotFeature(1001, 47.6097, -122.3331, {
attributes: {
// The 0x2013 below is serialized as the feed's 0x96 byte.
CameraTitle: 'I-5 – Seattle, looking south',
CompassDirection: 'S',
},
}),
wsdotFeature(9059, 47.66, -117.43, {
attributes: { CompassDirection: 'B' },
}),
// Partner-hosted frame (Oregon DOT): skipped, never proxied.
wsdotFeature(1002, 45.62, -122.675, {
attributes: {
ImageURL: 'https://www.tripcheck.com/RoadCams/cams/bridge.jpg',
},
}),
// Bad rows: non-integer id, missing geometry, out-of-state point.
wsdotFeature('1003', 47.6, -122.3),
wsdotFeature(1004, 47.6, -122.3, { geometry: null }),
wsdotFeature(1005, 34.05, -118.24),
],
});
});
const cameras = await loadWsdotSourcesFromOpenData();
assert.deepEqual(requested, [WSDOT_CAMERAS_URL]);
// Exactly the two valid official-host cameras survive; the partner-hosted
// frame and the malformed rows are dropped.
assert.deepEqual(cameras.map((camera) => camera.id).sort(), [
'wsdot-1001',
'wsdot-9059',
]);
const seattle = cameras.find((camera) => camera.id === 'wsdot-1001');
assert.equal(seattle.name, 'I-5 – Seattle, looking south');
assert.equal(seattle.provider, 'WSDOT');
assert.equal(seattle.url, 'https://images.wsdot.wa.gov/nw/1001.jpg');
assert.equal(seattle.snapshotUrl, seattle.url);
assert.equal(seattle.feedType, 'image');
assert.equal(seattle.headingDeg, 180);
assert.equal(seattle.headingConfidence, 'high');
assert.equal(seattle.fovDeg, 56);
assert.ok(Math.abs(seattle.lat - 47.6097) < 0.001);
assert.ok(Math.abs(seattle.lon - -122.3331) < 0.001);
// "Both directions" carries no single facing: id-hash fallback at low
// confidence, with the low-confidence pose personality.
const spokane = cameras.find((camera) => camera.id === 'wsdot-9059');
assert.equal(spokane.headingConfidence, 'low');
assert.equal(spokane.fovDeg, 44);
assert.ok(spokane.headingDeg >= 0 && spokane.headingDeg < 360);
});
test('WSDOT loader honors the max-sources cap', async (t) => {
quiet(t);
withEnv(t, { CCTV_WSDOT_MAX_SOURCES: '8' });
const features = [];
for (let i = 0; i < 24; i += 1) {
features.push(wsdotFeature(2000 + i, 47.0 + i * 0.05, -122.9 + i * 0.05));
}
t.mock.method(globalThis, 'fetch', async () => cp1252Response({ features }));
const cameras = await loadWsdotSourcesFromOpenData();
assert.equal(cameras.length, 8);
});
test('WSDOT loader defaults its cap and survives upstream failure', async (t) => {
quiet(t);
withEnv(t, { CCTV_WSDOT_MAX_SOURCES: undefined });
assert.equal(DEFAULT_WSDOT_MAX_SOURCES, 250);
t.mock.method(
globalThis,
'fetch',
async () => new Response('', { status: 503 }),
);
assert.deepEqual(await loadWsdotSourcesFromOpenData(), []);
t.mock.method(globalThis, 'fetch', async () => {
throw new Error('network down');
});
assert.deepEqual(await loadWsdotSourcesFromOpenData(), []);
});
+10
View File
@@ -293,6 +293,16 @@ export const DATA_CREDITS = [
'<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: 'wsdot-cctv',
html:
'CCTV cameras &amp; frames (Washington State): ' +
'<a href="https://wsdot.wa.gov/" target="_blank" rel="noopener">Washington State Department of Transportation</a> ' +
'(<a href="https://wsdot.com/travel/real-time/cameras" target="_blank" rel="noopener">Travel Center</a>), ' +
'used per the ' +
'<a href="https://wsdot.wa.gov/about/policies/travel-information-disclaimer" target="_blank" rel="noopener">WSDOT travel information disclaimer</a> ' +
'(low-volume public feed; catalog cached, frames proxied on demand)',
},
{
key: 'tallinn-cctv',
html: