- Feed/catalog error text (shown to every signed-in user via GET /cameras and
/traffic/511 status fields) carried httpx's full request URL, including
?key= for Ontario 511 and the 511NY DAA key. Query strings are now dropped.
- TfL and Calgary licences are per GEV only (TfL's terms page 403'd), the same
footing as Ontario 511, so they are UNVERIFIED (acknowledgement required)
instead of ungated attribution_required. Attribution still renders.
- The map camera layer clamps its bbox to +/-180/+/-90; zoomed-out bounds
produced a 422 ("could not reach server").
Refs server-26#198
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
374 lines
23 KiB
Python
374 lines
23 KiB
Python
"""General / other-region camera sources -- design-canvas/camera-sources.md §C,
|
|
including every God's Eye View (GEV) source.
|
|
|
|
Endpoints and record shapes for the implemented adapters were checked
|
|
first-hand against each live catalog on 2026-10-04 (Ontario 511 excepted: it now
|
|
answers "Invalid Key" without a developer key, so its shape is the legacy CARS
|
|
v2 shape, UNVERIFIED). Imagery URLs are pinned to each agency's own hosts. None
|
|
of these cover DRB's region; they exist so a non-NY customer has a path, and
|
|
every one of them is OFF until an admin enables it.
|
|
|
|
GEV is MIT-licensed code, but nothing here is copied from it, and none of GEV's
|
|
bundled data (config/*.json curated catalogs) is used -- the MIT grant does not
|
|
cover data.
|
|
"""
|
|
import asyncio
|
|
from typing import List
|
|
|
|
from app.config import settings
|
|
from app.internal.cameras.base import (
|
|
Camera, CameraProvider, Licence, LicenceInfo, ProviderMeta, StubProvider, finite, get_json, https_on,
|
|
make_camera, plausible, require_parsed,
|
|
)
|
|
|
|
|
|
def _unverified(label: str, terms: str, url: str = None) -> LicenceInfo:
|
|
return LicenceInfo(status=Licence.UNVERIFIED, label=label, terms=terms, terms_url=url)
|
|
|
|
|
|
# ── Caltrans CWWP2 ───────────────────────────────────────────────────────────
|
|
class CaltransProvider(CameraProvider):
|
|
DISTRICTS = (4, 7, 11, 3) # SF Bay, LA, San Diego, Sacramento
|
|
meta = ProviderMeta(
|
|
id="caltrans", name="Caltrans CWWP2", region="California (districts 4, 7, 11, 3)",
|
|
coverage=(32.4, -124.6, 42.1, -114.0),
|
|
licence=_unverified(
|
|
"Licence UNVERIFIED -- likely commercial OK; Conditions of Use not yet read",
|
|
"\"Commercial Wholesale Web Portal\", \"no charge\"; Conditions of Use apply and have not been read.",
|
|
"https://cwwp2.dot.ca.gov/documentation/cctv/cctv.htm"),
|
|
formats="stills (~5 min) + HLS", implemented=True,
|
|
notes="Schema verified 2026-10-04 (D4). HLS host CORS unverified -- the player falls back to the still.",
|
|
homepage="https://cwwp2.dot.ca.gov",
|
|
)
|
|
|
|
async def _district(self, d: int) -> list:
|
|
payload = await get_json(f"https://cwwp2.dot.ca.gov/data/d{d}/cctv/cctvStatusD{d:02d}.json")
|
|
return payload.get("data") or [] if isinstance(payload, dict) else []
|
|
|
|
async def fetch_catalog(self) -> List[Camera]:
|
|
results = await asyncio.gather(*(self._district(d) for d in self.DISTRICTS), return_exceptions=True)
|
|
ok = [r for r in results if not isinstance(r, BaseException)]
|
|
if not ok:
|
|
raise RuntimeError(f"every Caltrans district failed: {results[0]}")
|
|
rows = [row for district in ok for row in district]
|
|
out: List[Camera] = []
|
|
for row in rows:
|
|
c = (row or {}).get("cctv") or {}
|
|
if str(c.get("inService", "")).lower() != "true":
|
|
continue
|
|
loc = c.get("location") or {}
|
|
lat, lon = finite(loc.get("latitude")), finite(loc.get("longitude"))
|
|
if not plausible(lat, lon, self.meta.coverage):
|
|
continue
|
|
img = https_on(((c.get("imageData") or {}).get("static") or {}).get("currentImageURL"), "dot.ca.gov")
|
|
if not img:
|
|
continue
|
|
vid = https_on((c.get("imageData") or {}).get("streamingVideoURL"), "dot.ca.gov")
|
|
name = str(loc.get("locationName") or "")
|
|
code = name.split("--", 1)[0].strip() or str(c.get("index"))
|
|
out.append(make_camera("caltrans", f"d{loc.get('district')}-{code}", lat, lon,
|
|
name=name.split("--", 1)[-1].strip() or name, roadway=str(loc.get("route") or ""),
|
|
direction=str(loc.get("direction") or ""), image_url=img, video_url=vid))
|
|
return require_parsed(len(rows), out, "Caltrans") if rows else out
|
|
|
|
|
|
# ── DelDOT ───────────────────────────────────────────────────────────────────
|
|
class DelDotProvider(CameraProvider):
|
|
meta = ProviderMeta(
|
|
id="deldot", name="DelDOT TMC", region="Delaware",
|
|
coverage=(38.4, -75.8, 39.9, -75.0),
|
|
licence=_unverified("Licence UNVERIFIED -- \"public data\", no explicit licence found",
|
|
"No licence or terms of reuse located for tmc.deldot.gov's camera catalog."),
|
|
formats="live HLS only (no stills)", implemented=True,
|
|
notes="Schema verified 2026-10-04 (361 cams); https HLS host sends ACAO: * (verified). Video-only, so a "
|
|
"camera shows nothing when its stream is down. Played directly from the browser -- no DRB egress.",
|
|
homepage="https://deldot.gov/map/",
|
|
)
|
|
|
|
async def fetch_catalog(self) -> List[Camera]:
|
|
payload = await get_json("https://tmc.deldot.gov/json/videocamera.json")
|
|
rows = payload.get("videoCameras") or [] if isinstance(payload, dict) else []
|
|
out: List[Camera] = []
|
|
for r in rows:
|
|
if r.get("enabled") is not True or r.get("status") != "Active" or not r.get("id"):
|
|
continue
|
|
lat, lon = finite(r.get("lat")), finite(r.get("lon"))
|
|
vid = https_on((r.get("urls") or {}).get("m3u8s"), "deldot.gov")
|
|
if not plausible(lat, lon, self.meta.coverage) or not vid:
|
|
continue
|
|
out.append(make_camera("deldot", r["id"], lat, lon, name=str(r.get("title") or r["id"]),
|
|
roadway=str(r.get("county") or ""), video_url=vid))
|
|
return require_parsed(len(rows), out, "DelDOT")
|
|
|
|
|
|
# ── Ontario 511 ──────────────────────────────────────────────────────────────
|
|
class Ontario511Provider(CameraProvider):
|
|
meta = ProviderMeta(
|
|
id="ontario511", name="Ontario 511", region="Ontario, Canada",
|
|
coverage=(41.6, -95.2, 57.0, -74.3),
|
|
licence=_unverified("Licence UNVERIFIED -- Ontario open-data terms page not read",
|
|
"GEV reports the Open Government Licence - Ontario; not confirmed first-hand.",
|
|
"https://511on.ca/developers/doc"),
|
|
formats="stills", implemented=True,
|
|
notes="Needs ONTARIO511_API_KEY (verified 2026-10-04: keyless now answers \"Invalid Key\"); 10 calls/60 s. "
|
|
"Record shape is the CARS v2 shape (Id/Latitude/Longitude/Views[]), UNVERIFIED since the key change.",
|
|
homepage="https://511on.ca", key_setting="ontario511_api_key",
|
|
)
|
|
|
|
async def fetch_catalog(self) -> List[Camera]:
|
|
if not settings.ontario511_api_key:
|
|
raise RuntimeError("ONTARIO511_API_KEY not set")
|
|
rows = await get_json("https://511on.ca/api/v2/get/cameras",
|
|
params={"format": "json", "lang": "en", "key": settings.ontario511_api_key})
|
|
rows = rows if isinstance(rows, list) else []
|
|
out: List[Camera] = []
|
|
for r in rows:
|
|
lat, lon = finite(r.get("Latitude")), finite(r.get("Longitude"))
|
|
if not r.get("Id") or not plausible(lat, lon, self.meta.coverage):
|
|
continue
|
|
views = [v for v in (r.get("Views") or []) if str(v.get("Status", "")).lower() == "enabled"]
|
|
img = next((https_on(v.get("Url"), "511on.ca") for v in views if https_on(v.get("Url"), "511on.ca")), None)
|
|
if not img:
|
|
continue
|
|
out.append(make_camera("ontario511", r["Id"], lat, lon, name=str(r.get("Location") or ""),
|
|
roadway=str(r.get("Roadway") or ""), direction=str(r.get("Direction") or ""),
|
|
image_url=img))
|
|
return require_parsed(len(rows), out, "Ontario 511")
|
|
|
|
|
|
# ── DriveBC ──────────────────────────────────────────────────────────────────
|
|
class DriveBcProvider(CameraProvider):
|
|
meta = ProviderMeta(
|
|
id="drivebc", name="DriveBC", region="British Columbia, Canada",
|
|
coverage=(48.2, -139.1, 60.1, -114.0),
|
|
licence=LicenceInfo(
|
|
status=Licence.ATTRIBUTION_REQUIRED, label="Commercial OK with attribution (OGL - British Columbia)",
|
|
terms="Open Government Licence - British Columbia: reuse permitted with attribution.",
|
|
terms_url="https://www2.gov.bc.ca/gov/content/data/policy-standards/open-data/open-government-licence-bc",
|
|
attribution="DriveBC -- contains information licensed under the Open Government Licence - British Columbia",
|
|
attribution_url="https://www.drivebc.ca"),
|
|
formats="stills", implemented=True, notes="Schema verified 2026-10-04 (1,066 cams).",
|
|
homepage="https://www.drivebc.ca",
|
|
)
|
|
|
|
async def fetch_catalog(self) -> List[Camera]:
|
|
rows = await get_json("https://www.drivebc.ca/api/webcams/")
|
|
rows = rows if isinstance(rows, list) else []
|
|
out: List[Camera] = []
|
|
for r in rows:
|
|
if r.get("is_on") is not True or r.get("should_appear") is not True:
|
|
continue
|
|
cid = r.get("id")
|
|
coords = (r.get("location") or {}).get("coordinates") or []
|
|
if not isinstance(cid, int) or len(coords) < 2:
|
|
continue
|
|
lon, lat = finite(coords[0]), finite(coords[1])
|
|
if not plausible(lat, lon, self.meta.coverage):
|
|
continue
|
|
out.append(make_camera("drivebc", cid, lat, lon, name=str(r.get("name") or ""),
|
|
roadway=str(r.get("highway_description") or ""),
|
|
direction=str(r.get("orientation") or ""),
|
|
image_url=f"https://www.drivebc.ca/images/{cid}.jpg"))
|
|
return require_parsed(len(rows), out, "DriveBC")
|
|
|
|
|
|
# ── Open Calgary ─────────────────────────────────────────────────────────────
|
|
class CalgaryProvider(CameraProvider):
|
|
meta = ProviderMeta(
|
|
id="calgary", name="Open Calgary traffic cameras", region="Calgary, Alberta",
|
|
coverage=(50.8, -114.5, 51.3, -113.8),
|
|
# UNVERIFIED like Ontario 511: the OGL - Calgary grant is per GEV, not read
|
|
# first-hand (camera-sources.md: "Yes, attribution (per GEV)"), so it is gated.
|
|
licence=LicenceInfo(
|
|
status=Licence.UNVERIFIED,
|
|
label="Licence UNVERIFIED -- OGL - City of Calgary per GEV, terms page not read first-hand",
|
|
terms="Open Government Licence - City of Calgary (reuse with attribution) per GEV; not confirmed first-hand.",
|
|
terms_url="https://data.calgary.ca/stories/s/Open-Calgary-Terms-of-Use/u45n-7awa",
|
|
attribution="Contains information licensed under the Open Government Licence - City of Calgary",
|
|
attribution_url="https://data.calgary.ca"),
|
|
formats="stills", implemented=True,
|
|
notes="Schema verified 2026-10-04 (216 cams). Catalog lists http:// stills; upgraded to https on the same host.",
|
|
homepage="https://data.calgary.ca",
|
|
)
|
|
|
|
async def fetch_catalog(self) -> List[Camera]:
|
|
rows = await get_json("https://data.calgary.ca/resource/k7p9-kppz.json", params={"$limit": "1000"})
|
|
rows = rows if isinstance(rows, list) else []
|
|
out: List[Camera] = []
|
|
for r in rows:
|
|
url = str((r.get("camera_url") or {}).get("url") or "")
|
|
if url.startswith("http://"):
|
|
url = "https://" + url[len("http://"):]
|
|
img = https_on(url, "trafficcam.calgary.ca")
|
|
coords = (r.get("point") or {}).get("coordinates") or []
|
|
if not img or len(coords) < 2:
|
|
continue
|
|
lon, lat = finite(coords[0]), finite(coords[1])
|
|
if not plausible(lat, lon, self.meta.coverage):
|
|
continue
|
|
cid = img.rsplit("/", 1)[-1].rsplit(".", 1)[0]
|
|
out.append(make_camera("calgary", cid, lat, lon, name=str(r.get("camera_location") or cid),
|
|
roadway=str(r.get("quadrant") or ""), image_url=img))
|
|
return require_parsed(len(rows), out, "Calgary")
|
|
|
|
|
|
# ── TfL JamCams ──────────────────────────────────────────────────────────────
|
|
class TflProvider(CameraProvider):
|
|
IMAGE_PREFIX = "https://s3-eu-west-1.amazonaws.com/jamcams.tfl.gov.uk/"
|
|
meta = ProviderMeta(
|
|
id="tfl", name="TfL JamCams", region="London, UK",
|
|
coverage=(51.2, -0.7, 51.8, 0.4),
|
|
# UNVERIFIED: the TfL terms page 403'd during the survey and the grant is
|
|
# per GEV only (camera-sources.md: "re-read"), so it is gated until re-read.
|
|
licence=LicenceInfo(
|
|
status=Licence.UNVERIFIED,
|
|
label="Licence UNVERIFIED -- commercial OK with attribution per GEV; TfL terms page not read first-hand",
|
|
terms="TfL transport data terms: free incl. commercial, with attribution (per GEV; the terms page "
|
|
"returned 403 during the survey -- re-read before selling).",
|
|
terms_url="https://tfl.gov.uk/info-for/open-data-users/our-open-data",
|
|
attribution="Powered by TfL Open Data", attribution_url="https://tfl.gov.uk"),
|
|
formats="stills (+ short MP4, unused)", implemented=True, notes="Schema verified 2026-10-04 (890 cams).",
|
|
homepage="https://api.tfl.gov.uk",
|
|
)
|
|
|
|
async def fetch_catalog(self) -> List[Camera]:
|
|
rows = await get_json("https://api.tfl.gov.uk/Place/Type/JamCam")
|
|
rows = rows if isinstance(rows, list) else []
|
|
out: List[Camera] = []
|
|
for r in rows:
|
|
props = {p.get("key"): p.get("value") for p in (r.get("additionalProperties") or []) if isinstance(p, dict)}
|
|
if str(props.get("available", "")).lower() != "true":
|
|
continue
|
|
img = str(props.get("imageUrl") or "")
|
|
lat, lon = finite(r.get("lat")), finite(r.get("lon"))
|
|
raw_id = str(r.get("id") or "").replace("JamCams_", "")
|
|
if not img.startswith(self.IMAGE_PREFIX) or not raw_id or not plausible(lat, lon, self.meta.coverage):
|
|
continue
|
|
out.append(make_camera("tfl", raw_id, lat, lon, name=str(r.get("commonName") or raw_id),
|
|
direction=str(props.get("view") or ""), image_url=img))
|
|
return require_parsed(len(rows), out, "TfL")
|
|
|
|
|
|
# ── Live Traffic NSW ─────────────────────────────────────────────────────────
|
|
class NswProvider(CameraProvider):
|
|
meta = ProviderMeta(
|
|
id="nsw", name="Live Traffic NSW", region="New South Wales, Australia",
|
|
coverage=(-37.6, 140.9, -28.0, 153.7),
|
|
licence=LicenceInfo(
|
|
status=Licence.ATTRIBUTION_REQUIRED, label="Commercial OK with attribution (CC BY 4.0)",
|
|
terms="Catalog's own rights block: copyright Transport for NSW, licence per the Open Data Hub dataset "
|
|
"(CC BY 4.0).",
|
|
terms_url="https://opendata.transport.nsw.gov.au/dataset/live-traffic-cameras",
|
|
attribution="Transport for NSW, CC BY 4.0", attribution_url="https://www.livetraffic.com"),
|
|
formats="stills", implemented=True, notes="Schema verified 2026-10-04 (241 cams).",
|
|
homepage="https://www.livetraffic.com",
|
|
)
|
|
|
|
async def fetch_catalog(self) -> List[Camera]:
|
|
payload = await get_json("https://data.livetraffic.com/cameras/traffic-cam.json")
|
|
feats = payload.get("features") or [] if isinstance(payload, dict) else []
|
|
out: List[Camera] = []
|
|
for f in feats:
|
|
p = f.get("properties") or {}
|
|
coords = (f.get("geometry") or {}).get("coordinates") or []
|
|
img = https_on(p.get("href"), "transport.nsw.gov.au")
|
|
if not img or len(coords) < 2 or not f.get("id"):
|
|
continue
|
|
lon, lat = finite(coords[0]), finite(coords[1])
|
|
if not plausible(lat, lon, self.meta.coverage):
|
|
continue
|
|
out.append(make_camera("nsw", f["id"], lat, lon, name=str(p.get("title") or ""),
|
|
roadway=str(p.get("region") or ""), direction=str(p.get("direction") or ""),
|
|
image_url=img))
|
|
return require_parsed(len(feats), out, "NSW")
|
|
|
|
|
|
# ── Fintraffic / Digitraffic weathercams ─────────────────────────────────────
|
|
class FintrafficProvider(CameraProvider):
|
|
meta = ProviderMeta(
|
|
id="fintraffic", name="Fintraffic / Digitraffic weathercams", region="Finland",
|
|
coverage=(59.5, 19.0, 70.2, 31.7),
|
|
licence=LicenceInfo(
|
|
status=Licence.ATTRIBUTION_REQUIRED, label="Commercial OK with attribution (CC BY 4.0)",
|
|
terms="\"Source: Fintraffic / digitraffic.fi, license CC 4.0 BY\".",
|
|
terms_url="https://www.digitraffic.fi/en/terms-of-service/",
|
|
attribution="Source: Fintraffic / digitraffic.fi, license CC 4.0 BY",
|
|
attribution_url="https://www.digitraffic.fi"),
|
|
formats="stills (~10 min)", implemented=True,
|
|
notes="Schema verified 2026-10-04 (810 stations, several presets each). Digitraffic asks clients to "
|
|
"identify themselves (Digitraffic-User header).",
|
|
homepage="https://www.digitraffic.fi", ttl_s=30 * 60,
|
|
)
|
|
|
|
async def fetch_catalog(self) -> List[Camera]:
|
|
payload = await get_json("https://tie.digitraffic.fi/api/weathercam/v1/stations",
|
|
headers={"Digitraffic-User": "DRB/c2-core"})
|
|
feats = payload.get("features") or [] if isinstance(payload, dict) else []
|
|
out: List[Camera] = []
|
|
for f in feats:
|
|
p = f.get("properties") or {}
|
|
coords = (f.get("geometry") or {}).get("coordinates") or []
|
|
sid = str(p.get("id") or "")
|
|
if not sid or str(p.get("collectionStatus", "")).upper() != "GATHERING" or len(coords) < 2:
|
|
continue
|
|
lon, lat = finite(coords[0]), finite(coords[1])
|
|
if not plausible(lat, lon, self.meta.coverage):
|
|
continue
|
|
for preset in p.get("presets") or []:
|
|
pid = str((preset or {}).get("id") or "")
|
|
if preset.get("inCollection") is not True or not pid.startswith(sid) or not pid[1:].isdigit():
|
|
continue
|
|
out.append(make_camera("fintraffic", pid, lat, lon, name=f"{p.get('name') or sid} ({pid[-2:]})",
|
|
image_url=f"https://weathercam.digitraffic.fi/{pid}.jpg"))
|
|
return require_parsed(len(feats), out, "Fintraffic")
|
|
|
|
|
|
# ── Stubs: catalogued, no adapter ────────────────────────────────────────────
|
|
def _stub(id, name, region, coverage, licence, formats, notes, homepage=None) -> StubProvider:
|
|
return StubProvider(ProviderMeta(id=id, name=name, region=region, coverage=coverage, licence=licence,
|
|
formats=formats, implemented=False, notes=notes, homepage=homepage))
|
|
|
|
|
|
STUBS = [
|
|
_stub("austin", "City of Austin open data", "Austin, Texas", (30.0, -98.1, 30.6, -97.5),
|
|
_unverified("Licence UNVERIFIED -- city open data, no explicit licence", "No explicit licence per GEV."),
|
|
"stills (5 min)", "Stub: Socrata column mapping not built.", "https://data.austintexas.gov"),
|
|
_stub("txdot", "TxDOT ITS", "Texas (per district)", (25.8, -106.7, 36.6, -93.5),
|
|
_unverified("Licence UNVERIFIED -- \"public data\"", "No explicit licence per GEV."),
|
|
"base64 JPEG inside JSON", "Stub: frames are base64 inside JSON, so the browser cannot show them "
|
|
"without a DRB frame proxy (video/image egress on DRB's bill -- not built).", "https://its.txdot.gov"),
|
|
_stub("md_chart", "Maryland CHART", "Maryland", (37.9, -79.5, 39.8, -75.0),
|
|
_unverified("Licence UNVERIFIED -- MD open data portal terms not read", "Not read."),
|
|
"live video URLs", "Stub: catalog schema not verified.", "https://chart.maryland.gov"),
|
|
_stub("wsdot", "WSDOT Traveler Information API", "Washington State", (45.5, -124.9, 49.1, -116.9),
|
|
_unverified("Licence UNVERIFIED -- WSDOT disclaimer only", "Free access code required; terms not read."),
|
|
"stills", "Stub: needs a WSDOT access code; schema not verified.", "https://wsdot.wa.gov/traffic/api/"),
|
|
_stub("tallinn", "Tallinn city cameras", "Tallinn, Estonia", (59.35, 24.55, 59.5, 24.95),
|
|
_unverified("Licence UNVERIFIED", "No licence found."),
|
|
"stills", "Stub: GEV uses a curated catalog file; GEV's data is not covered by its MIT licence.",
|
|
"https://ristmikud.tallinn.ee"),
|
|
_stub("tarktee", "Tarktee (Estonian Transport Administration)", "Estonia", (57.5, 21.7, 59.8, 28.3),
|
|
_unverified("Licence UNVERIFIED", "No licence found."),
|
|
"DATEX2 stills", "Stub: DATEX2 XML parser not built.", "https://tarktee.transpordiamet.ee"),
|
|
_stub("warendorf", "Warendorf municipal webcam", "Warendorf, Germany", (51.9, 7.9, 52.0, 8.1),
|
|
_unverified("Licence UNVERIFIED", "No licence found."),
|
|
"stills", "Stub: single curated webcam; no catalog API.", "https://www.warendorf.de"),
|
|
_stub("trafficland", "TrafficLand", "US (18k cams, 50+ DOTs)", (18.0, -170.0, 72.0, -60.0),
|
|
LicenceInfo(status=Licence.PAID, label="Paid commercial licence -- no contract",
|
|
terms="Licensed API; contractual redistribution with 50+ DOTs. Price UNVERIFIED.",
|
|
terms_url="http://www.trafficland.com/api.html"),
|
|
"live video", "Stub: the clean route if DRB ever needs video beyond 511NY; needs a contract."),
|
|
_stub("windy", "Windy Webcams API", "Global (scenic/weather)", (-90.0, -180.0, 90.0, 180.0),
|
|
LicenceInfo(status=Licence.PAID, label="Paid professional tier for commercial use -- no contract",
|
|
terms="Free tier image URLs expire after 10 min; commercial terms UNVERIFIED.",
|
|
terms_url="https://api.windy.com/webcams/docs"),
|
|
"tokenised stills", "Stub: scenic/weather cams, not traffic."),
|
|
_stub("earthcam", "EarthCam", "Global", (-90.0, -180.0, 90.0, 180.0),
|
|
LicenceInfo(status=Licence.PAID, label="Paid commercial licence -- no contract",
|
|
terms="Commercial licensing exists; terms UNVERIFIED."),
|
|
"live video", "Stub: low local value."),
|
|
]
|
|
|
|
PROVIDERS = [CaltransProvider(), DelDotProvider(), Ontario511Provider(), DriveBcProvider(), CalgaryProvider(),
|
|
TflProvider(), NswProvider(), FintrafficProvider(), *STUBS]
|