diff --git a/.env.example b/.env.example index 9edc74f0a..cbf2941bd 100644 --- a/.env.example +++ b/.env.example @@ -237,7 +237,7 @@ AISSTREAM_API_KEY= # CCTV_TORONTO_LIST_URL= # override the Toronto RESCU camera list URL # CCTV_LITHUANIA_ENABLED= # off by default; set 1 to load Via Lietuva / Eismoinfo road cameras (photo licence unconfirmed; see DATA_SOURCES.md) # CCTV_LITHUANIA_MAX_SOURCES=400 # cap Lithuanian cameras; no API key required -# CCTV_FLORIDA_ENABLED= # off by default; set 1 to load FL511 (Florida) cameras (individual, non-commercial use only; see DATA_SOURCES.md) +# CCTV_FLORIDA_ENABLED= # off by default; set 1 to load FL511 (Florida) cameras (individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent; see DATA_SOURCES.md) # CCTV_FLORIDA_MAX_SOURCES=500 # cap cameras loaded from the FL511 catalog (nearest regional anchors first) # CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs # CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 6afc12f4a..3fc0cd08b 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -48,7 +48,7 @@ How to read this: | **GDELT Project DOC 2.0** | Fail-soft fallback for location-matched cockpit headlines | [GDELT Terms of Use](https://www.gdeltproject.org/about.html#termsofuse): unrestricted academic/commercial/governmental dataset use, with citation and link required; linked articles retain publisher terms | "GDELT Project" plus each article's linked publisher/domain | | **City of Austin Open Data** | CCTV camera catalog + frames | City of Austin Open Data Terms of Use | "City of Austin, TX — data.austintexas.gov" | | **TxDOT ITS (its.txdot.gov)** | CCTV camera catalogs + frames, Texas districts | Public TxDOT traffic camera data | "Texas Department of Transportation" (courtesy) | -| **FL511 (Florida DOT)** | CCTV camera catalog + frames, Florida statewide | FL511 content, individual non-commercial use only ([terms](https://fl511.com/privacy#disclaimer)). **Off by default**; set `CCTV_FLORIDA_ENABLED=1` to enable | "Florida DOT — FL511.com" (courtesy) | +| **FL511 (Florida DOT)** | CCTV camera catalog + frames, Florida statewide | FL511 content, individual personal use only; any other use needs FDOT's express written consent ([terms](https://fl511.com/privacy#disclaimer)). **Off by default**; set `CCTV_FLORIDA_ENABLED=1` to enable | "Florida DOT — FL511.com" (courtesy) | | **City of Tallinn (ristmikud.tallinn.ee)** | CCTV camera catalog + frames, Tallinn intersections | Public City of Tallinn traffic camera data | "City of Tallinn — ristmikud.tallinn.ee" (courtesy) | | **Transpordiamet / Tarktee** | CCTV camera catalog + frames, Estonia road-weather cameras | Public Transpordiamet / Tarktee road camera data | "Transpordiamet / Tarktee — tarktee.transpordiamet.ee" (courtesy) | | **Stadt Warendorf webcam** | Webcam frames, Marktplatz / Historisches Rathaus (`config/cctv_sources.warendorf.json`) | Public municipal webcam data | "Stadt Warendorf — webcam.warendorf.de" (courtesy) | @@ -161,7 +161,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **Warendorf webcam.** The Stadt Warendorf Marktplatz webcam, registered from the curated `config/cctv_sources.warendorf.json` (the Kreis Warendorf registration-office cameras from the contributing PR are indoor waiting rooms and were left out); only the municipal image host is proxied, frames are fetched at request time and never stored or redistributed. Disable with `CCTV_WARENDORF_ENABLED=0`. - **Live Traffic NSW cameras.** The camera list is the keyless public `data.livetraffic.com/cameras/traffic-cam.json` feed (Transport for NSW open data, CC BY 4.0); frames are stills on `webcams.transport.nsw.gov.au`, and only that host is registered. That image host answers non-browser clients with an HTML placeholder, so the proxy identifies as a browser for that one host and as itself everywhere else. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_NSW_ENABLED=0`. - **Open Calgary traffic cameras.** Keyless. The camera list is the public Socrata dataset `k7p9-kppz` on `data.calgary.ca`; frames are stills on the city's own `trafficcam.calgary.ca` host, and only that host is registered. Most rows publish an `http://` frame URL and the host redirects those to HTTPS, so URLs are upgraded and pinned to that origin before registration. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `quadrant` field and the quadrant suffix on each camera name are Calgary's address grid, not a bearing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. The licence requires the attribution statement above. Disable with `CCTV_CALGARY_ENABLED=0`. -- **FL511 Florida cameras.** Off by default; set `CCTV_FLORIDA_ENABLED=1` to load them. The camera list is the keyless FL511 traffic camera layer on ArcGIS (`services.arcgis.com/.../FL511_Traffic_Cameras`), published from a Florida Division of Emergency Management account rather than by FL511 directly, and the service carries no licence text of its own. It is paged 2000 rows at a time to cover the ~4,000-camera statewide set. Frames are stills on two Divas Cloud hosts (`images-dis.divas.cloud`, `snapshots.divas.cloud`); both are host-pinned and a row whose image resolves elsewhere is dropped. `CCTV_FLORIDA_MAX_SOURCES` caps the pack (default 500, anchored per region across the state). Each catalog page is read under a 2 MiB cap and redirects are refused. Frames are fetched live at request time and never stored or redistributed. FL511's terms (fl511.com/privacy#disclaimer) license its content for individual, non-commercial use only; deployment beyond personal use needs FDOT's written consent. +- **FL511 Florida cameras.** Off by default; set `CCTV_FLORIDA_ENABLED=1` to load them. The camera list is the keyless FL511 traffic camera layer on ArcGIS (`services.arcgis.com/.../FL511_Traffic_Cameras`), published from a Florida Division of Emergency Management account rather than by FL511 directly, and the service carries no licence text of its own. It is paged up to 2,000 rows at a time, ordered by the layer's object id and following ArcGIS's `exceededTransferLimit`, to cover the ~4,000-camera statewide set. Frames are stills on two Divas Cloud hosts (`images-dis.divas.cloud`, `snapshots.divas.cloud`); both are host-pinned and a row whose image resolves elsewhere is dropped. `CCTV_FLORIDA_MAX_SOURCES` caps the pack (default 500, anchored per region across the state). Each catalog page is read under a 2 MiB cap and redirects are refused. Frames are fetched live at request time and never stored or redistributed. FL511's terms (fl511.com/privacy#disclaimer) license its content for individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent. - **Catalonia (Servei Català de Trànsit) cameras.** The camera list is the keyless `https://www.gencat.cat/transit/opendata/cameres.xml` WFS/GML feed, fetched over https with redirects refused and the body read capped at 1 MiB, since the list decides ids, coordinates and which allowlisted frame path is proxied. It carries a `font` field naming the real publisher per camera; this pack keeps SCT's own highway cameras plus hotlinked Barcelona (IMI) and Terrassa municipal cameras (~130 total), each confirmed to return a real frame, so frame URLs are checked against a small host allowlist (`mct.gencat.cat`, `www.bcn.cat`, `emap.terrassa.cat`) rather than one pinned origin (a link carrying `user:pass@` userinfo or an explicit port, or pointing outside that host's frame directory, is rejected even on an allowed host; an SCT `sctidcam` must be a bare `.gif`/`.jpg` filename), and every non-SCT camera carries its real owner as a `credit` shown beside the shared `provider`. The feed carries no compass heading, so every camera loads at low heading confidence (same as TfL and Fintraffic). SCT's own feed link (`RenderService`) needed a workaround: it always 302s to a second endpoint (`TransitCamera`) on a Location the upstream hardcodes as plain http even when requested over https — refused by `fetchWithinHost`'s same-origin (scheme-included) redirect policy — and separately, `mct.gencat.cat`'s TLS setup fails the handshake with Node's fetch outright (`ERR_SSL_WRONG_SIGNATURE_TYPE`) even though other HTTP clients complete it. The loader therefore builds the `TransitCamera` URL itself, over plain http, from `RenderService`'s `sctidcam` id, rather than registering the (broken) upstream link — the only transport this one host actually serves to this runtime. Camera ids are built from the SCT camera file or the partner image path, not the feed's `fid` (which embeds an upstream server-start stamp and changes on every upstream restart), and rows the feed lists more than once collapse to one camera. The feed carries no camera status field, so a camera its publisher has taken offline still registers and shows the publisher's own "image unavailable" still (SCT and Barcelona both serve one with HTTP 200), the same as other packs without a status field. Frames are fetched at request time and never stored or redistributed. The feed itself states no licence; the pack relies on the Generalitat de Catalunya open-data portal's terms for this dataset (the licence and attribution above). Disable with `CCTV_CATALONIA_ENABLED=0`. - **King County road cameras.** Keyless. The camera list is one public ArcGIS FeatureServer GeoJSON query (King County DOT Road Services, "King County and WSDOT Traffic Cameras"). The `outFields` list is explicit on purpose: the layer also carries asset-administration columns — including staff names — that are never fetched. Only live, active cameras with frames on the county's own `info.kingcounty.gov` image handler are registered (the feed writes `http://`; the handler serves HTTPS, so URLs are upgraded and pinned before registration). WSDOT-owned rows in the layer are skipped because their frames are WSDOT-hosted, outside the pinned image origin; partner-city cameras hosted on the county image handler (e.g. Newcastle) name their owner as the per-camera credit. The layer publishes no facing, so headings use the shared id-hash fallback at low confidence. Uniquely, the county publishes each camera's hardware `Manufacturer`/`Model` (Cohu 3950/3965, Bosch AUTODOME, Axis PTZ domes); both are carried on the source (and served by `/api/cctv/sources`) as `model` and `manufacturer` for identified-hardware consumers (vendor-generic strings such as `PTZ` name no model and are omitted). Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_KINGCOUNTY_ENABLED=0`. - **WSDOT highway cameras.** Keyless. The camera list is the Esri JSON layer `data.wsdot.wa.gov/travelcenter/Cameras.json` — the same layer WSDOT's own Travel Center map consumes (~1,700 point features, EPSG:3857 geometry converted to WGS84 on load; the body carries Windows-1252 bytes and is decoded as such). 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, and those are skipped rather than proxied. WSDOT's layer metadata marks the feed for "low volume" use (see WSDOT's [travel information disclaimer](https://wsdot.wa.gov/about/policies/travel-information-disclaimer)). The catalog is fetched once per CCTV catalog refresh (every 15 minutes), and a camera's frame is fetched only when that camera is viewed; frames are never stored or redistributed. The live feed's `CompassDirection` is a single cardinal (N/E/S/W), `B` (both directions — most rows), `O`, or null. A single cardinal gives a high-confidence heading (the diagonals NE/SE/SW/NW are mapped too should they appear); `B`, `O`, null, and any other code carry no single facing and use the shared id-hash fallback at low confidence. By default the 250 cameras nearest Seattle and Spokane load; `CCTV_WSDOT_MAX_SOURCES` changes the cap (8–1,200) and `CCTV_WSDOT_ENABLED=0` turns the pack off. A keyed WSDOT Traveler Information API serves the same population for higher-volume use and is deliberately not required here. diff --git a/README.md b/README.md index b6600ea48..a4cc32acd 100644 --- a/README.md +++ b/README.md @@ -377,7 +377,7 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi | 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 | | ◍ **Air quality** | Live AQHI on the published 1–10+ health-risk scale, with a Weather-panel card showing the band legend and the highest readings in view. **Canada only** — ECCC is the first registered provider | ECCC MSC GeoMet | 🟢 | | 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds | -| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual non-commercial use only). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | +| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | | 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 | | 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer and **FOLLOW**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key | | 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 | diff --git a/changelog.d/605-fl511-florida-cameras.md b/changelog.d/605-fl511-florida-cameras.md index ecb65553a..76f840198 100644 --- a/changelog.d/605-fl511-florida-cameras.md +++ b/changelog.d/605-fl511-florida-cameras.md @@ -1,8 +1,9 @@ - Add FL511 (Florida DOT) traffic cameras as an opt-in CCTV source pack: the statewide ArcGIS catalog, read a page at a time under a byte cap with redirects refused, and frames pinned to FL511's two image hosts over HTTPS. - FL511's terms limit its content to individual, non-commercial use, so the - pack stays off unless `CCTV_FLORIDA_ENABLED=1`. Headings come from the + FL511's terms limit its content to individual, personal use, and any other + use (a public deployment included) needs FDOT's express written consent, so + the pack stays off unless `CCTV_FLORIDA_ENABLED=1`. Headings come from the catalog's direction codes, and camera ids come from the image channel, since the catalog's ID column repeats across regions. `CCTV_FLORIDA_MAX_SOURCES` sets the cap (Danielslay86, #605). diff --git a/server/providers/cctv/catalog.js b/server/providers/cctv/catalog.js index 91e5336cf..0e69ac3c9 100644 --- a/server/providers/cctv/catalog.js +++ b/server/providers/cctv/catalog.js @@ -146,8 +146,9 @@ const LIVE_PACKS = [ load: loadLithuaniaSources, }, { - // FL511's terms limit its content to individual, non-commercial use, so - // the pack is off unless the operator opts in (see DATA_SOURCES.md). + // FL511's terms limit its content to individual, personal use (other use + // needs FDOT's written consent), so the pack is off unless the operator + // opts in (see DATA_SOURCES.md). name: 'florida', enabled: () => envOptIn('CCTV_FLORIDA_ENABLED'), load: loadFloridaSourcesFromOpenData, diff --git a/server/providers/cctv/constants.js b/server/providers/cctv/constants.js index 26d8b43b8..6ac545028 100644 --- a/server/providers/cctv/constants.js +++ b/server/providers/cctv/constants.js @@ -122,6 +122,8 @@ export const FLORIDA_IMAGE_HOSTS = [ export const DEFAULT_FLORIDA_MAX_SOURCES = 500; /** Byte cap per catalog page (2,000 rows, about 650 KiB measured). */ export const FLORIDA_MAX_CATALOG_BYTES = 2 * 1024 * 1024; +/** Catalog page ceiling: 40 pages covers 4,000+ cameras even at 100 rows a page. */ +export const FLORIDA_MAX_CATALOG_PAGES = 40; export const FLORIDA_ANCHORS = [ { lat: 25.7617, lon: -80.1918 }, // Miami { lat: 28.5384, lon: -81.3789 }, // Orlando diff --git a/server/providers/cctv/sources.js b/server/providers/cctv/sources.js index 91a5aa35e..2e818c046 100644 --- a/server/providers/cctv/sources.js +++ b/server/providers/cctv/sources.js @@ -60,6 +60,7 @@ import { DEFAULT_FLORIDA_MAX_SOURCES, FLORIDA_ANCHORS, FLORIDA_MAX_CATALOG_BYTES, + FLORIDA_MAX_CATALOG_PAGES, CATALONIA_CAMERAS_URL, CATALONIA_MAX_CATALOG_BYTES, CATALONIA_IMAGE_HOSTS, @@ -2883,14 +2884,18 @@ export function floridaCameraToSource(feature) { url: imageUrl, snapshotUrl: imageUrl, sourceKind: 'fl511-open-data', - license: 'FL511 (FDOT), individual non-commercial use only', + license: + 'FL511 (FDOT), individual personal use only; other use needs FDOT written consent', }; } /** * Fetch FL511 (Florida DOT) cameras, keyless: an ArcGIS FeatureServer paged - * 2,000 rows at a time. Frames are stills on the Divas Cloud hosts. A redirect - * on any page drops the pack; any other failed page keeps the rows already read. + * up to 2,000 rows at a time, ordered by the layer's object id so offsets are + * stable. Paging follows `exceededTransferLimit` and advances by the rows each + * page returned, so a lower server-side max record count still reads the whole + * layer. Frames are stills on the Divas Cloud hosts. A redirect on any page + * drops the pack; any other failed page keeps the rows already read. * * @returns {Promise>} Normalized camera source objects. */ @@ -2899,9 +2904,17 @@ export async function loadFloridaSourcesFromOpenData() { const cameras = []; const seen = new Set(); const pageSize = 2000; + let offset = 0; // Page ceiling, in case the service ignores resultOffset. - for (let offset = 0, page = 0; page < 10; offset += pageSize, page += 1) { - const url = `${FLORIDA_CCTV_URL}?where=1%3D1&outFields=*&f=json&resultRecordCount=${pageSize}&resultOffset=${offset}`; + for (let page = 0; ; page += 1) { + if (page >= FLORIDA_MAX_CATALOG_PAGES) { + console.warn( + `[CCTV] Florida catalog stopped at the ${FLORIDA_MAX_CATALOG_PAGES}-page ceiling; the catalog may be incomplete`, + ); + break; + } + // OBJECTID_1 is this layer's object id field (a plain OBJECTID is a 400). + const url = `${FLORIDA_CCTV_URL}?where=1%3D1&outFields=*&orderByFields=OBJECTID_1&f=json&resultRecordCount=${pageSize}&resultOffset=${offset}`; let resp; try { resp = await fetch(url, { @@ -2953,9 +2966,10 @@ export async function loadFloridaSourcesFromOpenData() { seen.add(camera.id); cameras.push(camera); } - // ArcGIS sets exceededTransferLimit while more rows remain. - if (feats.length < pageSize || body?.exceededTransferLimit !== true) - break; + // ArcGIS sets exceededTransferLimit while more rows remain. A page may + // be shorter than pageSize when the layer's max record count is lower. + if (body?.exceededTransferLimit !== true || feats.length === 0) break; + offset += feats.length; } const maxRaw = Number( diff --git a/src/data/cctvFlorida.test.mjs b/src/data/cctvFlorida.test.mjs index 17cd5284a..6d433a2f2 100644 --- a/src/data/cctvFlorida.test.mjs +++ b/src/data/cctvFlorida.test.mjs @@ -9,6 +9,7 @@ import { import { FLORIDA_CCTV_URL, FLORIDA_MAX_CATALOG_BYTES, + FLORIDA_MAX_CATALOG_PAGES, } from '../../server/providers/cctv/constants.js'; import { fallbackHeadingFromId, @@ -88,7 +89,8 @@ test('an FL511 feature maps to a source on a pinned HTTPS frame host', () => { assert.equal(source.url, `${IMAGE_HOST}chan-9431_h.jpg`); assert.equal(source.url, source.snapshotUrl); assert.equal(source.sourceKind, 'fl511-open-data'); - assert.match(source.license, /non-commercial/); + assert.match(source.license, /personal use only/); + assert.match(source.license, /written consent/); }); test('NOT DIRECTIONAL takes the id-hash fallback at low confidence', () => { @@ -161,7 +163,7 @@ test('isLikelyFloridaCoordinate spans the state and rejects the rest', () => { assert.equal(isLikelyFloridaCoordinate(lat, lon), false); }); -test('the loader pages until a short page and collapses repeated rows', async (t) => { +test('the loader pages until exceededTransferLimit clears and collapses repeated rows', async (t) => { t.mock.method(console, 'log', () => {}); uncapped(t); const calls = []; @@ -170,7 +172,7 @@ test('the loader pages until a short page and collapses repeated rows', async (t calls.push([offsetOf(url), init.redirect]); if (offsetOf(url) === 0) return Response.json(page(0, 2000, { exceededTransferLimit: true })); - // Short last page: two new cameras and a repeat of one already read. + // Last page (no exceededTransferLimit): two new cameras and a repeat. return Response.json(page(1999, 3)); }); const cameras = await loadFloridaSourcesFromOpenData(); @@ -182,6 +184,44 @@ test('the loader pages until a short page and collapses repeated rows', async (t assert.equal(new Set(cameras.map((camera) => camera.id)).size, 2002); }); +test('pages shorter than requested keep paging while the server says more remain', async (t) => { + t.mock.method(console, 'log', () => {}); + uncapped(t); + const offsets = []; + t.mock.method(globalThis, 'fetch', async (url) => { + const params = new URL(String(url)).searchParams; + // Offset paging is only stable over a fixed order on the object id. + assert.equal(params.get('orderByFields'), 'OBJECTID_1'); + const offset = offsetOf(url); + offsets.push(offset); + // A layer whose max record count was lowered to 1,000. + return Response.json( + page(offset, offset < 2000 ? 1000 : 500, { + exceededTransferLimit: offset < 2000, + }), + ); + }); + const cameras = await loadFloridaSourcesFromOpenData(); + assert.deepEqual(offsets, [0, 1000, 2000]); + assert.equal(cameras.length, 2500); +}); + +test('a service that ignores resultOffset stops at the page ceiling with a warning', async (t) => { + t.mock.method(console, 'log', () => {}); + const warnings = []; + t.mock.method(console, 'warn', (...args) => warnings.push(args.join(' '))); + uncapped(t); + let calls = 0; + t.mock.method(globalThis, 'fetch', async () => { + calls += 1; + return Response.json(page(0, 2000, { exceededTransferLimit: true })); + }); + const cameras = await loadFloridaSourcesFromOpenData(); + assert.equal(calls, FLORIDA_MAX_CATALOG_PAGES); + assert.equal(cameras.length, 2000); + assert.ok(warnings.some((line) => /page ceiling/.test(line))); +}); + test('a redirect on any page drops the whole pack and releases it', async (t) => { t.mock.method(console, 'warn', () => {}); const redirected = streamingResponse({ diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js index deba638e4..781db0643 100644 --- a/src/data/dataCredits.js +++ b/src/data/dataCredits.js @@ -267,8 +267,9 @@ export const DATA_CREDITS = [ key: 'florida-cctv', html: 'Traffic cameras (Florida): FL511, Florida Department of Transportation. ' + - 'Individual, non-commercial use only under the ' + - 'FL511 terms', + 'Individual, personal use only under the ' + + 'FL511 terms; ' + + "any other use needs FDOT's express written consent", }, { key: 'catalonia-cctv',