Windy moved its Webcams API behind a paid tier (only the in-site Plugins API is free), so the windycams layer is gone before ever going live. In its place: roadcams — official public traffic cameras from keyless 511 APIs (a North-America-standard platform, one adapter covers many regions). Launching with Alberta (367) + Ontario (946); more regions are one entry in REGIONS + serve.py CAMS_REGIONS + an nginx location each (several 511s just need free keys). InfoBox descriptions are CallbackProperties, so the snapshot cache-buster is computed at click time — always-fresh images with zero entity rebuilds. Verified: 1,302 cams, live Toronto snapshot in-app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
82 lines
3.5 KiB
JavaScript
82 lines
3.5 KiB
JavaScript
// Road cams layer (Wave 6.2) — official public traffic cameras from keyless
|
|
// government APIs. The "511" platform is a North-America standard, so one
|
|
// adapter covers many regions; today: Alberta + Ontario (~1,100 cams). London
|
|
// has its own dedicated TfL layer. JSON comes via proxy/cams/<region> (no CORS
|
|
// upstream); snapshot IMAGES load directly in the InfoBox <img> (no CORS needed).
|
|
//
|
|
// Add a region = one entry in REGIONS here + serve.py CAMS_REGIONS + a prod
|
|
// nginx location. Several more 511s (Georgia, Nevada, New England…) just need
|
|
// their free API keys.
|
|
|
|
const REGIONS = [
|
|
{ id: 'alberta', name: 'Alberta' },
|
|
{ id: 'ontario', name: 'Ontario' },
|
|
];
|
|
|
|
export default function create(ctx) {
|
|
const { viewer, Cesium, lib, ui } = ctx;
|
|
const ds = new Cesium.CustomDataSource('roadcams');
|
|
ds.show = false;
|
|
viewer.dataSources.add(ds);
|
|
|
|
let enabled = false;
|
|
let loaded = false;
|
|
|
|
ui.addLayer('roadcams', 'Road cams (511 network)', false, (on) => {
|
|
enabled = on;
|
|
ds.show = on;
|
|
if (on && !loaded) load();
|
|
else if (on) ui.setStatus('roadcams', statusText(), 'ok');
|
|
else ui.setStatus('roadcams', '', 'ok');
|
|
}, 'Context');
|
|
|
|
const okRegions = [];
|
|
function statusText() {
|
|
return `${ds.entities.values.length} cams · ${okRegions.join(' · ') || '—'}`;
|
|
}
|
|
|
|
async function load() {
|
|
ui.setStatus('roadcams', 'loading cameras…', 'warn');
|
|
const results = await Promise.allSettled(REGIONS.map(async (r) => {
|
|
const res = await fetch(`proxy/cams/${r.id}`);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
return { region: r, cams: await res.json() };
|
|
}));
|
|
for (const rr of results) {
|
|
if (rr.status !== 'fulfilled') continue;
|
|
const { region, cams } = rr.value;
|
|
let n = 0;
|
|
for (const c of Array.isArray(cams) ? cams : []) {
|
|
if (!isFinite(c.Latitude) || !isFinite(c.Longitude)) continue;
|
|
const views = (c.Views || []).filter((v) => v && v.Url && v.Status !== 'Disabled');
|
|
if (!views.length) continue;
|
|
ds.entities.add({
|
|
position: Cesium.Cartesian3.fromDegrees(c.Longitude, c.Latitude),
|
|
point: {
|
|
pixelSize: 5, color: lib.cz('#ffb84d'),
|
|
outlineColor: Cesium.Color.BLACK, outlineWidth: 1,
|
|
disableDepthTestDistance: 50000,
|
|
},
|
|
// CallbackProperty: HTML is built the moment the InfoBox opens, so the
|
|
// snapshot cache-buster is always current — no stale images, no rebuilds.
|
|
description: new Cesium.CallbackProperty(() => {
|
|
const cb = Math.floor(Date.now() / 120000); // 2-min buckets
|
|
const imgs = views.slice(0, 2).map((v) =>
|
|
`<p><img src="${lib.escapeHtml(lib.safeUrl(v.Url))}?godsigh=${cb}" style="width:100%;border-radius:6px" alt="traffic cam"/>` +
|
|
(v.Description ? `<br/><span style="opacity:.7">${lib.escapeHtml(v.Description)}</span>` : '') + `</p>`).join('');
|
|
return `<p><b>${lib.escapeHtml(c.Location || c.Roadway || 'Camera')}</b></p>` + imgs +
|
|
`<p style="opacity:.7">${lib.escapeHtml([c.Roadway, region.name].filter(Boolean).join(' · '))} · 511 open data</p>`;
|
|
}, false),
|
|
});
|
|
n++;
|
|
}
|
|
if (n) okRegions.push(region.name);
|
|
}
|
|
loaded = true;
|
|
const anyFail = results.some((r) => r.status !== 'fulfilled');
|
|
if (enabled) ui.setStatus('roadcams', statusText() + (anyFail ? ' · some regions down' : ''), anyFail ? 'warn' : 'ok');
|
|
}
|
|
|
|
return { id: 'roadcams' };
|
|
}
|