From 8ccb0cd89536f84415f73055d53351565a2916a4 Mon Sep 17 00:00:00 2001 From: Cliff Flood Date: Wed, 16 Sep 2026 00:34:43 -0400 Subject: [PATCH] feat(transit): register YRT/Viva off, named by Viva colour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit York Region's feed is first-party infrastructure under an open licence that permits commercial use and does not even require attribution. It is the one feed that fills the gap north of Toronto, and it is switched off anyway. The operator asks that its licence be accepted through a form collecting a name, company, phone and email. No credential is issued and nothing gates the endpoint — the licence body says use is itself acceptance — but a form asking for contact details is close enough to the registration this registry refuses that a person should decide it, not a poller. Barrie is off for the same reason. Both are one line from on. Viva route ids get labels: YRT publishes `601` where every station sign says Viva Blue. The numeric local routes already read correctly and carry no entry, so the table is six lines, not 125. No vehicle in this feed carries a bearing; heading comes from observed movement, as with Durham. --- DATA_SOURCES.md | 3 ++- docs/CURRENT-STATE.md | 4 +-- src/data/transitFeeds.js | 48 ++++++++++++++++++++++++++++++++++ src/data/transitFeeds.test.mjs | 17 ++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 0c245c828..54358e75c 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -84,6 +84,7 @@ How to read this: | **Guelph Transit** (glphtrpcloud.com) | Transit layer live vehicles, Guelph ON | Guelph Transit open realtime vehicle data, published by the City of Guelph on its open-data developer page and used for the live view. Its route ids are internal database keys, so the 33-row route-number table in `src/data/transitFeeds.js` is derived from the City’s own published GTFS `routes.txt` under the same licence. | "Contains information licensed under the Open Government Licence – City of Guelph" — wording prescribed by the licence | | **Kingston Transit** (api.cityofkingston.ca) | Transit layer live vehicles, Kingston ON | Kingston Transit open realtime vehicle data, catalogued by the City of Kingston as open data and used for the live view. | "Contains information licensed under the Open Data Licence – City of Kingston" — wording prescribed by the licence | | **Barrie Transit** (myridebarrie.ca) | REGISTERED BUT OFF — not polled, not offered to the browser | Barrie Transit realtime data, published by the City of Barrie behind a click-through acceptance of its licence. Registered so the decision is visible; it ships unpolled until a person accepts those terms. The licence is revocable and requires no attribution. | Courtesy credit to the City of Barrie (the licence requires none, and its trademark clause is broad) | +| **YRT/Viva** (rtu.york.ca) | REGISTERED BUT OFF — not polled, not offered to the browser | York Region Transit realtime data, first-party York Region infrastructure under the YRT Open Data Licence v2.0, which permits commercial use and makes attribution optional. Registered unpolled because the operator asks that the licence be accepted through a contact form first. Viva route ids are named by colour from York Region’s published GTFS. | "Contains public transit Information made available under YRT’s Open Data Licence" — the licence’s own wording, given though not required | | **Radio Browser** | Geolocated internet-radio station directory and station-level tags | Public-domain directory data under PDDL 1.0; individual broadcaster stream terms apply | "Radio Browser" plus a link to the selected broadcaster | | **Re:Earth Terrain** (Mapterhorn) | Terrain (keyless globe stacks — OSM etc. — + `/api/terrain/heights` ellipsoidal-height lookups) | Terrain mesh: CC BY 4.0; geoid: EGM2008 (NGA, public domain) | "Terrain (keyless globe stacks): Re:Earth Terrain / Mapterhorn (CC BY 4.0) / EGM2008 (NGA)" | | **OSRM on the FOSSGIS routing servers** (`routing.openstreetmap.de`) | Street-following routes for the Directions layer and voice route annotations, via `/api/route` | [FOSSGIS routing usage policy](https://routing.openstreetmap.de/about.html): "Display the required attribution and display a link to 'fix the map'", "Use a valid user agent and, if applicable, a correct referrer", "One request per second max", "No scraping, no heavy usage". The full policy is the German [FOSSGIS Nutzungsbedingungen](https://www.fossgis.de/arbeitsgruppen/osm-server/nutzungsbedingungen/); FOSSGIS also states that the server may be embedded in your own pages but "eine gewerbliche Nutzung ist nur mit Einschränkungen erlaubt" (commercial use only with restrictions). Route data derives from OpenStreetMap (ODbL 1.0) | "Routing: OSRM on the FOSSGIS servers" + a "fix the map" link — shown in the Data attribution popover | @@ -162,7 +163,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **NYC DOT Traffic Management Center cameras.** Off by default. NYC DOT's [developer data feeds page](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) asks developers who want the camera feed to contact `tmcdot@dot.nyc.gov` and sign a data-sharing agreement. This project does not hold one that covers downstream installations, so the pack loads only when an operator who holds their own agreement sets `CCTV_NYC_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to NYC DOT. The endpoint needs no key. The camera list is the public `webcams.nyctmc.org/api/cameras` catalog (~980 cameras across the five boroughs); frames are stills at `webcams.nyctmc.org/api/cameras//image`, synthesized from a validated UUID rather than taken from the payload, and only that host is registered. Cameras the feed reports as offline are dropped. The catalog publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. Once enabled, the 250 cameras nearest the borough cores load; `CCTV_NYC_MAX_SOURCES` (8–400) changes the cap. Frames are fetched at request time and never stored or redistributed. NYC DOT asks every application that uses its data to carry its disclaimer; it is registered verbatim in the Data attribution popover. - **City of Toronto RESCU cameras.** Keyless. The camera list is the City's public `tmcearthcameras.json` on `opendata.toronto.ca` (dataset "Traffic Cameras", Transportation Services), served as JSONP and unwrapped with an anchored pattern. This is the municipal network the Ontario 511 pack does not carry: downtown arterials plus the Gardiner, DVP, Allen and Lake Shore. The list publishes no frame URL, so frame URLs are BUILT from the documented `CameraImages/loc####.jpg` convention and a strictly-digits camera number, pinning the frame proxy to the portal origin by construction; the list fetch refuses redirects and bounds the body it buffers. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `D1`-`D4` fields are the directions of the *comparison* images (static reference stills captured in 2016 under a different path), not a bearing for the live pan-tilt-zoom frame, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. A camera that is temporarily out of service still answers 200 with a byte-identical "Image Unavailable" placeholder rather than an error (about 6% of the network at any moment), so such a camera reads as healthy; the frame it shows is the agency's, not a synthesized one. The licence requires the attribution statement above. Disable with `CCTV_TORONTO_ENABLED=0`. - **Camera frame content.** Public camera frames can contain people, vehicles and license plates, and the camera layer does nothing with them beyond putting them on the map. A successful upstream response is relayed as the provider served it: nothing in the camera pipeline blurs, redacts, enhances or restores it, and nothing there runs face, plate or object detection or otherwise recognises or classifies what is in the picture. (The ALPR layer described below maps where plate-reader cameras are, from locations contributors tagged in OpenStreetMap; it reads nothing from any camera frame.) When an upstream has no frame for a camera, what you see instead is a Google Street View still or the app's own placeholder card; the panel's source badge names which of them you are looking at, and a refresh that fails after a frame has arrived leaves the last good picture on screen rather than replacing it. The browser resamples each frame for display: onto a fixed 1920x1080 surface for the camera's projection plane in the scene, and onto a 192x108 canvas for the small ambient card. That scaling is the only change made to the picture, and the only pixels the camera layer reads back are a 64x36 downscale hashed to tell whether the picture has changed. Live streams are piped through; the still path reads the image into memory to pass it on — TxDOT wraps its JPEG in a JSON envelope, which is decoded there — and no frame is written to disk (the disk cache, `.gev-cache/`, holds feed JSON for other layers and never camera imagery). Outside that pipeline, the voice assistant is the one feature that sends imagery anywhere: when it answers a question about what is in view it may attach a downscaled screenshot of the rendered scene — including any camera plane on screen — to the request it sends to the voice model. Frames are fetched only from server-registered camera URLs — a client cannot hand the proxy a URL (see [SECURITY.md](SECURITY.md)). What the app does stays inside the line README draws: no named-person search, no face recognition, no tracking individuals. -- **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. Thirteen of the fourteen registered regions are live-enabled (Barrie is registered and deliberately off); each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. The TTC feed is BusTime, the surface system: it carries buses and streetcars and no subway at all. About four in ten of its vehicles report no route at all (unassigned or deadheading), and those are shown with the feed default rather than a route-established mode. It sends no ETag or Last-Modified, so its conditional requests fall through to the ordinary snapshot cache. Three Golden Horseshoe neighbours run alongside it — MiWay, Hamilton HSR and Durham Region Transit — each bus-only and each polled only near its own region. Durham Region Transit publishes no bearing on any vehicle, so those sprites take their heading from observed movement like any other feed that omits it. Guelph, Kingston and Barrie extend the same pattern eastward and north. Guelph is the one feed whose route ids are internal keys rather than the number on the bus, so its entry carries a route-number table taken from the City’s own published GTFS; any id missing from that table renders unchanged rather than blank. GO Transit / Metrolinx is deliberately absent: its REALTIME feed requires an API key issued by manual review, which the keyless admission rule excludes (its static schedule download is keyless, but carries no vehicle positions). Grand River Transit in Waterloo is absent for a different reason: it answers, but its feed header was months stale and it publishes no per-vehicle timestamps, so it would render vehicles that look live and are not. +- **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. Thirteen of the fifteen registered regions are live-enabled; Barrie and YRT/Viva are registered and deliberately off; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. The TTC feed is BusTime, the surface system: it carries buses and streetcars and no subway at all. About four in ten of its vehicles report no route at all (unassigned or deadheading), and those are shown with the feed default rather than a route-established mode. It sends no ETag or Last-Modified, so its conditional requests fall through to the ordinary snapshot cache. Three Golden Horseshoe neighbours run alongside it — MiWay, Hamilton HSR and Durham Region Transit — each bus-only and each polled only near its own region. Durham Region Transit publishes no bearing on any vehicle, so those sprites take their heading from observed movement like any other feed that omits it. Guelph, Kingston and Barrie extend the same pattern eastward and north. Guelph is the one feed whose route ids are internal keys rather than the number on the bus, so its entry carries a route-number table taken from the City’s own published GTFS; any id missing from that table renders unchanged rather than blank. GO Transit / Metrolinx is deliberately absent: its REALTIME feed requires an API key issued by manual review, which the keyless admission rule excludes (its static schedule download is keyless, but carries no vehicle positions). Two feeds are registered and switched off rather than absent: Barrie and YRT/Viva both ask that their licence be accepted through a form before the data is taken, and that is a person’s decision rather than an assumption a poller should make. Grand River Transit in Waterloo is absent for a different reason: it answers, but its feed header was months stale and it publishes no per-vehicle timestamps, so it would render vehicles that look live and are not. - **Radio Browser.** `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and malformed or empty successful payloads are rejected atomically. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply. - **TomTom Traffic.** Optional and BYOK: without `TOMTOM_API_KEY` the traffic layer runs its built-in simulation and no TomTom data (or attribution) appears. With a key, flow vector tiles are fetched through the server-side `/api/tomtom` proxy (120 s cache + a daily tile-budget governor — `TOMTOM_DAILY_TILE_BUDGET`, default 6,000 — sized so a 31-day month stays inside TomTom's 200K monthly free allowance; still a configurable application safety ceiling, not a guarantee; TomTom's [current pricing](https://docs.tomtom.com/pricing/) lists 200K free tile requests per month) and the "Traffic flow data © TomTom" credit is registered in the Data attribution popover the moment live mode activates. TomTom data is served live and cached only transiently (≤120 s TTL under `.gev-cache/`, gitignored) — it is not bundled or redistributed. One 23 KB point-in-time tile snapshot is committed as a decode-test fixture (`src/data/fixtures/`, © TomTom, never served to the app). - **Routing (OSRM / FOSSGIS).** `/api/route` proxies the public OSRM instances FOSSGIS e.V. runs for the OpenStreetMap community (car, bike, foot profiles). They are keyless and intended for fair, interactive use — the proxy rate-limits clients (60/min, 200/min total), caps route span and response size, caches each route for 10 minutes, and identifies itself in the User-Agent. Heavy or automated use must run its own OSRM. Routes are computed from OpenStreetMap data (ODbL); the Directions layer and voice routes never redistribute them. A route the router cannot find is reported as such; the Directions layer draws nothing in that case, and voice routes fall back to a clearly labeled straight line. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 1e1651e89..c6910dcc0 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3663,8 +3663,8 @@ gate. Thirteen keyless regions are available: Boston, Austin, Minneapolis–St P Helsinki, the Netherlands, Norway, South East Queensland, Toronto (TTC surface vehicles; the subway publishes no positions), the Golden Horseshoe neighbours MiWay, Hamilton HSR and Durham Region Transit, and Guelph and -Kingston. Barrie is registered but ships off, pending acceptance of its -click-through licence. Mode silhouettes +Kingston. Barrie and YRT/Viva are registered but ship off, +pending acceptance of their licences. Mode silhouettes identify buses, trams, subways, trains, ferries and unclassified vehicles. Retained transit history uses the same outlier quarantine as live observations and diff --git a/src/data/transitFeeds.js b/src/data/transitFeeds.js index 0083d3b3e..cb6c3ef0b 100644 --- a/src/data/transitFeeds.js +++ b/src/data/transitFeeds.js @@ -184,6 +184,31 @@ function guelphRouteLabel(routeId) { return GUELPH_ROUTE_NAMES[routeId] || routeId; } +/** + * YRT publishes Viva bus rapid transit by route id (`601`), while every rider, + * station sign and map calls it "Viva Blue". The local routes are already the + * number on the bus and are left alone. Colours are from York Region's own + * published GTFS `routes.txt`; Viva Green (604) and Pink (606) are not in + * current service and so are absent here too. + */ +const YRT_VIVA_ROUTE_NAMES = Object.freeze({ + 601: 'Viva Blue', + 60102: 'Viva Blue B', + 603: 'Viva Purple', + 60301: 'Viva Purple A', + 605: 'Viva Orange', + 607: 'Viva Yellow', +}); + +/** + * YRT route id to the name a rider would say. + * @param {string|null} routeId + * @returns {string|null} + */ +function yrtRouteLabel(routeId) { + return YRT_VIVA_ROUTE_NAMES[routeId] || routeId; +} + /** * Registry of feeds. Order is presentation order in the stats/credit text. * `loadRadiusKm` is the distance from `center` inside which the feed is polled. @@ -490,6 +515,29 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([ }), defaultMode: 'bus', }), + Object.freeze({ + id: 'yrt-york', + name: 'YRT/Viva', + operator: + 'York Region Transit (YRT/Viva), The Regional Municipality of York', + region: 'York Region, ON', + center: Object.freeze({ lat: 44.025, lon: -79.436 }), + loadRadiusKm: 38, + url: 'https://rtu.york.ca/gtfsrealtime/VehiclePositions', + license: 'YRT Open Data Licence v2.0 (UK OGL-derived)', + licenseUrl: + 'https://www.yrt.ca/en/about-us/open-data-licence-agreement.aspx', + attribution: + "Contains public transit Information made available under YRT's Open Data Licence", + defaultEnabled: false, + terms: Object.freeze({ + quote: + 'YRT grants You a worldwide, royalty-free, perpetual, non-exclusive licence to Use the Information subject to the conditions below ... Use the Information commercially ... Provide credit to YRT when using Information under this licence but are not required to. ... Please note: You must agree with and accept the above stated License Agreement in order to download YRT GTFS or real-time GTFS data.', + note: 'OFF for the same reason as Barrie: the operator asks that its licence be accepted through a form (forms.yrt.ca/YRT-GTFS-Data, which collects a name, company, phone and email and issues no credential) before the data is taken. Nothing gates the endpoint, and the licence body also says use is itself acceptance \u2014 but a form asking for contact details is close enough to the registration this registry refuses that a person, not a poller, should decide. Flip this one line once someone has answered it. The credit above is the licence\u2019s own wording and must be reproduced as given; attribution is optional here, and given anyway. Logos, trademarks and crests are carved out, so the credit stays text. Two data notes: no vehicle carries a bearing, so heading comes from observed movement; and one stuck unit reported a position ten hours old, which the layer shows as a stale report rather than hides.', + }), + defaultMode: 'bus', + routeLabel: yrtRouteLabel, + }), ]); /** diff --git a/src/data/transitFeeds.test.mjs b/src/data/transitFeeds.test.mjs index 08c86759a..711177a03 100644 --- a/src/data/transitFeeds.test.mjs +++ b/src/data/transitFeeds.test.mjs @@ -390,3 +390,20 @@ test('Barrie is registered but unpolled until its licence is accepted', () => { 'never offered to the browser', ); }); + +test('YRT is registered but unpolled until its licence form is answered', () => { + assert.equal(getRegisteredTransitFeed('yrt-york')?.name, 'YRT/Viva'); + assert.equal(getTransitFeed('yrt-york'), null, 'not routable'); +}); + +test('Viva bus rapid transit is named by its colour, not its route number', () => { + // YRT publishes `601` where every rider, map and station sign says + // "Viva Blue". The numeric local routes already read correctly and are + // left alone. + const yrt = getRegisteredTransitFeed('yrt-york'); + assert.equal(transitRouteLabel(yrt, '601'), 'Viva Blue'); + assert.equal(transitRouteLabel(yrt, '60301'), 'Viva Purple A'); + assert.equal(transitRouteLabel(yrt, '607'), 'Viva Yellow'); + assert.equal(transitRouteLabel(yrt, '105'), '105', 'a local bus is a number'); + assert.equal(transitRouteLabel(yrt, '9899'), '9899'); +});