Compare commits

..
Author SHA1 Message Date
German Khokhlov 73bd952736 Share one render loop fixture across the render failure tests
Four test files built the same widget on CesiumWidget.prototype, viewer
proxy, store and notice; renderLoopFixture.mjs now does it once. The notice
test drives Cesium.Event instead of a hand-made double. frameEvents() drops
tileset.tileLoad, which Cesium raises inside its own error handling, and the
notice no longer guards a viewer without a canvas or requestRender.
2026-11-13 20:16:08 +04:00
German Khokhlov df41c0affb Shorten the render failure notice's comments and docs
Comments keep only the why: the loop-running flag, the mid-raise events and
the one-time canvas focus. The CURRENT-STATE section lists behaviour and
pitfalls instead of retelling the code, and no longer says every tileset
event is raised outside Scene's error handling.
2026-11-13 19:19:15 +04:00
German Khokhlov d42b6fe764 Merge branch 'main' into german/map-render-error-state 2026-11-13 18:47:36 +04:00
German Khokhlov 29cf9ff8f6 Add the render failure notice in Swedish 2026-11-11 19:37:06 +04:00
German Khokhlov 897a74ea27 Merge remote-tracking branch 'origin/main' into german/map-render-error-state
# Conflicts:
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/he.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
2026-11-11 19:33:49 +04:00
German Khokhlov bd1da596b8 Keep the draw tool test file in its own formatting 2026-11-11 19:25:02 +04:00
German Khokhlov 6976cbc941 Hand focus back to the globe canvas after a render failure
When the control that had focus is gone or cannot take it, focus went to
the #cesiumContainer div, where Enter no longer finished a drawing. It now
goes to the globe's canvas, which the map's keys read as the map; the canvas
is focusable only until focus moves on or the globe is pressed. A control
disabled while the notice was up no longer leaves focus on the page, and
neither focus call scrolls.
2026-11-11 18:03:30 +04:00
German Khokhlov 771ed98e3b Add the render failure notice in Hebrew 2026-11-11 12:48:44 +04:00
German Khokhlov 282ad6c5f6 Merge remote-tracking branch 'origin/main' into german/map-render-error-state
# Conflicts:
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
2026-11-11 12:45:05 +04:00
German Khokhlov d1f4d1acdc Say the map is not updated in the Arabic render failure notice 2026-11-11 12:44:08 +04:00
German Khokhlov ebaa439739 Tighten the render failure notice's error handling and focus
- An error a frame of the inline panel's timer throws no longer reaches the
  host page; it has been raised as a render error by then.
- An error Scene raised and threw on (rethrowRenderErrors) is raised once.
- After a lost context, later render errors are only logged, so the notice
  keeps saying why.
- A render error without a value shows no detail.
- The globe's tile load progress and the scene's tileset load events are
  finished too when a listener's error cuts them short.
- Focus goes back where it was when the globe draws again, or to the map.
- The appearance counter and the guard for a missing viewer are gone.

Tests cover every finished event, a thrown timer frame, a single report under
rethrowRenderErrors and the focus return; the inline tests share one setup.
2026-11-11 12:44:08 +04:00
German Khokhlov 265074aa54 Raise the render failure notice above floating panels
The notice sat at z-index 1001, under the Québec camera viewer (2000), which
covered its text and, with video playing, its buttons on a phone. It now sits
at 2001. On touch screens its buttons are 44 px tall.
2026-11-11 12:44:08 +04:00
German Khokhlov c763c38193 Keep the render loop marked running after a frame Retry could race
An error Scene catches itself marks Cesium's render loop stopped while the
loop's next animation frame is still queued. A Retry, or a tab shown again,
before that frame ran started a second loop, and the globe drew every frame
twice. The render and resize wrapper now keeps the loop marked running after
a call made while it ran; the queued frame stops it if nothing restarted it.
This also covers the inline panel's timer frames, so its own guard goes.
2026-11-10 16:40:22 +04:00
German Khokhlov 2448662574 Merge remote-tracking branch 'origin/main' into german/map-render-error-state
# Conflicts:
#	src/app/scene.js
#	src/app/viewer.js
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
#	src/ui/views/SceneChromeView.vue
2026-11-10 14:29:34 +04:00
German Khokhlov d9e34f10c6 Finish an interrupted frame event as soon as its error surfaces
Retry used to apply the queued listener changes of a Cesium event a
throwing listener left mid-raise, adds first and removes second. A layer
turned off and on while the notice was up landed in both queues and was
removed. The render and resize wrapper now finishes those events before
it raises the render error, so later adds and removes apply at once and
in order.
2026-11-10 14:28:01 +04:00
German Khokhlov 02a8442e6e Let Retry finish a frame event a throwing listener cut short
Cesium's Event.raiseEvent leaves an event mid-raise when a listener throws, so listeners added or removed afterwards stay queued. Retry now applies that queue for the frame and clock events: the notice's frame watchers see the next frame, and a listener removed since, such as a layer turned off, is not called again.
2026-11-06 14:45:42 +04:00
German Khokhlov 628de8386d Report thrown render loop errors and keep one loop after an inline Retry
An error thrown outside Scene's own error handling (a postRender listener,
the clock's onTick, a resize) stopped Cesium's loop without a renderError,
and with Cesium's panel turned off the globe froze silently. The notice now
raises such an error as a render error.

In an inline panel whose host stopped animation frames, Retry left the
panel's timer latched off, and once frames resumed a second render loop
drew every frame twice. The timer now follows Cesium's loop, and keeps the
loop marked running when its next frame is still queued.
2026-11-05 21:21:03 +04:00
German Khokhlov abfc85e03d Add the changelog entry for #1579 2026-11-05 13:03:42 +04:00
German Khokhlov fbec1488ac Show a recoverable notice when the globe stops drawing
Cesium stops its render loop after a render error and offers only a
panel that dismisses itself, and a lost WebGL context never draws again,
so the globe froze or went black while the controls kept responding.

The scene chrome now shows an alert dialog with the error. Retry restarts
the render loop and keeps the notice until a frame renders without an
error; after a lost context it reloads the page, whose address keeps the
view. Reset view opens the app root without the restored hash or share
path. An inline conversation panel never reloads its host page. The
application viewer turns off Cesium's own panel; other callers keep it.
2026-11-05 13:03:14 +04:00
54 changed files with 1493 additions and 8614 deletions

No files matched your search

+1 -307
View File
@@ -175,312 +175,6 @@ How to read this:
| **Toulon Mistral** (feed-rdtpm-toulon.ratpdev.com) | Transit layer live vehicles, Toulon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-de-transport-urbain-de-la-metropole-toulon-provence-mediterranee). Dataset record’s licence field quoted. Speeds are km/h: readings ran 3.5 times the measured movement on 2026-11-07, so the proxy converts them. Route ids are zero-padded. | Métropole Toulon Provence Méditerranée (Mistral) |
| **Le Havre LiA** (opendata.transports-lia.fr) | Transit layer live vehicles, Le Havre, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-de-transport-en-commun-lia-gtfs-siri). The network publishes under ODbL (dataset record quoted); and this is its own GTFS-RT vehicle feed from that record; route ids carry the network prefix `8-`. The layer draws positions and redistributes no database. Lines A and B are the tramway. | Le Havre Seine Métropole (LiA) |
| **Tours Fil Bleu** (data.filbleu.fr) | Transit layer live vehicles, Tours, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/fil-bleu-syndicat-des-mobilites-gtfs-gtfs-rt). Dataset record’s licence field quoted. | Fil Bleu – Syndicat des Mobilités de Touraine |
| **Agde Cap’Bus** (zenbus.net) | Transit layer live vehicles, Agde, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-capbus). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (6 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Cap’Bus – Communauté d’agglomération Hérault-Méditerranée |
| **Agen Tempo** (api.pysae.com) | Transit layer live vehicles, Agen, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/agen-gtfs-urbain). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (33 vehicles on 2026-11-13). | Tempo Bus – Agglomération d’Agen |
| **Alès Alesy** (alesy.plateforme-2cloud.com) | Transit layer live vehicles, Alès, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-des-lignes-urbaines-et-interurbaines-sur-le-reseau-alesy-gtfs). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (62 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. | Alesy – Syndicat mixte transports publics du bassin d’Alès |
| **Aléop coaches** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Pays de la Loire, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/arrets-horaires-et-circuits-des-lignes-de-transports-aleop-1). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (103 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Route `YEU` is the Île d’Yeu boat. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Aléop - Pays de la Loire – Pays de la Loire |
| **Amiens Ametis** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Amiens, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/ametis). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (95 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | Ametis – Communauté d’agglomération Amiens Métropole |
| **Angers Irigo** (ara-api.enroute.mobi) | Transit layer live vehicles, Angers, France | ODbL (https://transport.data.gouv.fr/datasets/angers-loire-metropole-reseau-irigo-gtfs-gtfs-rt-siri). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (214 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Lines A, B and C are the tramway. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Irigo – Communauté urbaine Angers Loire Métropole |
| **Arles Envia** (accm.plateforme-2cloud.com) | Transit layer live vehicles, Arles, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-statiques-et-temps-reels-reseau-envia). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (33 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Envia – Communauté d’agglomération d’Arles-Crau-Camargue-Montagnette |
| **Arras Artis** (api.pysae.com) | Transit layer live vehicles, Arras, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-de-transport-artis). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (5 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Artis – Communauté urbaine d’Arras |
| **Aubenas Tout’enbus** (gtfs-rt.infra-hubup.fr) | Transit layer live vehicles, Aubenas, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (5 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Tout’enbus – Région Auvergne-Rhône-Alpes (CC du Bassin d’Aubenas) |
| **Auch Alliance** (zenbus.net) | Transit layer live vehicles, Auch, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-reseau-alliance-bus-grand-auch). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (7 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Alliance – Communauté d’agglomération Grand Auch Coeur de Gascogne |
| **Auray Glazgo** (api.pysae.com) | Transit layer live vehicles, Auray, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-glazgo). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (4 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Glazgo – Communauté de communes Auray Quiberon Terre Atlantique |
| **Avignon Orizo** (api.maas-fr.cityway.fr) | Transit layer live vehicles, Avignon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-orizo-grand-avignon). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (90 vehicles on 2026-11-13). Line T1 is the tramway. | Orizo – Communauté d’agglomération du Grand Avignon (COGA) |
| **Barentin MOCA** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Barentin, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/moca-communaute-de-communes-caux-austreberthe). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Published through the Atoumod (Normandy) hub; its positions stalled for about ten minutes once on 2026-11-13. | MOCA – Communauté de communes Caux - Austreberthe |
| **Beaune Côte&Bus** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Beaune, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/offre-de-transport-de-la-c-a-beaune-cote-sud-gtfs). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (5 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | Côte&Bus – Communauté d’agglomération Beaune, Côte et Sud - Communauté Beaune-Chagny-Nolay |
| **Beauvais Corolis** (api.oisemob.cityway.fr) | Transit layer live vehicles, Beauvais, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-corolis-urbain-communaute-dagglomeration-du-beauvaisis). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (23 vehicles on 2026-11-13). | Corolis – Communauté d’agglomération du Beauvaisis |
| **Beauvaisis Corolis interurbain** (api.oisemob.cityway.fr) | Transit layer live vehicles, Beauvais, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-corolis-interurbain-communaute-dagglomeration-du-beauvaisis). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (13 vehicles on 2026-11-13). | Corolis – Communauté d’agglomération du Beauvaisis |
| **Bernay l’Bus** (zenbus.net) | Transit layer live vehicles, Bernay, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-transport-urbain-de-bernay). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (1 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | l’Bus – Communauté de communes Intercom Bernay Terres de Normandie |
| **Blois Azalys** (app.mecatran.com) | Transit layer live vehicles, Blois, France | ODbL (https://transport.data.gouv.fr/datasets/agglopolys-offre-theorique-mobilite-reseau-urbain-azalys-de-blois). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (55 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Speeds are km/h: readings ran 6.63 times the measured movement on 2026-11-13, so the proxy converts them. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Azalys – Communauté d’agglomération de Blois "Agglopolys" |
| **Bourg-en-Bresse Rubis** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Bourg-en-Bresse, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/rubis-grand-bourg-mobilites-offre-de-transport-theorique-et-temps-reel). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (34 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Speeds are km/h: readings ran 4.99 times the measured movement on 2026-11-13, so the proxy converts them. The feed carries no route ids. | Rubis – Communauté d’agglomération du Bassin de Bourg-en-Bresse |
| **Bourgoin-Jallieu Ruban** (app.pysae.com) | Transit layer live vehicles, Bourgoin-Jallieu, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-ruban). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (28 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. | Ruban – Communauté d’agglomération Porte de l’Isère (CAPI) |
| **Brest Bibus** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Brest, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-des-bus-et-tramways-circulant-sur-le-territoire-de-brest-metropole). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (113 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Lines A and B are the tramway; line C is the cable car, which has no mode of its own here and draws as a bus. Vehicles carry no timestamp of their own, so the feed header’s is used. | Bibus – Brest Métropole |
| **Béziers beeMob** (feed-beemob-beziers.ratpdev.com) | Transit layer live vehicles, Béziers, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-du-reseau-de-transports-beemob-beziers-mediterranee). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (40 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. | beeMob – Communauté d’agglomération de Béziers-Méditerranée |
| **Cannes Palm Bus** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Cannes, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-gtfs-gtfs-rt-du-reseau-palmbus-cannes-pays-de-lerins). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (86 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Palmbus – Communauté d’agglomération Cannes Pays de Lérins |
| **Carcassonne RTCA** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Carcassonne, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-rtca). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (23 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | RTCA – Communauté d’agglomération Carcassonne Agglo |
| **Caux Seine Rezo’Bus** (app.pysae.com) | Transit layer live vehicles, Lillebonne, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-caux-seine-mobilite-rezobus). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. | REZO’BUS – Communauté d’agglomération Caux Seine Agglo |
| **Cavaillon C’mon bus** (app.pysae.com) | Transit layer live vehicles, Cavaillon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-reseau-cmonbus). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (11 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Cmonbus – Communauté d’agglomération Luberon Monts de Vaucluse |
| **Cayenne Agglo’bus** (cacl.plateforme-2cloud.com) | Transit layer live vehicles, Cayenne, French Guiana | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/agence-agglobus). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (44 vehicles on 2026-11-13). | Agglobus – Communauté d’agglomération du Centre Littoral |
| **Chamonix buses** (api.pysae.com) | Transit layer live vehicles, Chamonix-Mont-Blanc, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-reseau-chamonix-mobilite). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (11 vehicles on 2026-11-13). | Communauté de communes de la Vallée de Chamonix-Mont-Blanc |
| **Chantilly Aire’MOB** (api.oisemob.cityway.fr) | Transit layer live vehicles, Chantilly, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-airemob-communaute-de-communes-de-laire-cantilienne). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). | Aire’MOB – Communauté de communes de l’Aire Cantilienne |
| **Cherbourg Cap Cotentin** (app.pysae.com) | Transit layer live vehicles, Cherbourg-en-Cotentin, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-en-temps-reel-des-bus-et-autocars-circulant-sur-le-reseau-cap-cotentin). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (53 vehicles on 2026-11-13). | Cap Cotentin – Communauté d’agglomération du Cotentin |
| **Chinon CVL Mobilité** (cvlmobilite.plateforme-2cloud.com) | Transit layer live vehicles, Chinon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-cvlmobilite-plan-de-transport-theorique-ligne-a-format-gtfs). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (7 vehicles on 2026-11-13). | CVL Mobilité – Communauté de communes Chinon, Vienne et Loire |
| **Clermont (Oise) Le Bus** (api.oisemob.cityway.fr) | Transit layer live vehicles, Clermont, Oise, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-le-bus-communaute-de-communes-du-clermontois). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (4 vehicles on 2026-11-13). | Le Bus – Communauté de communes du Clermontois |
| **Compiègne TIC** (api.oisemob.cityway.fr) | Transit layer live vehicles, Compiègne, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-tic-urbain-communaute-dagglomeration-de-la-region-de-compiegne-et-de-la-basse-automne). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (19 vehicles on 2026-11-13). | TIC – Communauté d’agglomération de la Région de Compiègne et de la Basse Automne |
| **Compiègne TIC interurbain** (api.oisemob.cityway.fr) | Transit layer live vehicles, Compiègne, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-tic-interurbain-communaute-dagglomeration-de-la-region-de-compiegne-et-de-la-basse-automne). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (21 vehicles on 2026-11-13). | TIC – Communauté d’agglomération de la Région de Compiègne et de la Basse Automne |
| **Cosne-sur-Loire Papillon** (api.pysae.com) | Transit layer live vehicles, Cosne-Cours-sur-Loire, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-du-reseau-de-bus-papillon-a-cosne-cours-sur-loire). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Papillon – Communauté de communes Coeur de Loire |
| **Creil AXO** (api.oisemob.cityway.fr) | Transit layer live vehicles, Creil, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-axo-communaute-dagglomeration-creil-sud-oise). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (28 vehicles on 2026-11-13). | AXO – Communauté d’agglomération Creil Sud Oise |
| **Crépy-en-Valois Mobi** (api.oisemob.cityway.fr) | Transit layer live vehicles, Crépy-en-Valois, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-mobi-communaute-de-communes-du-pays-de-valois). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (5 vehicles on 2026-11-13). | Mobi – Communauté de communes du Pays de Valois |
| **Cœur du Var Le TACO** (api.pysae.com) | Transit layer live vehicles, Le Luc, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/le-taco-1). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (1 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Le TACO – Région Provence-Alpes-Côte d’Azur (CC Coeur du Var) |
| **Dieppe DeepMob** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Dieppe, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/deepmob-dieppe-maritime). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (12 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Published through the Atoumod (Normandy) hub; its positions stalled for about ten minutes once on 2026-11-13. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | DeepMob – Communauté d’agglomération de la Région Dieppoise |
| **Dijon Divia** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Dijon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-diviamobilites). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (149 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Speeds are km/h: readings ran 4.42 times the measured movement on 2026-11-13, so the proxy converts them. Lines T1 and T2 are the tramway. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | DiviaMobilités – Dijon Métropole |
| **Dole Grandole Mobilités** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Dole, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-de-transport-du-grand-dole). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (18 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | Grandole Mobilités – Communauté d’agglomération du Grand Dole |
| **Draguignan TEDbus** (pysae.com) | Transit layer live vehicles, Draguignan, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-tedbus-lignes-regulieres-dracenie-provence-verdon-agglomeration). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (15 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | TEDbus – Dracénie Provence Verdon Agglomération |
| **Figeac Le Bus** (zenbus.net) | Transit layer live vehicles, Figeac, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-du-reseau-figeac-le-bus-gtfs). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (3 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Figeac - Le Bus – Région Occitanie (CC Grand-Figeac) |
| **Flers Némus** (flers-agglo.plateforme-2cloud.com) | Transit layer live vehicles, Flers, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-du-reseau-urbain-de-flers-agglo-au-format-gtfs). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (5 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Nemus – Communauté d’agglomération Flers Agglo |
| **Fougères SURF** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Fougères, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-surf). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (8 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Served by the national access point’s own proxy of the operator feed. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | SURF – Communauté d’agglomération Fougères Agglomération |
| **Fréjus–Saint-Raphaël Le Bus** (api.pysae.com) | Transit layer live vehicles, Fréjus, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-transport-urbain-le-bus). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (31 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Le Bus – Estérel Côte d’Azur Agglomération |
| **Gap L’Agglo en Bus** (gtfs-rt.infra-hubup.fr) | Transit layer live vehicles, Gap, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reels-du-reseau-de-transports-lagglo-en-bus-communaute-dagglomeration-gap-tallard-durance). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (11 vehicles on 2026-11-13). | L’Agglo en Bus – Communauté d’agglomération Gap-Tallard-Durance |
| **Guingamp-Paimpol Mobilité** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Guingamp, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-scolaire-guingamp-paimpol-mobilite). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (13 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Served by the national access point’s own proxy of the operator feed. | Guingamp-Paimpol Mobilité – Communauté d’agglomération Guingamp-Paimpol Agglomération de l’Armor à l’Argoat |
| **Guérande Lila Presqu’île** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Guérande, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-scolaire-lila). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (20 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Served by the national access point’s own proxy of the operator feed. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Lila Presqu’ile – Communauté d’agglomération de la Presqu’île de Guérande Atlantique (CAP ATLANTIQUE) |
| **Haguenau Ritmo** (zenbus.net) | Transit layer live vehicles, Haguenau, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-du-reseau-ritmo-haguenau-gtfs-gtfs-rt). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (20 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Ritmo – Communauté d’agglomération de Haguenau |
| **Hazebrouck Hop Bus** (gtfs-rt.infra-hubup.fr) | Transit layer live vehicles, Hazebrouck, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/hop-bus-le-reseau-100-gratuit-du-coeur-de-flandre). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (8 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Hop Bus – Communauté d’agglomération Coeur de Flandre |
| **Honfleur Hobus** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Honfleur, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/hobus-honfleur). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Published through the Atoumod (Normandy) hub; its positions stalled for about ten minutes once on 2026-11-13. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Hobus – Communauté de communes du Pays de Honfleur-Beuzeville |
| **Issoire Cherpa** (zenbus.net) | Transit layer live vehicles, Issoire, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/cherpa-horaires-lignes-regulieres). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (1 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Cherpa – Agglo Pays d’Issoire |
| **Lavaur Lisatis** (zenbus.net) | Transit layer live vehicles, Lavaur, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-de-transport-urbain-lisatis). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (3 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | LISATIS – Région Occitanie (CC Tarn-Agout) |
| **Le Mans SETRAM** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Le Mans, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-du-reseau-des-transports-bus-et-tramway-setram-circulant-sur-le-territoire-le-mans-metropole). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (102 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Speeds are km/h: readings ran 3.67 times the measured movement on 2026-11-13, so the proxy converts them. Lines T1 and T2 are the tramway. | SETRAM – Communauté urbaine Le Mans Métropole |
| **Le Tampon CarSud** (zenbus.net) | Transit layer live vehicles, Le Tampon, La Réunion, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/referentiel-topologique-reseau-carsud). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (22 vehicles on 2026-11-13). | CarSud – Communauté d’agglomération du Sud |
| **Liancourt Le Libellule** (api.oisemob.cityway.fr) | Transit layer live vehicles, Liancourt, France | ODbL (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-le-libellule-communaute-du-liancourtois-la-vallee-doree). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (1 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. | Libellule – Communauté de communes du Liancourtois |
| **Lisieux Astrobus** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Lisieux, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/astrobus-lisieux-normandie). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (6 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Published through the Atoumod (Normandy) hub; its positions stalled for about ten minutes once on 2026-11-13. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Astrobus – Communauté d’agglomération Lisieux Normandie |
| **Longwy TGL** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Longwy, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-theoriques-du-reseau-tgl). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (21 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | TGL – Syndicat mixte intercommunal des transports de l’agglomération de Longwy |
| **Lunel Olé** (ole.plateforme-2cloud.com) | Transit layer live vehicles, Lunel, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/offre-de-transport-du-reseau-ole-lunel-agglo-gtfs-et-gtfs-rt). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). | Olé – Communauté d’agglomération Lunel Agglo |
| **Lunéville Lunéo** (pysae.com) | Transit layer live vehicles, Lunéville, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-du-reseau-lulu-en-bus-ex-luneo-gtfs-gtfs-rt). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Lunéo – PETR du Pays du Lunévillois |
| **Martinique Centre (CACEM)** (ext.saemartinique.ovh:4435) | Transit layer live vehicles, Fort-de-France, Martinique, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-urbain-de-la-zone-centre). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (28 vehicles on 2026-11-13). | de la zone Centre (CACEM) – Martinique Transport |
| **Martinique Sud (CAESM)** (api.pysae.com) | Transit layer live vehicles, Southern Martinique, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-urbain-de-la-zone-sud). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (42 vehicles on 2026-11-13). Routes 4001 to 4004 are the Vedettes boats. | de la zone Sud (CAESM) – Martinique Transport |
| **Mayenne May’bus** (zenbus.net) | Transit layer live vehicles, Mayenne, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-du-reseau-maybus-mayenne-gtfs-gtfs-rt). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | May’bus – Communauté de communes Mayenne Communauté |
| **Mende TUM** (zenbus.net) | Transit layer live vehicles, Mende, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-du-reseau-max-mende-mobilites-bus-gtfs-gtfs-rt). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (1 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | TUM – Région Occitanie (CC Coeur de Lozère) |
| **Menton Zest** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Menton, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-reseau-zest). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (22 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | Zest – Communauté d’agglomération de la Riviera Française |
| **Metz Le Met’** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Metz, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/fichiers-gtfs-eurometropole-de-metz). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (140 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Le Met’ – Metz Métropole |
| **Montluçon Maelis** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Montluçon, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-reel-du-reseau-maelis-montlucon-communaute-gtfs). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (15 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Served by the national access point’s own proxy of the operator feed. Speeds are km/h: readings ran 3.3 times the measured movement on 2026-11-13, so the proxy converts them. The feed carries no route ids. | Maelis – Communauté d’agglomération Montluçon Communauté |
| **Montélimar Montélibus** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Montélimar, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-montelibus). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (20 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | Montelibus – Communauté d’agglomération Montélimar Agglomération |
| **Moulins Aléo** (api.pysae.com) | Transit layer live vehicles, Moulins, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-du-reseau-de-transport-aleo). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (16 vehicles on 2026-11-13). | Aléo – Communauté d’agglomération Moulins Communauté |
| **Narbonne Citibus** (feed-citibus-narbonne.ratpdev.com) | Transit layer live vehicles, Narbonne, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/offre-de-transport-communaute-dagglomeration-le-grand-narbonne). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (54 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Citibus – Communauté d’agglomération Le Grand Narbonne |
| **Nice Lignes d’Azur** (ara-api.enroute.mobi) | Transit layer live vehicles, Nice, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-statiques-et-dynamiques-du-reseau-de-transport-lignes-dazur). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (241 vehicles on 2026-11-13). Lines L1, L2 and L3 are the tramway. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Lignes d’Azur – Métropole Nice Côte d’Azur |
| **Nogent-le-Rotrou Nosbus** (api.pysae.com) | Transit layer live vehicles, Nogent-le-Rotrou, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-nosbus). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Nosbus – Région Centre-Val de Loire (CC du Perche) |
| **Noyon Lib’Bus** (api.oisemob.cityway.fr) | Transit layer live vehicles, Noyon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-libbus-communaute-de-communes-du-pays-noyonnais). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). | Lib’Bus – Communauté de communes du Pays Noyonnais |
| **Oloron-Sainte-Marie La Navette** (zenbus.net) | Transit layer live vehicles, Oloron-Sainte-Marie, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-du-reseau-oloron-sainte-marie-la-navette-gtfs-gtfs-rt). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | La Navette Oloron Sainte-Marie – Communauté de communes du Haut Béarn |
| **Orange TCVO** (tcvo.2cloud.app) | Transit layer live vehicles, Orange, France | ODbL (https://transport.data.gouv.fr/datasets/transport-en-commun-de-la-ville-dorange-ccpop-1). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (5 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. | TCVO – Pays d’Orange en Provence |
| **Pays de l’Or Transp’Or** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Mauguio, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/offre-transport-en-commun-du-reseau-transpor-gtfs). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (6 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Transp’Or – Communauté d’agglomération du Pays de l’Or |
| **Pays des Écrins buses** (app.pysae.com) | Transit layer live vehicles, L’Argentière-la-Bessée, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/transports-en-commun-pays-des-ecrins). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). | Communauté de communes du Pays des Ecrins |
| **Perpignan Sankéo** (zenbus.net) | Transit layer live vehicles, Perpignan, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-sankeo). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (50 vehicles on 2026-11-13). | Sankéo – Communauté urbaine Perpignan Méditerranée Métropole |
| **Pont-Sainte-Maxence TOHM** (api.oisemob.cityway.fr) | Transit layer live vehicles, Pont-Sainte-Maxence, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-theoriques-et-temps-reel-du-reseau-tohm-communaute-de-communes-des-pays-doise-et-dhalatte). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (5 vehicles on 2026-11-13). | TOHM – Communauté de communes des Pays d’Oise et d’Halatte |
| **Pontivy PondiBUS** (pysae.com) | Transit layer live vehicles, Pontivy, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-pondibus). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | PondiBUS – Communauté de communes Pontivy Communauté |
| **Porto-Vecchio A Citadina** (pysae.com) | Transit layer live vehicles, Porto-Vecchio, Corsica, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-a-citadina-porto-vecchio-horaires-theoriques-et-temps-reel). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | A Citadina – Communauté de communes du Sud Corse |
| **Quimper QUB** (feed-qub-quimper.ratpdev.com) | Transit layer live vehicles, Quimper, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-du-reseau-de-transports-qub-transports-urbains-de-quimper-bretagne-occidentale). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (49 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Qub – Communauté d’agglomération Quimper Bretagne Occidentale |
| **Riom RLV Mobilités** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Riom, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-rlv-mobilites). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (7 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | RLV Mobilités – Communauté d’agglomération Riom Limagne et Volcans |
| **Rosny-sous-Bois Titus** (zenbus.net) | Transit layer live vehicles, Rosny-sous-Bois, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-du-reseau-titus-rosny-sous-bois-gtfs-gtfs-rt). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Titus – Île-de-France Mobilités |
| **Rumilly J’ybus** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Rumilly, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/offre-de-transports-jybus-a-rumilly). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (6 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | Jybus – Communauté de communes Rumilly Terre de Savoie |
| **Rémi coaches** (api.okina.fr) | Transit layer live vehicles, Centre-Val de Loire, France | ODbL (https://transport.data.gouv.fr/datasets/remi-offre-theorique-mobilite-reseau-interurbain-regional). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (100 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Rémi – Centre-Val de Loire |
| **Réunion Car Jaune** (pysae.com) | Transit layer live vehicles, La Réunion, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-theoriques-au-format-gtfs-et-horaires-temps-reel-au-format-gtfs-rt-du-reseau-car-jaune-a-la-reunion). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (59 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Car Jaune – La Réunion |
| **Sablé-sur-Sarthe Réso** (gtfs-rt.infra-hubup.fr) | Transit layer live vehicles, Sablé-sur-Sarthe, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-reso). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Réso – Communauté de communes du Pays Sabolien |
| **Saint-Brevin Brévibus** (pysae.com) | Transit layer live vehicles, Saint-Brevin-les-Pins, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-brevibus). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (1 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Brévibus – Communauté de communes du Sud Estuaire |
| **Saint-Malo MAT** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Saint-Malo, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-urbain-mat). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (30 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | MAT – Communauté d’agglomération du Pays de Saint Malo Agglomération |
| **Saint-Omer Mouvéo** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Saint-Omer, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/transports-en-commun-audomarois-1). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (32 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | Mouvéo – Communauté d’agglomération du Pays de Saint-Omer |
| **Saint-Pierre Alternéo** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Saint-Pierre, La Réunion, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-du-reseau-alterneo). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (17 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Alternéo – Communauté d’agglomération CIVIS (Communauté Intercommunale des Villes Solidaires) |
| **Saint-Sulpice-la-Pointe buses** (zenbus.net) | Transit layer live vehicles, Saint-Sulpice-la-Pointe, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-de-transport-urbain-de-la-ville-de-st-sulpice-la-pointe). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (2 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | ST SULPICE LA POINTE – Région Occitanie (CC Tarn-Agout) |
| **Saintes Buss** (zenbus.net) | Transit layer live vehicles, Saintes, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-du-reseau-buss-saintes-gtfs-gtfs-rt). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (19 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Buss – Communauté d’agglomération de Saintes |
| **Sarlat Bus** (zenbus.net) | Transit layer live vehicles, Sarlat-la-Canéda, France | ODbL (https://transport.data.gouv.fr/datasets/horaires-theoriques-et-temps-reel-du-reseau-sarlatbus-gtfs-gtfs-rt). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (1 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Sarlat Bus – Communauté de communes Sarlat-Périgord Noir |
| **Sarthe Illygo** (pysae.com) | Transit layer live vehicles, Sarthe, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-illygo). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (5 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Illygo – Pôle Métropolitain Mobilités Le Mans – Sarthe |
| **Sète Agglopôle Mobilité** (sete.ceccli.com) | Transit layer live vehicles, Sète, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-bus-sete-agglopole-mobilite). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (48 vehicles on 2026-11-13). | Sète Agglopole Mobilité – Communauté d’agglomération Sète Agglopôle Méditerranée |
| **Tarbes–Lourdes TLP Mobilités** (zenbus.net) | Transit layer live vehicles, Tarbes, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-theoriques-du-reseau-tarbes-lourdes-tlp-mobilites-gtfs). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (24 vehicles on 2026-11-13). | TLP – Communauté d’agglomération Tarbes-Lourdes-Pyrénées |
| **Troyes TCAT** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Troyes, France | ODbL (https://transport.data.gouv.fr/datasets/donnees-tcat-troyes-champagne-metropole-1). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (52 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Served by the national access point’s own proxy of the operator feed. | TCAT – Communauté d’agglomération Troyes Champagne Métropole |
| **Valence–Romans Citéa** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Valence, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/ce-jeu-de-donnees-contient-la-liste-des-arrets-des-horaires-et-des-parcours-theoriques-du-reseau-de-transport-urbain-et-interurbain-de-vrm). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (130 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | Citéa – Valence-Romans déplacements |
| **Valenciennes Transvilles** (ara-api.enroute.mobi) | Transit layer live vehicles, Valenciennes, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/simouv-horaires-theoriques-format-gtfs-et-temps-reel-format-siri-transvilles-valenciennes). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (87 vehicles on 2026-11-13). Lines T1 and T2 (route ids 190 and 191) are the tramway. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Transvilles – Syndicat intercommunal de mobilité et d’organisation urbaine du Valenciennois |
| **Vannes Kicéo** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Vannes, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-urbain-kiceo). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (63 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Kicéo – Communauté d’agglomération Golfe du Morbihan - Vannes Agglomération |
| **Vendôme Move** (pysae.com) | Transit layer live vehicles, Vendôme, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/gtfs-move-vendome). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (7 vehicles on 2026-11-13). Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Move – Communauté d’agglomération Territoires Vendômois |
| **Vernon SNgo!** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Vernon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/sngo-vernon-les-andelys-seine-normandie-agglo). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (7 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Published through the Atoumod (Normandy) hub; its positions stalled for about ten minutes once on 2026-11-13. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | SNgo! – Communauté d’agglomération Seine Normandie Agglomération |
| **Vesoul Moova** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Vesoul, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/horaires-theoriques-du-reseau-moova-communaute-dagglomeration-de-vesoul-gtfs). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (7 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | moova – Communauté d’agglomération de Vesoul |
| **Vichy Mobivie** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Vichy, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/mobivie-reseau-urbain-vichy-communaute). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (11 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. | Mobivie – Communauté d’agglomération Vichy Communauté |
| **Vienne L’va** (feed-vienne-mobi.ratpdev.com) | Transit layer live vehicles, Vienne, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-lva). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (44 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. | L’va – Communauté d’agglomération Vienne Condrieu |
| **Vierzon Le Vib’** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Vierzon, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-urbain-le-vib). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (4 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. Served by the national access point’s own proxy of the operator feed. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Le Vib’ – Région Centre-Val de Loire (CC Vierzon-Sologne-Berry) |
| **Yvetot Vikibus** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Yvetot, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/vikibus-yvetot-normandie). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (3 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Published through the Atoumod (Normandy) hub; its positions stalled for about ten minutes once on 2026-11-13. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Vikibus – Communauté de communes Yvetot Normandie |
| **liO Occitanie coaches** (lio.2cloud.app) | Transit layer live vehicles, Occitanie, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-lio-occitanie). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (243 vehicles on 2026-11-13). ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database. | liO Occitanie – Occitanie |
| **Évreux Transurbain** (proxy.transport.data.gouv.fr) | Transit layer live vehicles, Évreux, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/transurbain-evreux-portes-de-normandie). Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (24 vehicles on 2026-11-13). Served by the national access point’s own proxy of the operator feed. Published through the Atoumod (Normandy) hub; its positions stalled for about ten minutes once on 2026-11-13. Where a route id is not the line name, the name comes from `routes.txt` in the same record’s GTFS. | Transurbain – Communauté d’agglomération Evreux Portes de Normandie |
| **Metro Bilbao** (ctb-gtfs-rt.s3.eu-south-2.amazonaws.com) | Transit layer live vehicles, Bilbao, Spain | CC BY 4.0 (https://data.ctb.eus/en/pages/legal-notice). Legal notice read 2026-11-13. Reuse is allowed for commercial and non-commercial purposes on condition that the source and the date of the last update are mentioned, hence the dated credit. The consortium reserves the right to set other conditions. 15 vehicles on 2026-11-13. | Consorcio de Transportes de Bizkaia (data.ctb.eus) |
| **PKS Rzeszów** (www.mpkrzeszow.pl) | Transit layer live vehicles, Rzeszów, Poland | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3428). Same server as the MPK Rzeszów feed; its open-data page was read 2026-11-13. Keyless feed on www.mpkrzeszow.pl, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 71 vehicles on 2026-11-13. The feed carries no route ids. | PKS w Rzeszowie S.A. |
| **Metro St. Louis** (gtfs.metrostlouis.org) | Transit layer live vehicles, St. Louis, MO | Metro Transit developer terms of use (https://www.metrostlouis.org/developer-resources/). Developer resources page read 2026-11-13. Acceptance is by use. Agency trademarks may not be used with the data unless approved, so the credit is the name as text. 127 vehicles on 2026-11-13. | Metro Transit – St. Louis |
| **Community Transit** (s3.amazonaws.com) | Transit layer live vehicles, Snohomish County, WA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1448). Community Transit’s open data page (https://www.communitytransit.org/OpenData, read 2026-11-13) lists this feed for developers, with disclaimers only. Keyless feed on s3.amazonaws.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 137 vehicles on 2026-11-13. | Community Transit |
| **Beach Cities Transit** (redondobeachbct.com) | Transit layer live vehicles, Redondo Beach, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1566). The City of Redondo Beach transit pages were read 2026-11-13. Keyless feed on redondobeachbct.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 9 vehicles on 2026-11-13. The feed carries no route ids. | Beach Cities Transit |
| **Madison Metro Transit** (metromap.cityofmadison.com) | Transit layer live vehicles, Madison, WI | City of Madison developer licence agreement (https://www.cityofmadison.com/metro/business/information-for-developers/developer-license-agreement-and-terms-of-use). Agreement read 2026-11-13. Acceptance is by use; the licence is limited to uses that assist transit riders or promote public transport, which a live vehicle map is. The credit is the wording the agreement offers. 101 vehicles on 2026-11-13. | Data provided under license granted by City of Madison, WI, Metro Transit |
| **MVgo Mountain View** (mtma.tripshot.com) | Transit layer live vehicles, Mountain View, CA | ODC-BY 1.0 (https://mvgo.org/developers/). Developers page read 2026-11-13. ODC-BY asks for attribution on public use. 7 vehicles on 2026-11-13. The feed carries no route ids. | MVgo – Mountain View Transportation Management Association |
| **Emery Go-Round** (emerygoround.tripshot.com) | Transit layer live vehicles, Emeryville, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2227). The Emery Go-Round site was read 2026-11-13. Keyless feed on emerygoround.tripshot.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 13 vehicles on 2026-11-13. The feed carries no route ids. | Emery Go-Round |
| **Valley Regional Transit** (s3.amazonaws.com) | Transit layer live vehicles, Boise, ID | CC BY 3.0 (https://www.valleyregionaltransit.org/about-us/resources/). Resources page read 2026-11-13; it lists this realtime file among the works. 29 vehicles on 2026-11-13. The feed carries no route ids. | Valley Regional Transit |
| **Greater Portland Metro Bus** (gtfsrt.gptd.cadavl.com) | Transit layer live vehicles, Portland, ME | Greater Portland Metro developer terms (https://www.gpmetro.org/about-us/projects-initiatives/developers/). Developers page read 2026-11-13. No credit wording or registration is asked for. 30 vehicles on 2026-11-13. The feed carries no route ids. | Greater Portland Metro |
| **Greater Cleveland RTA** (gtfs-rt.gcrta.vontascloud.com) | Transit layer live vehicles, Cleveland, OH | GCRTA Developer’s License Agreement (https://www.riderta.com/developers). Agreement read 2026-11-13. It asks licensees to “Clearly acknowledge GCRTA as the provider of the Data”, forbids implying GCRTA commissioned the product and forbids its logos, so the credit is text. Acceptance is by use. 294 vehicles on 2026-11-13. | Greater Cleveland Regional Transit Authority (GCRTA) |
| **Fairfax Connector** (www.fairfaxcounty.gov) | Transit layer live vehicles, Fairfax County, VA | Fairfax County transportation data licence (https://www.fairfaxcounty.gov/connector/bustracker/data). Terms read 2026-11-13. They require the statement “Visit fairfaxconnector.com for more information” with a link, which the credit carries, and limit use to assisting transit users. 94 vehicles on 2026-11-13. | Fairfax Connector. Visit fairfaxconnector.com for more information |
| **Butler County RTA** (www.buztrakr.com) | Transit layer live vehicles, Wayne Township, OH | Public realtime vehicle feed (https://gtfsrt.io/). BCRTA’s GTFS page (https://www.butlercountyrta.com/gtfs-download/, read 2026-11-13) links this host. Keyless feed on www.buztrakr.com, listed in the gtfsrt.io directory as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 18 vehicles on 2026-11-13. The feed carries no route ids. | Butler County RTA |
| **Société de transport collectif de Pierre-De Saurel** (zenbus.net) | Transit layer live vehicles, Sorel-Tracy, QC, Canada | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2380). Keyless feed on zenbus.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 5 vehicles on 2026-11-13. | Société de transport collectif de Pierre-De Saurel |
| **Regional Municipality of Wood Buffalo Transit** (rmwb-gtfsrt.civilia.ca) | Transit layer live vehicles, Fort McMurray, AB, Canada | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4691-vp). Keyless feed on rmwb-gtfsrt.civilia.ca, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 9 vehicles on 2026-11-13. | Regional Municipality of Wood Buffalo Transit |
| **Bus Mont-Tremblant** (zenbus.net) | Transit layer live vehicles, Mont-Tremblant, QC, Canada | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5619-vp). Keyless feed on zenbus.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 1 vehicles on 2026-11-13. | Bus Mont-Tremblant |
| **Saint Jean Sur Richelieu** (zenbus.net) | Transit layer live vehicles, Saint-Jean-sur-Richelieu, QC, Canada | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5631-vp). Keyless feed on zenbus.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 9 vehicles on 2026-11-13. | Saint Jean Sur Richelieu |
| **L'Inter des Laurentides** (zenbus.net) | Transit layer live vehicles, Saint-Sauveur, QC, Canada | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5723-vp). Keyless feed on zenbus.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 1 vehicles on 2026-11-13. | L'Inter des Laurentides |
| **MRC du Haut-Saint-Laurent** (gtfs-rt.infra-hubup.fr) | Transit layer live vehicles, Salaberry-de-Valleyfield, QC, Canada | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6693-tu_vp). Keyless feed on gtfs-rt.infra-hubup.fr, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 3 vehicles on 2026-11-13. | MRC du Haut-Saint-Laurent |
| **Empresa Municipal de Transportes de Fuenlabrada (EMTF)** (api.control.optibus.co) | Transit layer live vehicles, Fuenlabrada, Spain | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2402). Keyless feed on api.control.optibus.co, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 26 vehicles on 2026-11-13. | Empresa Municipal de Transportes de Fuenlabrada (EMTF) |
| **Orosháza local bus** (gtfs.gpspositions.net) | Transit layer live vehicles, Orosháza, Hungary | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3484). Keyless feed on gtfs.gpspositions.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 2 vehicles on 2026-11-13. | FlexCom Kommunikációs Kft. |
| **TEP PARMA** (s3.eu-south-1.amazonaws.com) | Transit layer live vehicles, Parma, Italy | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3452). Keyless feed on s3.eu-south-1.amazonaws.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 62 vehicles on 2026-11-13. | TEP PARMA |
| **Chișinău RTEC and PUA** (chisinau-rt-feed.gtfs.ro) | Transit layer live vehicles, Chișinău, Moldova | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2342). Keyless feed on chisinau-rt-feed.gtfs.ro, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 409 vehicles on 2026-11-13. | Regia Transport Electric Chișinău, Î.M. Parcul Urban de Autobuze |
| **Transbus Buzău** (gtfsagent.transportpublic.ro) | Transit layer live vehicles, Buzău, Romania | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2113). Keyless feed on gtfsagent.transportpublic.ro, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 2 vehicles on 2026-11-13. | Transbus Buzău |
| **Merced County Transit (The Bus)** (thebuslive.com) | Transit layer live vehicles, Merced, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1506). Keyless feed on thebuslive.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 27 vehicles on 2026-11-13. The feed carries no route ids. | Merced County Transit (The Bus) |
| **Intercity Transit** (its.rideralerts.com) | Transit layer live vehicles, Olympia, WA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1537). Keyless feed on its.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 63 vehicles on 2026-11-13. | Intercity Transit |
| **Stanislaus Regional Transit Authority (StanRTA)** (max.availtec.com) | Transit layer live vehicles, Modesto, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1614). Keyless feed on max.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 54 vehicles on 2026-11-13. | Stanislaus Regional Transit Authority (StanRTA) |
| **Tompkins Consolidated Area Transit (TCAT)** (realtimetcatbus.availtec.com) | Transit layer live vehicles, Ithaca, NY | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1649). Keyless feed on realtimetcatbus.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 26 vehicles on 2026-11-13. | Tompkins Consolidated Area Transit (TCAT) |
| **Riverside Transit Agency** (rtabus.com) | Transit layer live vehicles, Riverside, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1665). Keyless feed on rtabus.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 148 vehicles on 2026-11-13. | Riverside Transit Agency |
| **Towson Loop** (passio3.com) | Transit layer live vehicles, Towson, MD | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1787). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 7 vehicles on 2026-11-13. | Towson Loop |
| **Commerce Municipal Bus Lines** (citycommbus.com) | Transit layer live vehicles, Commerce, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1794). Keyless feed on citycommbus.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 9 vehicles on 2026-11-13. The feed carries no route ids. | Commerce Municipal Bus Lines |
| **Mountain Line Transit** (mountainline.syncromatics.com) | Transit layer live vehicles, Flagstaff, AZ | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1980). Keyless feed on mountainline.syncromatics.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 21 vehicles on 2026-11-13. The feed carries no route ids. | Mountain Line Transit |
| **Ben Franklin Transit** (myride.bft.org) | Transit layer live vehicles, Pasco, WA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2125). Keyless feed on myride.bft.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 55 vehicles on 2026-11-13. | Ben Franklin Transit |
| **Montebello Bus Lines** (mbl.rideralerts.com) | Transit layer live vehicles, Montebello, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2218). Keyless feed on mbl.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 36 vehicles on 2026-11-13. | Montebello Bus Lines |
| **Spirit Bus** (passio3.com) | Transit layer live vehicles, Monterey Park, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2221). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 3 vehicles on 2026-11-13. | Spirit Bus |
| **Middletown Area Transit** (passio3.com) | Transit layer live vehicles, Portland, CT | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2271). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 8 vehicles on 2026-11-13. | Middletown Area Transit |
| **Via Mobility** (passio3.com) | Transit layer live vehicles, Boulder, CO | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2398). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 9 vehicles on 2026-11-13. | Via Mobility |
| **Butte Regional Transit (B-Line)** (planmyride.blinetransit.com) | Transit layer live vehicles, Chico, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2428). Keyless feed on planmyride.blinetransit.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 20 vehicles on 2026-11-13. | Butte Regional Transit (B-Line) |
| **University of Georgia** (passio3.com) | Transit layer live vehicles, Athens, GA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2619). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 33 vehicles on 2026-11-13. | University of Georgia |
| **Transit Authority of River City (TARC)** (tarc.rideralerts.com) | Transit layer live vehicles, Louisville, KY | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2665). Keyless feed on tarc.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 70 vehicles on 2026-11-13. | Transit Authority of River City (TARC) |
| **South Metro Area Regional Transit (SMART)** (mysmartbus.com) | Transit layer live vehicles, Wilsonville, OR | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2707). Keyless feed on mysmartbus.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 13 vehicles on 2026-11-13. The feed carries no route ids. | South Metro Area Regional Transit (SMART) |
| **ClackCo Connects (Clackamas County)** (passio3.com) | Transit layer live vehicles, Clackamas County, OR | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2744). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 5 vehicles on 2026-11-13. | ClackCo Connects (Clackamas County) |
| **Sandy Area Metro** (passio3.com) | Transit layer live vehicles, Sandy, OR | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2747). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 6 vehicles on 2026-11-13. | Sandy Area Metro |
| **Champaign-Urbana Mass Transit District** (gtfs-rt.mtd.org) | Transit layer live vehicles, Champaign, IL | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2889). Keyless feed on gtfs-rt.mtd.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 72 vehicles on 2026-11-13. | Champaign-Urbana Mass Transit District |
| **Clovis Transit** (passio3.com) | Transit layer live vehicles, Clovis, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2897). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 14 vehicles on 2026-11-13. | Clovis Transit |
| **Frederick County TransIT** (passio3.com) | Transit layer live vehicles, Frederick, MD | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3070). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 14 vehicles on 2026-11-13. | Frederick County TransIT |
| **South Bend Public Transportation Corporation** (passio3.com) | Transit layer live vehicles, South Bend, IN | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3077). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 23 vehicles on 2026-11-13. | South Bend Public Transportation Corporation |
| **Cecil Transit** (passio3.com) | Transit layer live vehicles, Elkton, MD | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3080). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 4 vehicles on 2026-11-13. | Cecil Transit |
| **NY Waterway** (nywaterway.connexionz.net) | Transit layer live vehicles, New York, NY | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3199). Keyless feed on nywaterway.connexionz.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 3 vehicles on 2026-11-13. The feed carries no route ids. | NY Waterway |
| **El Paso Transportation Authority** (data.peaktransit.com) | Transit layer live vehicles, El Paso County, TX | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3204). Keyless feed on data.peaktransit.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 6 vehicles on 2026-11-13. | El Paso Transportation Authority |
| **Monroe County Transportation Authority (MCTA)** (ponytracker.availtec.com) | Transit layer live vehicles, Pocono Township, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3436). Keyless feed on ponytracker.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 7 vehicles on 2026-11-13. | Monroe County Transportation Authority (MCTA) |
| **Shore Transit** (myride.shoretransit.org) | Transit layer live vehicles, Salisbury, MD | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3443). Keyless feed on myride.shoretransit.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 3 vehicles on 2026-11-13. | Shore Transit |
| **Rogue Valley Transportation District** (gtfsrt.rvtd.cadavl.com) | Transit layer live vehicles, Medford, OR | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3458). Keyless feed on gtfsrt.rvtd.cadavl.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 13 vehicles on 2026-11-13. The feed carries no route ids. | Rogue Valley Transportation District |
| **Broward County Transit** (myride2.broward.org) | Transit layer live vehicles, Broward County, FL | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3475). Keyless feed on myride2.broward.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 149 vehicles on 2026-11-13. | Broward County Transit |
| **University of Minnesota** (data.peaktransit.com) | Transit layer live vehicles, Minneapolis, MN | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3528). Keyless feed on data.peaktransit.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 19 vehicles on 2026-11-13. | University of Minnesota |
| **Greenlink** (gtfsrt.greenlink.cadavl.com) | Transit layer live vehicles, Greenville, SC | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-2266-vp_1). Keyless feed on gtfsrt.greenlink.cadavl.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 13 vehicles on 2026-11-13. | Greenlink |
| **Sacramento Regional Transit** (bustime.sacrt.com) | Transit layer live vehicles, Sacramento, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-3391-vp). Keyless feed on bustime.sacrt.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 137 vehicles on 2026-11-13. | Sacramento Regional Transit |
| **GoDurham** (godurham.rideralerts.com) | Transit layer live vehicles, Durham, NC | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4062-vp). Keyless feed on godurham.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 48 vehicles on 2026-11-13. | GoDurham |
| **Cascades East Transit** (passio3.com) | Transit layer live vehicles, Bend, OR | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4067-vp). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 18 vehicles on 2026-11-13. | Cascades East Transit |
| **Capital Area Transit** (cat.rideralerts.com) | Transit layer live vehicles, Harrisburg, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4299-vp). Keyless feed on cat.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 25 vehicles on 2026-11-13. | Capital Area Transit |
| **City of Santa Clarita Transit** (apps.santaclaritatransit.com) | Transit layer live vehicles, Santa Clarita, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4328-vp). Keyless feed on apps.santaclaritatransit.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 44 vehicles on 2026-11-13. The feed carries no route ids. | City of Santa Clarita Transit |
| **Akron Metro Regional Transit Authority** (realtimemetro.availtec.com) | Transit layer live vehicles, Akron, OH | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4332-vp). Keyless feed on realtimemetro.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 69 vehicles on 2026-11-13. | Akron Metro Regional Transit Authority |
| **Whatcom Transportation Authority** (bustracker.ridewta.com) | Transit layer live vehicles, Bellingham, WA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4358-vp). Keyless feed on bustracker.ridewta.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 47 vehicles on 2026-11-13. | Whatcom Transportation Authority |
| **Santa Barbara Metropolitan Transit District** (bustracker.sbmtd.gov) | Transit layer live vehicles, Santa Barbara, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4360-vp). Keyless feed on bustracker.sbmtd.gov, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 60 vehicles on 2026-11-13. | Santa Barbara Metropolitan Transit District |
| **Spokane Transit Authority** (gtfsbridge.spokanetransit.com) | Transit layer live vehicles, Spokane, WA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4363-vp). Keyless feed on gtfsbridge.spokanetransit.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 123 vehicles on 2026-11-13. | Spokane Transit Authority |
| **Torrance Transit** (www.mybusinfo.com) | Transit layer live vehicles, Torrance, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4364-vp). Keyless feed on www.mybusinfo.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 47 vehicles on 2026-11-13. | Torrance Transit |
| **Metropolitan Tulsa Transit Authority** (tulsa.rideralerts.com) | Transit layer live vehicles, Tulsa, OK | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4374-vp). Keyless feed on tulsa.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 40 vehicles on 2026-11-13. | Metropolitan Tulsa Transit Authority |
| **MARC Train (Maryland Transit Administration)** (mdotmta-gtfs-rt.s3.amazonaws.com) | Transit layer live vehicles, Baltimore, MD | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4451-vp). Keyless feed on mdotmta-gtfs-rt.s3.amazonaws.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 2 vehicles on 2026-11-13. | MARC Train (Maryland Transit Administration) |
| **Fresno Area Express** (gis4u.fresno.gov) | Transit layer live vehicles, Fresno, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4483-vp). Keyless feed on gis4u.fresno.gov, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 106 vehicles on 2026-11-13. | Fresno Area Express |
| **Antelope Valley Transit Authority** (track-it.avta.com) | Transit layer live vehicles, Palmdale, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4485-vp). Keyless feed on track-it.avta.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 36 vehicles on 2026-11-13. | Antelope Valley Transit Authority |
| **River Valley Transit** (my.ridervt.com) | Transit layer live vehicles, Williamsport, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4549-vp). Keyless feed on my.ridervt.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 14 vehicles on 2026-11-13. | River Valley Transit |
| **Area Transit Authority** (rideata.rideralerts.com) | Transit layer live vehicles, DuBois, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4552-vp). Keyless feed on rideata.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 13 vehicles on 2026-11-13. | Area Transit Authority |
| **County of Lackawanna Transit System** (coltsivl.availtec.com) | Transit layer live vehicles, Scranton, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4554-vp). Keyless feed on coltsivl.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 19 vehicles on 2026-11-13. | County of Lackawanna Transit System |
| **Luzerne County Transportation Authority** (realtimelctabus.availtec.com) | Transit layer live vehicles, Wilkes-Barre, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4556-vp). Keyless feed on realtimelctabus.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 22 vehicles on 2026-11-13. | Luzerne County Transportation Authority |
| **Butler Transit Authority** (butlerivl.availtec.com) | Transit layer live vehicles, Butler, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4660-vp). Keyless feed on butlerivl.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 4 vehicles on 2026-11-13. | Butler Transit Authority |
| **Indiana County Transit Authority** (indigobus.availtec.com) | Transit layer live vehicles, Indiana, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4662-vp). Keyless feed on indigobus.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 7 vehicles on 2026-11-13. | Indiana County Transit Authority |
| **Shenango Valley Shuttle Service** (svss.availtec.com) | Transit layer live vehicles, Hermitage, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4664-vp). Keyless feed on svss.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 3 vehicles on 2026-11-13. | Shenango Valley Shuttle Service |
| **Hazleton Public Transit** (realtimehpts.availtec.com) | Transit layer live vehicles, Hazleton, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4666-vp). Keyless feed on realtimehpts.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 8 vehicles on 2026-11-13. | Hazleton Public Transit |
| **Altoona Metro Transit** (amtran.rideralerts.com) | Transit layer live vehicles, Altoona, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4670-vp). Keyless feed on amtran.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 12 vehicles on 2026-11-13. | Altoona Metro Transit |
| **Cambria County Transit Authority** (live.camtranbus.com) | Transit layer live vehicles, Dale, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4672-vp). Keyless feed on live.camtranbus.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 21 vehicles on 2026-11-13. | Cambria County Transit Authority |
| **Centre County Transit Authority** (realtime.catabus.com) | Transit layer live vehicles, State College, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4673-vp). Keyless feed on realtime.catabus.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 34 vehicles on 2026-11-13. | Centre County Transit Authority |
| **Crawford Area Transportation Authority** (catabus.rideralerts.com) | Transit layer live vehicles, Troy Township, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4675-vp). Keyless feed on catabus.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 6 vehicles on 2026-11-13. | Crawford Area Transportation Authority |
| **The Rapid** (connect.ridetherapid.org) | Transit layer live vehicles, Grand Rapids, MI | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4676-vp). Keyless feed on connect.ridetherapid.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 78 vehicles on 2026-11-13. | The Rapid |
| **Kalamazoo Metro Transit** (trackmybus.kmetro.org) | Transit layer live vehicles, Kalamazoo, MI | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4677-vp). Keyless feed on trackmybus.kmetro.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 26 vehicles on 2026-11-13. | Kalamazoo Metro Transit |
| **Schuylkill Transportation System** (sts.rideralerts.com) | Transit layer live vehicles, Pottsville, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4689-vp). Keyless feed on sts.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 7 vehicles on 2026-11-13. | Schuylkill Transportation System |
| **Western Reserve Transit Authority** (myvalleystops.wrtaonline.com) | Transit layer live vehicles, Youngstown, OH | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4692-vp). Keyless feed on myvalleystops.wrtaonline.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 29 vehicles on 2026-11-13. | Western Reserve Transit Authority |
| **Athens Clarke County Transit** (bustracker.accgov.com) | Transit layer live vehicles, Athens, GA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4783-vp). Keyless feed on bustracker.accgov.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 21 vehicles on 2026-11-13. | Athens Clarke County Transit |
| **Manatee County Area Transit** (realtimemcat.availtec.com) | Transit layer live vehicles, Manatee County, FL | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4784-vp). Keyless feed on realtimemcat.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 24 vehicles on 2026-11-13. | Manatee County Area Transit |
| **Lehigh and Northampton Transportation Authority** (realtimelanta.availtec.com) | Transit layer live vehicles, Allentown, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4785-vp). Keyless feed on realtimelanta.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 48 vehicles on 2026-11-13. | Lehigh and Northampton Transportation Authority |
| **Norwalk Transit District** (mystop.norwalktransit.com) | Transit layer live vehicles, Norwalk, CT | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4790-vp). Keyless feed on mystop.norwalktransit.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 20 vehicles on 2026-11-13. | Norwalk Transit District |
| **Valley Transit** (itransitnw.com) | Transit layer live vehicles, Walla Walla, WA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4793-vp). Keyless feed on itransitnw.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 10 vehicles on 2026-11-13. The feed carries no route ids. | Valley Transit |
| **Muskegon Area Transit System** (muskegon.connexionz.net) | Transit layer live vehicles, Muskegon, MI | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-4832-vp). Keyless feed on muskegon.connexionz.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 8 vehicles on 2026-11-13. The feed carries no route ids. | Muskegon Area Transit System |
| **Bustang** (open-data.rtd-denver.com) | Transit layer live vehicles, Denver, CO | RTD GTFS-realtime licence agreement (https://www.rtd-denver.com/business-center/open-spatial-data/gtfs-realtime-license-agreement). Served from RTD’s open-data host beside the Denver RTD feed, under the same click-through agreement (see `rtd-denver`). Same server and basis as that feed. 10 vehicles on 2026-11-13. | Bustang (CDOT), via RTD open data |
| **Link Transit** (link.rideralerts.com) | Transit layer live vehicles, Wenatchee, WA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5502-vp). Keyless feed on link.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 33 vehicles on 2026-11-13. | Link Transit |
| **Mid Mon Valley Transit Authority** (mmvta.rideralerts.com) | Transit layer live vehicles, Charleroi, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5504-vp). Keyless feed on mmvta.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 9 vehicles on 2026-11-13. | Mid Mon Valley Transit Authority |
| **Lebanon Transit** (realtime.lebanontransit.org) | Transit layer live vehicles, Lebanon, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5532-vp). Keyless feed on realtime.lebanontransit.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 7 vehicles on 2026-11-13. | Lebanon Transit |
| **rabbittransit** (realtime.rabbittransit.org) | Transit layer live vehicles, York, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5539-vp). Keyless feed on realtime.rabbittransit.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 21 vehicles on 2026-11-13. | rabbittransit |
| **Norwalk Transit System** (nts.rideralerts.com) | Transit layer live vehicles, Norwalk, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5546-vp). Keyless feed on nts.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 24 vehicles on 2026-11-13. | Norwalk Transit System |
| **Ride Gwinnett** (realtimegwinnett.availtec.com) | Transit layer live vehicles, Gwinnett County, GA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5550-vp). Keyless feed on realtimegwinnett.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 29 vehicles on 2026-11-13. | Ride Gwinnett |
| **StarTran** (startran.connexionz.net) | Transit layer live vehicles, Lincoln, NE | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5554-vp). Keyless feed on startran.connexionz.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 23 vehicles on 2026-11-13. The feed carries no route ids. | StarTran |
| **Yamhill County Transit Area** (ycta.connexionz.net) | Transit layer live vehicles, Yamhill County, OR | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5555-vp). Keyless feed on ycta.connexionz.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 6 vehicles on 2026-11-13. The feed carries no route ids. | Yamhill County Transit Area |
| **Greater Peoria Mass Transit** (clk.rideralerts.com) | Transit layer live vehicles, West Peoria, IL | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5581-vp). Keyless feed on clk.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 24 vehicles on 2026-11-13. | Greater Peoria Mass Transit |
| **Racine Transit** (racine.connexionz.net) | Transit layer live vehicles, Racine, WI | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5582-vp). Keyless feed on racine.connexionz.net, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 14 vehicles on 2026-11-13. The feed carries no route ids. | Racine Transit |
| **Berks Area Regional Transportation Authority** (busfinder.bartabus.com) | Transit layer live vehicles, Reading, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5839-vp). Keyless feed on busfinder.bartabus.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 27 vehicles on 2026-11-13. | Berks Area Regional Transportation Authority |
| **Red Rose Transit Authority** (busfinder.redrosetransit.com) | Transit layer live vehicles, Lancaster, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5840-vp). Keyless feed on busfinder.redrosetransit.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 22 vehicles on 2026-11-13. | Red Rose Transit Authority |
| **Concho Valley Transit District** (passio3.com) | Transit layer live vehicles, San Angelo, TX | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5865-vp). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 5 vehicles on 2026-11-13. | Concho Valley Transit District |
| **Culver CityBus** (nextccbus.org) | Transit layer live vehicles, Culver City, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5892-vp). Keyless feed on nextccbus.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 40 vehicles on 2026-11-13. | Culver CityBus |
| **Billings Metropolitan Transit** (passio3.com) | Transit layer live vehicles, Billings, MT | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5894-vp). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 13 vehicles on 2026-11-13. | Billings Metropolitan Transit |
| **Skagit Transit** (strweb.skagittransit.org) | Transit layer live vehicles, Burlington, WA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5896-vp). Keyless feed on strweb.skagittransit.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 21 vehicles on 2026-11-13. | Skagit Transit |
| **Lakeland Area Mass Transit District** (www.ccbusinfo.com) | Transit layer live vehicles, Lakeland, FL | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5898-vp). Keyless feed on www.ccbusinfo.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 29 vehicles on 2026-11-13. | Lakeland Area Mass Transit District |
| **Housatonic Area Regional Transit** (passio3.com) | Transit layer live vehicles, Danbury, CT | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-5903-vp). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 12 vehicles on 2026-11-13. | Housatonic Area Regional Transit |
| **Bis-Man Transit** (passio3.com) | Transit layer live vehicles, Bismarck, ND | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6096-vp). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 7 vehicles on 2026-11-13. | Bis-Man Transit |
| **Chapel Hill Transit** (mychtransit.org) | Transit layer live vehicles, Chapel Hill, NC | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6626-vp). Keyless feed on mychtransit.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 35 vehicles on 2026-11-13. The feed carries no route ids. | Chapel Hill Transit |
| **Wichita Transit** (bus.wichitatransit.org) | Transit layer live vehicles, Wichita, KS | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6669-vp). Keyless feed on bus.wichitatransit.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 23 vehicles on 2026-11-13. | Wichita Transit |
| **New River Transit Authority** (passio3.com) | Transit layer live vehicles, Beckley, WV | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6714-vp). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 5 vehicles on 2026-11-13. | New River Transit Authority |
| **City of Irvine** (passio3.com) | Transit layer live vehicles, Irvine, CA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6725-vp). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 6 vehicles on 2026-11-13. | City of Irvine |
| **North Central Regional Transit District** (bluebustracker.availtec.com) | Transit layer live vehicles, Rio Arriba County, NM | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6744-vp). Keyless feed on bluebustracker.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 17 vehicles on 2026-11-13. | North Central Regional Transit District |
| **City of Mobile** (realtimemobile.availtec.com) | Transit layer live vehicles, Mobile, AL | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6774-vp). Keyless feed on realtimemobile.availtec.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 12 vehicles on 2026-11-13. | City of Mobile |
| **Capital Area Transit System** (cats.rideralerts.com) | Transit layer live vehicles, Baton Rouge, LA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6784-vp). Keyless feed on cats.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 29 vehicles on 2026-11-13. | Capital Area Transit System |
| **Westmoreland County Transit Authority** (wcta.rideralerts.com) | Transit layer live vehicles, Hempfield Township, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6786-vp). Keyless feed on wcta.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 13 vehicles on 2026-11-13. | Westmoreland County Transit Authority |
| **City of Jacksonville** (passio3.com) | Transit layer live vehicles, Jacksonville, NC | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6820-vp). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 5 vehicles on 2026-11-13. | City of Jacksonville |
| **GoCary** (www.gocarylive.org) | Transit layer live vehicles, Cary, NC | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6821-vp). Keyless feed on www.gocarylive.org, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 16 vehicles on 2026-11-13. | GoCary |
| **Bloomington Transit** (s3.amazonaws.com) | Transit layer live vehicles, Bloomington, IN | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6882-vp). Keyless feed on s3.amazonaws.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 27 vehicles on 2026-11-13. The feed carries no route ids. | Bloomington Transit |
| **NCSU Wolfline** (passio3.com) | Transit layer live vehicles, Raleigh, NC | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-6896-vp). Keyless feed on passio3.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 21 vehicles on 2026-11-13. | NCSU Wolfline |
| **Endless Mountains Transportation Authority** (best.rideralerts.com) | Transit layer live vehicles, Ulster Township, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-7022-vp). Keyless feed on best.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 7 vehicles on 2026-11-13. | Endless Mountains Transportation Authority |
| **Lower Anthracite Transit System** (lats.rideralerts.com) | Transit layer live vehicles, Mount Carmel Township, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-7024-vp). Keyless feed on lats.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 2 vehicles on 2026-11-13. | Lower Anthracite Transit System |
| **Pottstown Area Rapid Transit** (part.rideralerts.com) | Transit layer live vehicles, Pottstown, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-7026-vp). Keyless feed on part.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 3 vehicles on 2026-11-13. | Pottstown Area Rapid Transit |
| **Fayette Area Coordinated Transportation** (fact.rideralerts.com) | Transit layer live vehicles, South Union Township, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-7028-vp). Keyless feed on fact.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 5 vehicles on 2026-11-13. | Fayette Area Coordinated Transportation |
| **Freedom Transit** (freedom.rideralerts.com) | Transit layer live vehicles, Chartiers Township, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-7029-vp). Keyless feed on freedom.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 5 vehicles on 2026-11-13. | Freedom Transit |
| **New Castle Area Transit Authority** (ncata.rideralerts.com) | Transit layer live vehicles, New Castle, PA | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-7031-vp). Keyless feed on ncata.rideralerts.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 12 vehicles on 2026-11-13. | New Castle Area Transit Authority |
| **Lexington Transit Authority** (mystop.lextran.com) | Transit layer live vehicles, Lexington, KY | Public realtime vehicle feed (https://mobilitydatabase.org/feeds/gtfs_rt/tld-7044-vp). Keyless feed on mystop.lextran.com, listed in the Mobility Database as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 44 vehicles on 2026-11-13. | Lexington Transit Authority |
| **MetroLINK Quad Cities** (bustracker.metroqc.com) | Transit layer live vehicles, Moline, IL | Public realtime vehicle feed (https://www.transit.land/feeds/f-9zrs-metrorockislandcountymetropolitanmasstransitdistrict~rt). Keyless feed on bustracker.metroqc.com, listed in the Transitland Atlas as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 33 vehicles on 2026-11-13. | MetroLINK Quad Cities |
| **Streamline Bozeman** (ridestreamline.com) | Transit layer live vehicles, Bozeman, MT | Public realtime vehicle feed (https://www.transit.land/feeds/f-c815-streamlinetransit~mt~us~rt). Keyless feed on ridestreamline.com, listed in the Transitland Atlas as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 1 vehicles on 2026-11-13. The feed carries no route ids. | Streamline Bozeman |
| **Coast RTA** (coastrta.transloc.com) | Transit layer live vehicles, Myrtle Beach, SC | Public realtime vehicle feed (https://www.transit.land/feeds/f-coast~rta~sc~us~rt). Keyless feed on coastrta.transloc.com, listed in the Transitland Atlas as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 9 vehicles on 2026-11-13. | Coast RTA |
| **LAX airport shuttles** (laxtransportation.transloc.com) | Transit layer live vehicles, Los Angeles, CA | Public realtime vehicle feed (https://www.transit.land/feeds/f-lax~flyaway~rt). Keyless feed on laxtransportation.transloc.com, listed in the Transitland Atlas as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 6 vehicles on 2026-11-13. | LAX airport shuttles |
| **Mission Viejo Shuttle** (mvshuttle.transloc.com) | Transit layer live vehicles, Mission Viejo, CA | Public realtime vehicle feed (https://www.transit.land/feeds/f-mission~viejo~shuttle~rt). Keyless feed on mvshuttle.transloc.com, listed in the Transitland Atlas as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 2 vehicles on 2026-11-13. | Mission Viejo Shuttle |
| **LAX FlyAway** (laxflyaway.transloc.com) | Transit layer live vehicles, Los Angeles, CA | Public realtime vehicle feed (https://www.transit.land/feeds/f-laxflyaway~rt). Keyless feed on laxflyaway.transloc.com, listed in the Transitland Atlas as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 4 vehicles on 2026-11-13. | LAX FlyAway |
| **Triton Transit (UC San Diego)** (api.us.sparelabs.com) | Transit layer live vehicles, San Diego, CA | Public realtime vehicle feed (https://www.transit.land/feeds/f-9muy-tritontransit~rt). Keyless feed on api.us.sparelabs.com, listed in the Transitland Atlas as this operator’s vehicle positions. Shipped on the same basis as Metro Transit and OVapi, and credited as a courtesy; switch it off if the operator objects. 11 vehicles on 2026-11-13. | Triton Transit (UC San Diego) |
| **Zagreb ZET** (zet.hr) | Transit layer live vehicles, Zagreb, Croatia | Open Licence – Republic of Croatia (https://www.zet.hr/odredbe/datoteke-u-gtfs-formatu/669). ZET’s GTFS page, read 2026-11-13, places its GTFS and GTFS-realtime data under the Croatian open licence. The feed mixes trams and buses; route ids do not tell them apart, so the default is a guess. 345 vehicles on 2026-11-13. | Zagrebački električni tramvaj (ZET) |
| **Toyama Chitetsu Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-chitetsu-chitetsubus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山地方鉄道 (Toyama Chitetsu Bus) |
| **Imizu Kitokito Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-imizucity-imizushi-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 射水市 (Imizu Kitokito Bus) |
| **Kaetsunō Bus Himi loop** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-kaetsunou-kaetsunouhimi-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 加越能バス (Kaetsunō Bus Himi loop) |
| **Kaetsunō Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-kaetsunou-kaetsunouippan-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 加越能バス (Kaetsunō Bus) |
| **Kaetsunō World Heritage Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-kaetsunou-kaetsunousekaiisan-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 加越能バス (Kaetsunō World Heritage Bus) |
| **Kamiichi Town Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-kamiichitown-kamiichi-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 上市町 (Kamiichi Town Bus) |
| **Kurobe City Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-kurobecity-ishida-aimoto-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 黒部市 (Kurobe City Bus) |
| **Kurobe Shinkansen–Ikuji Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-kurobecity-shinkansenikujisen-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 黒部市 (Kurobe Shinkansen–Ikuji Bus) |
| **Namerikawa Noru my car** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-namerikawacity-norumycar-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 滑川市 (Namerikawa Noru my car) |
| **Nanto City Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-nantocity-nanbus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 南砺市 (Nanto City Bus) |
| **Nyūzen Noran my car** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-nyuzentown-noranmaicar-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 入善町 (Nyūzen Noran my car) |
| **Oyabe City Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-oyabecity-oyabecitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 小矢部市 (Oyabe City Bus) |
| **Takaoka City Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-takaokacity-koueibus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 高岡市 (Takaoka City Bus) |
| **Tateyama Town Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-tateyamatown-tateyama-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 立山町 (Tateyama Town Bus) |
| **Tokoname Gruun** (realtime.gtfs.info) | Transit layer live vehicles, Aichi Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-tokonamecity-Gruun-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 常滑市 (Tokoname Gruun) |
| **Tonami City Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-tonamicity-tonamishieibasu-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 砺波市 (Tonami City Bus) |
| **Asahi Asahimachi Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyama-asahitown-asahimachibus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山県朝日町 (Asahi Asahimachi Bus) |
| **Toyama Fuchū Community Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyamacity-fuchucommunitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山市 (Toyama Fuchū Community Bus) |
| **Toyama Horikawa-Minami Community Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyamacity-horikawaminamicommunitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山市 (Toyama Horikawa-Minami Community Bus) |
| **Toyama Jōjō Community Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyamacity-jojocommunitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山市 (Toyama Jōjō Community Bus) |
| **Toyama Kureha Ikiiki Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyamacity-kurehaikiikibus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山市 (Toyama Kureha Ikiiki Bus) |
| **Toyama Maidohaya Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyamacity-maidohayabus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山市 (Toyama Maidohaya Bus) |
| **Toyama Mizuhashi Fureai Community Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyamacity-mizuhashifureaicommunitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山市 (Toyama Mizuhashi Fureai Community Bus) |
| **Toyama Ōyama Community Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyamacity-oyamacommunitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山市 (Toyama Ōyama Community Bus) |
| **Toyama Yamada Community Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyamacity-yamadacommunitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山市 (Toyama Yamada Community Bus) |
| **Toyama Yatsuo Community Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-toyamacity-yatsuocommunitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 富山市 (Toyama Yatsuo Community Bus) |
| **Uozu Citizens’ Bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-uozucity-junkai-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 魚津市 (Uozu Citizens’ Bus) |
| **Uozu Omotenashi direct bus** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Toyama Prefecture, Japan | CC0 1.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-uozucity-omotenashi-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 魚津市 (Uozu Omotenashi direct bus) |
| **Chita Aiai Bus** (realtime.gtfs.info) | Transit layer live vehicles, Aichi Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-chitacity-chita-aiaibus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 知多市 (Chita Aiai Bus) |
| **Ise Okage Bus** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-isecity-communitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 伊勢市 (Ise Okage Bus) |
| **Keisei Bus Chiba West (Shiohama)** (s3-ajt-mobusta-gtfs.s3.ap-northeast-1.amazonaws.com) | Transit layer live vehicles, Chiba Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-keisei-transitbus-keiseitransitbus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 京成バス千葉ウエスト株式会社 (Keisei Bus Chiba West (Shiohama)) |
| **Komono Kamoshika-gō** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-komonotown-communitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 菰野町 (Komono Kamoshika-gō) |
| **Kumamoto Bus** (km.bus-vision.jp) | Transit layer live vehicles, Kumamoto Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-kuma-bus-kuma-bus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Vehicle positions were seen on 2026-11-13 around midnight Japan time. The centre is the catalogue’s bounding-box centre for the network. | 熊本バス (Kumamoto Bus) |
| **Kumamoto Toshi Bus** (km.bus-vision.jp) | Transit layer live vehicles, Kumamoto Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-kumamoto-toshibus-kumamoto-toshibus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Vehicle positions were seen on 2026-11-13 around midnight Japan time. The centre is the catalogue’s bounding-box centre for the network. | 熊本都市バス (Kumamoto Toshi Bus) |
| **Kumamoto Dentetsu Bus** (km.bus-vision.jp) | Transit layer live vehicles, Kumamoto Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-kumamotodentetsu-kumamotodentetsu-bus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Vehicle positions were seen on 2026-11-13 around midnight Japan time. The centre is the catalogue’s bounding-box centre for the network. | 熊本電鉄 (Kumamoto Dentetsu Bus) |
| **Mie Kōtsū Chūsei office** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-mie-kotsu-sancochusei-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 三重交通 (Mie Kōtsū Chūsei office) |
| **Mie Kōtsū Iga office** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-mie-kotsu-sancoiga-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 三重交通 (Mie Kōtsū Iga office) |
| **Mie Kōtsū Nanki office** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-mie-kotsu-sancoinanki-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 三重交通 (Mie Kōtsū Nanki office) |
| **Mie Kōtsū Ise office** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-mie-kotsu-sancoise-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 三重交通 (Mie Kōtsū Ise office) |
| **Mie Kōtsū Kuwana office** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-mie-kotsu-sancokuwana-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 三重交通 (Mie Kōtsū Kuwana office) |
| **Mie Kōtsū Matsusaka office** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-mie-kotsu-sancomatsusaka-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 三重交通 (Mie Kōtsū Matsusaka office) |
| **Mie Kōtsū Shima office** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-mie-kotsu-sancoshima-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 三重交通 (Mie Kōtsū Shima office) |
| **Mie Kōtsū Yokkaichi office** (bus-vision.jp) | Transit layer live vehicles, Mie Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-mie-kotsu-sancoyokkaichi-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 三重交通 (Mie Kōtsū Yokkaichi office) |
| **Nagai Bus** (nagai.nolbe.net) | Transit layer live vehicles, Gunma Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-nagai-unyu-Nagaibus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 永井運輸株式会社 (Nagai Bus) |
| **Nagano City Bus** (realtime.gtfs.info) | Transit layer live vehicles, Nagano Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-naganocity-shiei_noriaibus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 長野市 (Nagano City Bus) |
| **Owariasahi Asapī-gō** (gtfs-rt-files.buscatch.jp) | Transit layer live vehicles, Aichi Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-owariasahicity-communitybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 尾張旭市 (Owariasahi Asapī-gō) |
| **Sankō Bus** (km.bus-vision.jp) | Transit layer live vehicles, Kumamoto Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-sankobus-sankobus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Vehicle positions were seen on 2026-11-13 around midnight Japan time. The centre is the catalogue’s bounding-box centre for the network. | 産交バス (Sankō Bus) |
| **Takeya Kōtsū** (realtime.gtfs.info) | Transit layer live vehicles, Miyagi Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-takeyakoutu-takeyakotsu-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 株式会社タケヤ交通 (Takeya Kōtsū) |
| **Tsuchiura Tsuchimaru Bus** (realtime.gtfs.info) | Transit layer live vehicles, Ibaraki Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-tsuchiuracity-tsuchimarubus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 土浦市 (Tsuchiura Tsuchimaru Bus) |
| **Yanbaru Express Bus** (gtfs.yanbaru-bus-navi.com) | Transit layer live vehicles, Okinawa Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/jbda-yanbaru-expressbus-yanbaru-express-bus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 合同会社やんばる急行バス (Yanbaru Express Bus) |
| **Kenkō Hokubu Kōtsū** (api-public.odpt.org) | Transit layer live vehicles, Kōchi Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/odpt-KenkoHokubuKotsu-Localbus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 株式会社県交北部交通 (Kenkō Hokubu Kōtsū) |
| **Kōchi Kōryō Kōtsū** (api-public.odpt.org) | Transit layer live vehicles, Kōchi Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/odpt-KochiKoryoKotsu-Localbus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 高知高陵交通株式会社 (Kōchi Kōryō Kōtsū) |
| **Kōchi Seinan Kōtsū** (api-public.odpt.org) | Transit layer live vehicles, Kōchi Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/odpt-KochiSeinanTraffic-Localbus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 高知西南交通株式会社 (Kōchi Seinan Kōtsū) |
| **Susaki City Bus** (api-public.odpt.org) | Transit layer live vehicles, Kōchi Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/odpt-SusakiCity-Citybus-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Added while services were not running (2026-11-14, 01:40 Japan time): the feed answered with a well-formed, empty message, so its positions have not been seen yet. The centre is the catalogue’s bounding-box centre for the network. | 須崎市 (Susaki City Bus) |
| **Aomori City community bus** (toshisei.aomoricitybus.com) | Transit layer live vehicles, Aomori Prefecture, Japan | CC BY 4.0 (https://mobilitydatabase.org/feeds/gtfs_rt/odpt-UrbanDevelopment_AomoriCity-AllLines-vp). The Mobility Database record gives this licence for the feed; the quote is the licence it links. Vehicle positions were seen on 2026-11-13 around midnight Japan time. The centre is the catalogue’s bounding-box centre for the network. The catalogue’s ODPT address redirects to this host, so the feed is registered at its own address. | 青森市 (Aomori City community bus) |
| **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)" |
| **OpenSeaMap** (`tiles.openseamap.org`) | Sea Marks layer: raster overlay of buoys, lighthouses and ports (#10) | Chart tiles CC BY-SA 2.0; data ODbL 1.0 via OpenStreetMap | "Sea marks © OpenSeaMap contributors (CC BY-SA 2.0) · Data © OpenStreetMap contributors (ODbL)" |
@@ -574,7 +268,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **Houston TranStar cameras.** Keyless. The camera list is the public `traffic.houstontranstar.org/data/layers/cctvSnapshots_json.js` catalog (~1,000 cameras across Greater Houston); frames are stills at `www.houstontranstar.org/snapshots/cctv/<id>.jpg`, synthesized from a validated numeric id rather than taken from the payload, and only that host is registered. Cameras whose catalog id is `0` (a different, non-snapshot class) and cameras outside Greater Houston are dropped. TranStar's `dir` field is the side of the freeway the camera sits on, not the way it faces: TranStar's own map shows it as "Traffic closest to the camera is traveling …" (South → eastbound, North → westbound, West → southbound, East → northbound). That travel direction (90°, 270°, 180°, 0°) seeds the heading as a low-confidence prior with the low-confidence pose; `None` or an unknown value uses the shared id-hash fallback at low confidence. Headings are corrected with the in-app calibration gizmo. Off by default. TranStar's [Content Usage terms](https://traffic.houstontranstar.org/disclaimer.aspx) prohibit copying or distributing the site's information, and external framing or linking to specific images, without TxDOT's prior written permission; its FAQ grants permission-free use only for the RSS feeds. This project does not hold that permission for downstream installations, so the pack loads only when an operator who holds TxDOT's written permission sets `CCTV_HOUSTON_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to TranStar. Once enabled, it loads the 250 cameras nearest downtown, Uptown, the Medical Center, IAH and Galveston; `CCTV_HOUSTON_MAX_SOURCES` (8–400) changes the cap. Snapshots update about every 3 minutes, which is also the ambient still cadence. Frames are fetched from TranStar and relayed to the browser on demand, buffered only transiently per request, sent with `Cache-Control: no-store`, and not archived.
- **Autostrada del Brennero (A22) webcams.** Keyless. The camera list is one request to the Open Data Hub tourism API (`tourism.api.opendatahub.com/v1/WebcamInfo?source=a22`), run by NOI Techpark in South Tyrol, which republishes Autobrennero's own list of 13 motorway webcams with coordinates and kilometre markers. Frames are the operator's stills on `www.autobrennero.it/WebCamImg/km<N>.jpg`, refreshed about once a minute, and only the exact image for each camera's kilometre is registered. The feed names no bearing, so headings use the id-hash fallback at low confidence. The road climbs from about 20 m in the Po valley to about 1,370 m at the Brenner Pass, so each camera's ground elevation comes from a per-kilometre table sampled from the Copernicus GLO-90 DEM (via Open-Meteo's elevation API); a camera not in the table uses a 200 m prior. The Open Data Hub tags every record CC0 (`ClosedData: false`, Autobrennero as licence holder), but that tag is the aggregator's metadata and Autobrennero publishes no terms of its own for the images, so they are credited to the operator as a courtesy and are not redistributed. Frames are fetched at request time and never stored. Disable with `CCTV_A22_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. All 358 registered feeds are live-enabled; each can be disabled independently. Of those, 112 are French networks taken from the national access point, transport.data.gouv.fr: each is the GTFS-RT vehicle feed of a dataset record whose licence field is Licence Ouverte 2.0 or ODbL, and each returned live vehicle positions on 2026-11-13. Their rows live in `src/data/transitFeedsFrance.js`, with line names for the feeds that publish internal route keys in `src/data/transitRouteNamesFrance.js`, copied from each record’s own GTFS `routes.txt`. A network that was already here through its operator’s own endpoint (Bordeaux, Montpellier, Rouen, Toulon, Le Havre, Tours) was not added a second time, and neither was the Atoumod aggregate, which repeats Normandy networks listed one by one. Feeds republished by bus-tracker.fr, feeds served over plain HTTP, and the three Fluo Grand Est feeds, whose positions ran seven to fifteen minutes old, are left out. A further 139 feeds come from the open GTFS-Realtime catalogues (Mobility Database, Transitland Atlas), most of them in the United States; their rows are in `src/data/transitFeedsCatalogue.js`. Ten are operators whose published terms were read on 2026-11-13 and permit display (Metro Bilbao, Metro St. Louis, Madison, MVgo, Valley Regional Transit, Greater Portland Metro, Greater Cleveland RTA, Fairfax Connector, Zagreb ZET, and Bustang under RTD’s agreement); each row quotes the grant. The other 129 are keyless endpoints the catalogue lists as the operator’s own, usually on the operator’s tracking vendor (Avail, Passio, Connexionz, TransLoc); each links its catalogue record, is credited as a courtesy, and ships on the same basis as Metro Transit and OVapi. Seven catalogue feeds answer keyless but are excluded because their terms do not allow this use: MTA New York City Bus (the terms require an API key and forbid serving data straight from MTA servers), Detroit DDOT (use only with the city’s permission), Everett Transit (registration required), Duluth Transit (display of arrival information needs written consent), Greater Dayton RTA (an unpublished developer agreement governs), CTtransit and Pittsburgh Regional Transit (their licences cover static GTFS only). Eight more are held until their terms can be read, because the page was unreachable or empty: Sofia, Lviv, Saint-Avold, Des Moines DART, Springfield SMTD, Knoxville KAT, Pioneer Valley PVTA and Lafayette CityBus. The BusOne proxy feeds for Italian and Austrian operators are a third party’s republication and are left out, as are five more data.gov.my feeds that would share the API’s four requests a minute with the two already here. Fifty-five Japanese bus networks, most of them in Toyama, Mie, Kumamoto and Kōchi prefectures, are in `src/data/transitFeedsJapan.js`; the Mobility Database records each under CC0 1.0 or CC BY 4.0. Fifty of them were added while services were not running in Japan, when the feeds answer with an empty message, so their vehicle positions had not been seen at the time; each row says which case it is. 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. Hamilton, Guelph, Burlington, YRT and Rio’s SMTR publish route ids that are not the number on the vehicle, so those entries carry a route-name table taken from each operator’s own published GTFS; any id missing from a table renders unchanged rather than blank. A label is nevLine truncated
- **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All fifty-two registered feeds, covering forty-nine regions, are live-enabled; 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. Hamilton, Guelph, Burlington, YRT and Rio’s SMTR publish route ids that are not the number on the vehicle, so those entries carry a route-name table taken from each operator’s own published GTFS; any id missing from a table renders unchanged rather than blank. A label is never invented: YRT runs a route `9101` that appears nowhere in its published routes, so it stays as the feed sends it. Burlington’s is the one that cannot be shortcut: `351` is route 1 while `3510` is route 10, so the table is a real join rather than a trimmed prefix. 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). Barrie and YRT/Viva each invite acceptance of their licence through a form, and each licence also states that using the data is itself acceptance; this deployment relies on that clause, which is recorded in both entries. 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. Ottawa’s OC Transpo falls to the same keyless admission rule as GO Transit — its realtime feed requires an API key. London Transit and Thunder Bay Transit publish vehicle positions over plain HTTP with no HTTPS endpoint available. Every other Ontario system — Brampton, Oakville, Niagara, Milton, Windsor, Greater Sudbury, UP Express and a dozen smaller operators — publishes static GTFS schedules only, with no realtime vehicle positions to register. Between the nine included and these exclusions, every Ontario realtime vehicle-position feed catalogued in the Mobility Database is accounted for.
- **Rail (GTFS-Realtime trains).** Each feed in `src/data/railFeeds.js` records its licence, its credit and the passage of its terms the decision rests on, and a feed is switched on only when those terms were read and cover this use, or, for those that publish no licence at all, on the basis its entry records. Such a basis is never a licence, and the credit does not present it as one: its label says there is no licence ("No licence published", "No licence stated") or names the publication the use rests on. Five enabled feeds publish no licence for their train positions. MARC is used on what its publication says (open publication with a no-endorsement disclaimer), and METRO light rail on the same basis as the Transit layer's Metro Transit feed. Tri-Rail and Charlotte's LYNX are used without a published licence by project owner decision of 2026-11-10. The fifth is Irish Rail, whose train positions publish no licence while its NTA timetable is CC BY 4.0. The registry lists 30 feeds, all switched on, and every one is keyless: its realtime feed and its static timetable are published for anyone to fetch, so no feed needs a key, an account or a registration, and the server holds no rail credential. No rail request carries a credential of the deployer's; one feed's published URL includes a fixed public value its publisher prints for everyone (Bordeaux's `apiKey`). `/api/rail/feeds` lists the offered feeds, `/api/rail/trains/<feedId>` returns one feed's train snapshot, `/api/rail/updates/<feedId>` its trip updates, and `/api/rail/paths/<feedId>` and `/api/rail/schedule/<feedId>` its path and schedule packs, when built. The proxy requests only registered feeds and reuses the Transit proxy's request policy (a short snapshot cache with single-flight refresh, serve-stale, admission limits, the failure cooldown ladder, redirects followed hop by hop only on the feed's own https origin, and an 8 MiB body cap unless a feed sets its own, at most 64 MiB). Many operators publish trains in the same feed as their buses, so each feed has a classifier: the proxy keeps only vehicles it recognises as trains, tags each with a category (`transit`, `local`, `regional`, `national` or `international`) and a mode (`rail`, `metro` or `tram`), and drops everything else server-side, counted in the snapshot as `dropped`; the browser never receives a bus. Every train shown is live, where its operator last reported it: 29 feeds publish live GPS positions (SJ Nord's through SIRI VM), and Irish Rail reports the station or timing point a train last passed. No train is placed or moved from a timetable; a feed whose live positions fail shows as unavailable, and its trains age out as any quiet train does. 22 feeds register trip updates, polled while one of their trains is selected and that feed's schedule pack is loaded. 28 feeds work out of the box. Packs are an opt-in extra for live trains: nothing is downloaded when the app starts, and `node scripts/build-rail-paths.mjs --feed <id>` (repeatable) or `--all` downloads a feed's static GTFS (29 of the 30 have one) and writes a path pack (the operator's track shapes) and a schedule pack (the stop times for a window of days) to `.cache/rail-paths/` (or to `RAIL_PATHS_DIR` when set, the directory the server then serves). With them, a live train is drawn on its operator's route line and its card names its stops, its next stop and, from the trip updates, their delays. Renfe long and medium distance and KTMB, whose vehicles name no route, are classified from their pack's routes, so `/api/rail/feeds` lists them only once that pack is built. Lines are the operator's own shapes as published: nothing is routed or redrawn from other map data, and no line is drawn where an operator publishes no shape. **Share-alike sources bind the deployer.** A pack built from a ShareAlike or ODbL source is a derived database of it, so a deployer who serves it publicly makes it subject to the same licence and must offer it under that licence with the attribution kept. That covers the Montpellier packs (ODbL). For the ODbL datasets of France's national access point, its special conditions also require that "Les bases de données ainsi repartagées doivent être publiées sur le Point d'accès national transport.data.gouv.fr, dans le format d'origine" ([Licence ODbL](https://doc.transport.data.gouv.fr/le-point-d-acces-national/cadre-juridique/conditions-dutilisation-des-donnees/licence-odbl), read 2026-11-10): a deployer who serves the Montpellier packs publicly takes on republishing that derived data there, as GTFS. The build downloads each static GTFS itself, over https and with no credential. A feed's `staticRouteTypes` lists the route types it keeps when its classifier accepts every route of a mode-specific feed. A frequency-based trip becomes one run per departure on its template's stops and run times. Packs are build output, not part of the repository, and cover a fixed window of days, so they are rebuilt before it ends, anLine truncated
- **Radio country coverage (optional).** Set `RADIO_PRIORITY_COUNTRIES=IN,PL` in the local `.env` and restart to include stations beyond the global click charts. The operator may choose up to eight unique ISO alpha-2 country codes; no key is needed. Each country adds one bounded query (at most 200 rows) and reserves up to 40 unique, usable stations inside the existing 750-station catalog. Country seeds precede specialist-tag seeds and global popularity fill; unused slots return to the shared pool. This is a coverage preference, not an exhaustive national directory or a guarantee of stations in every city. Countries with no eligible HTTPS MP3/AAC geolocated stations remain empty. An unset list preserves global selection. Country requests share the directory cache and three-worker pool; failed country queries are reported in coverage without blocking a healthy global refresh. Failed/degraded refreshes back off for one minute, with at most three mirrors per query.
@@ -0,0 +1,6 @@
- When the globe stops drawing after a render error or a lost WebGL context,
an alert dialog says so instead of leaving a frozen or black globe behind
working controls. Retry restarts drawing (Reload after a lost context), and
Reset view reopens the app without the view restored from the address bar.
Cesium's "Rendering has stopped" panel no longer appears in the app
(German, @gekh, #1579).
@@ -1,26 +0,0 @@
- Add 112 French networks to the Transit layer, from Nice, Dijon, Angers,
Brest, Le Mans, Metz, Amiens, Avignon and Valenciennes to small-town
networks, the liO, Aléop and Rémi regional coaches, Réunion, Martinique and
French Guiana. Each is the keyless vehicle feed of a transport.data.gouv.fr
record under Licence Ouverte 2.0 or ODbL, recorded in the registry and
DATA_SOURCES.md. Trams and boats take their own mode, feeds that publish
km/h (Dijon, Le Mans, Bourg-en-Bresse, Blois, Montluçon) are converted to
m/s, and internal route keys read as the line on the vehicle. Networks
already in the layer are not added a second time (#1716).
- Add 139 more keyless feeds to the Transit layer from the open GTFS-Realtime
catalogues, most of them United States agencies (Sacramento, Spokane,
Cleveland, St. Louis, Madison, Louisville, Fresno, Broward County and many
smaller systems), plus Zagreb, Bilbao's metro, Chișinău, Parma and a few
Québec networks. Operators whose terms were read are credited as those
terms ask, and every feed is recorded in DATA_SOURCES.md. Feeds whose
terms need a key, registration or permission (New York City buses,
Detroit, Pittsburgh and others) are left out (#1716).
- Add 55 Japanese bus networks to the Transit layer, the first in Toyama,
Mie, Kumamoto, Kōchi, Aichi, Okinawa and Aomori prefectures, each recorded
under CC0 1.0 or CC BY 4.0 with the operator credited in Japanese (#1716).
- Show where Transit has coverage. From orbit each feed is a soft green
spot, hotter where networks cluster; at continental and regional zoom the
spots become patches the size of each network's service area, drawn under
the vehicles; close in they are off. Drawn from the feed registry, so no
feed is polled for it, and only feeds whose live positions have been seen
are shown (#1716).
+45
View File
@@ -2442,6 +2442,51 @@ future driver fix restores the effect with no code change; iOS/iPadOS platform
detection is the backstop for when no probe context can be created. Applied
during viewer construction, before any tile builds a draw command.
## Render failure notice
When the globe stops drawing, the scene chrome says so instead of leaving a
black globe behind working controls. `installRenderFailureNotice()`
(`src/ui/renderFailure.js`) listens for Cesium's `renderError` and the canvas'
`webglcontextlost`, logs the error, and publishes a notice that
`RenderFailure.vue` shows as an alert dialog with focus on its first action.
The app's viewer uses `showRenderLoopErrors: false`, so Cesium's dead-end
"Rendering has stopped" panel no longer appears; `applicationViewerOptions()`
keeps it by default for other callers.
- **Retry** restarts the render loop. The notice stays until a frame starts
and ends without an error: Cesium raises `postRender` after a failed frame
too.
- **Reload**, after a lost context, reloads the page; the address keeps the
view.
- **Reset view** opens `appRootUrl()`, without the hash or `/share/<code>`
path; the query, such as `?lang=`, is kept.
Pitfalls:
- An error thrown outside Scene's error handling (a frame listener, `onTick`,
a resize) stops the loop without a `renderError`. The notice wraps the
widget's `render()` and `resize()` and raises it as one, once even with
`rethrowRenderErrors`.
- A throwing listener leaves its Cesium event mid-raise, with listener changes
only queued. The wrapper finishes those raises first (`finishRaise()` over
`frameEvents()`), so a layer turned off or on meanwhile behaves.
- A render error marks the loop stopped while its next frame is still queued.
The wrapper keeps the loop marked running, so a Retry or a shown tab
(`src/app/tools.js`) before that frame starts no second loop.
- After a lost context, later render errors are only logged, so the notice
keeps saying why. The listener neither prevents nor restores the loss.
- When the globe draws again, focus goes back where it was, or to the globe's
canvas, which the map's keys read as the map; the canvas is focusable only
until focus leaves or the globe is pressed. Focus the user moved stays put.
- The notice sits above floating panels such as the Québec camera viewer, and
its buttons are 44 px tall on touch screens.
An inline conversation panel never reloads or navigates its host's page: it
offers Retry after a render error only, and nothing after a lost context, where
the panel runtime's own notice applies. Its timer (`keepPanelRendering()`)
draws only while the render loop is on. An error a timer frame throws is caught
there: it has already been raised as a render error.
## Map Source control ownership
Map Source controls own chip listeners, source-state subscriptions and selection
+7 -10
View File
@@ -557,6 +557,13 @@
"src/ui/frameRateMonitor.js",
"src/ui/frameRateMonitor.test.mjs",
"src/ui/frameRateView.test.mjs",
"src/ui/renderFailure.test.mjs",
"src/ui/renderFailureView.test.mjs",
"src/ui/renderFailureLoopErrors.test.mjs",
"src/ui/renderFailureEventRetry.test.mjs",
"src/app/renderFailureInlineRetry.test.mjs",
"src/ui/renderFailureSecondLoop.test.mjs",
"src/ui/renderLoopFixture.mjs",
"src/layers/cctv/lifecycle.js",
"src/layers/cctv/controls.js",
"src/layers/cctv/geometry.js",
@@ -801,16 +808,6 @@
"server/providers/transitHistory.js",
"server/providers/transitHistory.test.mjs",
"src/data/rioRouteNames.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsJapan.test.mjs",
"src/data/transitCoverage.js",
"src/data/transitCoverage.test.mjs",
"src/layers/transit/coverage.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitFeedsCatalogue.test.mjs",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsFrance.test.mjs",
"src/data/transitRouteNamesFrance.js",
"src/data/transitFeeds.js",
"src/data/transitFeeds.test.mjs",
"src/data/transitProxy.js",
+2 -48
View File
@@ -163,6 +163,7 @@
"src/ui/kit/UiBadge.vue",
"src/ui/stores/globeActions.js",
"src/ui/views/panels/GlobeActions.vue",
"src/ui/views/panels/RenderFailure.vue",
"src/ui/views/panels/GlobeCompass.vue",
"src/ui/stores/recording.js",
"src/ui/styleParameters.js",
@@ -3113,6 +3114,7 @@
"src/ui/displayControls.js",
"src/ui/effects.js",
"src/ui/frameRateMonitor.js",
"src/ui/renderFailure.js",
"src/ui/layerPanel.js",
"src/ui/weatherPanel.js",
"src/ui/railCards.js",
@@ -3304,10 +3306,6 @@
"src/data/transit.js",
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/data/transitIcons.js",
"src/data/transitPresetStyle.js",
"src/layers/transit/controls.js",
@@ -3322,8 +3320,6 @@
"src/layers/transit/state.js",
"src/layers/transit/testing.js",
"src/layers/transit/viewport.js",
"src/layers/transit/coverage.js",
"src/data/transitCoverage.js",
"src/data/cameraSensitivity.js",
"src/data/contactPlayback.js",
"src/data/contactTrailRenderer.js",
@@ -4293,10 +4289,6 @@
"src/data/railTimeZones.js",
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/data/transitIcons.js",
"src/data/transitPresetStyle.js",
"src/layers/transit/controls.js",
@@ -4311,8 +4303,6 @@
"src/layers/transit/state.js",
"src/layers/transit/testing.js",
"src/layers/transit/viewport.js",
"src/layers/transit/coverage.js",
"src/data/transitCoverage.js",
"src/data/cameraSensitivity.js",
"src/data/contactPlayback.js",
"src/data/contactTrailRenderer.js",
@@ -5151,10 +5141,6 @@
"src/data/gtfsRealtime.js",
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/geoDistance.js",
"src/data/transitProxy.js",
"src/sources/httpBody.js",
@@ -5172,10 +5158,6 @@
"src/data/gtfsRealtime.js",
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/geoDistance.js",
"src/data/transitProxy.js",
"src/sources/transitHistory.js"
@@ -5210,10 +5192,6 @@
"src/data/transit.js",
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/data/transitIcons.js",
"src/data/transitPresetStyle.js",
"src/data/transitProxy.js",
@@ -5232,8 +5210,6 @@
"src/layers/transit/testing.js",
"src/layers/transit/trails.js",
"src/layers/transit/viewport.js",
"src/layers/transit/coverage.js",
"src/data/transitCoverage.js",
"src/overlays/worldOverlay.js",
"src/overlays/worldOverlayDraw.js",
"src/overlays/worldOverlayTokens.js",
@@ -5279,10 +5255,6 @@
"src/data/inputOwnership.js",
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/geoDistance.js",
"src/data/transitIcons.js",
"src/data/transitPresetStyle.js",
@@ -5298,8 +5270,6 @@
"src/layers/transit/state.js",
"src/layers/transit/testing.js",
"src/layers/transit/viewport.js",
"src/layers/transit/coverage.js",
"src/data/transitCoverage.js",
"src/data/cameraSensitivity.js",
"src/data/contactPlayback.js",
"src/data/contactTrailRenderer.js",
@@ -5357,10 +5327,6 @@
"src/data/railNoPositions.js",
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/geoDistance.js",
"src/data/transitProxy.js",
"src/data/gtfsRealtime.js",
@@ -5391,10 +5357,6 @@
"src/data/railNoPositions.js",
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/geoDistance.js",
"src/data/transitProxy.js",
"src/data/gtfsRealtime.js",
@@ -5411,10 +5373,6 @@
"modules": [
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/geoDistance.js",
"src/data/transitProxy.js",
"src/data/gtfsRealtime.js",
@@ -5682,10 +5640,6 @@
"src/layers/transit/policy.js",
"src/data/transitFeeds.js",
"src/data/rioRouteNames.js",
"src/data/transitFeedsFrance.js",
"src/data/transitFeedsJapan.js",
"src/data/transitFeedsCatalogue.js",
"src/data/transitRouteNamesFrance.js",
"src/layers/transit/movement.js",
"src/data/contactPlayback.js",
"src/data/layerSnapshot.js",
@@ -400,6 +400,27 @@ test('Enter finishes from the canvas and the label field, never from a focused b
}
});
test('Enter on the globe canvas finishes, as after the render failure notice hands focus back', async () => {
const h = harness();
try {
h.act('toggleDraw');
h.tool.setShape('area');
h.tool.addVertex(-97.75, 30.26);
h.tool.addVertex(-97.74, 30.26);
h.tool.addVertex(-97.74, 30.27);
// src/ui/views/panels/RenderFailure.vue focuses the canvas when the globe
// draws again and the control that had focus is gone.
h.documentDouble.emit('keydown', { key: 'Enter', target: h.viewer.scene.canvas });
await Promise.resolve();
await Promise.resolve();
assert.equal(h.annotations.calls.annotate.length, 1, 'Enter on the canvas finishes');
assert.equal(h.annotations.calls.annotate[0].specs[0].type, 'area');
} finally {
h.restore();
}
});
test('the live preview hangs at the height the click landed on', async () => {
const h = harness();
try {
+13 -11
View File
@@ -122,28 +122,30 @@ export function keepPanelRendering(
frameRequest = windowRef.requestAnimationFrame(onFrame);
};
frameRequest = windowRef.requestAnimationFrame(onFrame);
// Cesium stops drawing after a render error, and so does the timer.
// Report the error in full: workers report plain objects, which Cesium
// prints as [object Object].
let failed = false;
// Cesium stops its render loop after a render error, and the timer stops
// with it; Retry restarts both. Report the error in full: workers report
// plain objects, which Cesium prints as [object Object].
const removeErrorListener = viewer.scene?.renderError?.addEventListener(
(_scene, error) => {
failed = true;
(_scene, error) =>
console.error(
"[God's Eye View panel] render error:",
describeError(error),
);
},
),
);
const timer = windowRef.setInterval(() => {
if (
failed ||
viewer.useDefaultRenderLoop === false ||
now() - lastFrame < MISSED_FRAMES_MS ||
viewer.isDestroyed?.()
)
return;
viewer.resize();
viewer.render();
try {
viewer.resize();
viewer.render();
} catch {
// Raised as a render error by now (src/ui/renderFailure.js), which
// stops this timer; thrown on, it would only reach the host page.
}
}, PANEL_FRAME_MS);
return () => {
windowRef.clearInterval(timer);
+2
View File
@@ -482,6 +482,8 @@ test('a panel reports a render error in full and stops drawing', () => {
try {
let clock = 0;
const stop = keepPanelRendering(viewer, { windowRef, now: () => clock });
// Cesium stops its render loop after a render error.
viewer.useDefaultRenderLoop = false;
listener(viewer.scene, { name: 'RuntimeError', message: 'worker failed' });
assert.match(logged[0], /render error: .*"message":"worker failed"/);
clock = 1000;
+120
View File
@@ -0,0 +1,120 @@
// Retry in an inline panel whose host stops animation frames, so the panel
// draws from keepPanelRendering's timer. Drives Cesium's real startRenderLoop
// and useDefaultRenderLoop setter (CesiumWidget.prototype).
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { keepPanelRendering } from './embed.js';
import { renderLoopFixture } from '../ui/renderLoopFixture.mjs';
function setup() {
let tick = null;
let clock = 0;
// The host reports the panel hidden: its own animation frames never arrive.
const windowRef = {
requestAnimationFrame: () => 1,
cancelAnimationFrame() {},
setInterval: (fn) => ((tick = fn), 1),
clearInterval() {},
};
const previousError = console.error;
console.error = () => {};
const state = { failure: null, drawn: 0 };
const scene = {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
postRender: new Cesium.Event(),
requestRender() {},
initializeFrame() {},
// Scene.render: a failing primitive update raises renderError
// (tryAndCatchError) and returns; a failing listener throws.
render() {
state.drawn += 1;
this.preUpdate.raiseEvent(this);
if (state.failure === 'caught')
this.renderError.raiseEvent(this, new Error('boom'));
if (state.failure === 'thrown') throw new Error('boom');
this.postRender.raiseEvent(this);
},
};
const { frames, widget, viewer, store, runFrames, restore } =
renderLoopFixture(scene, {
viewer: {
isDestroyed: () => false,
render: () => widget.render(),
resize: () => widget.resize(),
},
notice: { allowReload: false },
});
const stop = keepPanelRendering(viewer, { windowRef, now: () => clock });
viewer.useDefaultRenderLoop = true;
const timerFrame = () => {
clock += 400;
tick();
};
return {
frames,
state,
store,
timerFrame,
animationFrames: runFrames,
restore: () => {
stop();
restore();
console.error = previousError;
},
};
}
for (const failure of ['caught', 'thrown']) {
test(`an error ${failure} in a frame the timer draws: Retry draws again with one render loop`, () => {
const { frames, state, store, timerFrame, animationFrames, restore } =
setup();
try {
// The host stops animation frames with the loop's next one queued.
assert.equal(frames.length, 1);
state.failure = failure;
timerFrame(); // an error thrown here does not reach the host page
assert.equal(store.renderFailure?.reason, 'render-error');
assert.equal(typeof store.renderFailure.retry, 'function');
state.failure = null;
store.renderFailure.retry();
const before = state.drawn;
timerFrame();
assert.ok(state.drawn > before, 'the panel draws again after Retry');
assert.equal(
store.renderFailure,
null,
'a clean frame clears the notice',
);
assert.equal(frames.length, 1, 'Retry starts no second loop');
// The host resumes frames: the globe draws once a frame.
state.drawn = 0;
animationFrames(3);
assert.equal(state.drawn, 3);
} finally {
restore();
}
});
}
test('an error Scene caught in an animation frame right before the host stopped frames: Retry keeps one render loop', () => {
const { frames, state, store, timerFrame, animationFrames, restore } =
setup();
try {
state.failure = 'caught';
animationFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
assert.equal(frames.length, 1);
state.failure = null;
store.renderFailure.retry();
timerFrame();
assert.equal(store.renderFailure, null);
assert.equal(frames.length, 1, 'Retry starts no second loop');
state.drawn = 0;
animationFrames(3);
assert.equal(state.drawn, 3);
} finally {
restore();
}
});
+8
View File
@@ -15,6 +15,8 @@ import {
governorRequestRender,
} from '../renderGovernor.js';
import { describeError } from './errors.js';
import { isEmbeddedInline } from './embed.js';
import { installRenderFailureNotice } from '../ui/renderFailure.js';
import { worldClock, worldTimeFor } from '../time/worldClock.js';
import { followWorldTime } from '../time/viewerClock.js';
import { onLocaleChange, t } from '../i18n/index.js';
@@ -55,11 +57,17 @@ export async function createApplicationScene({
const viewer = createApplicationViewer({
container: 'cesiumContainer',
creditContainer,
// The render failure notice below replaces Cesium's dead-end panel.
showRenderLoopErrors: false,
});
defer(() => {
uninstallRenderGovernor(viewer);
if (!viewer.isDestroyed()) viewer.destroy();
});
// An inline panel must not reload or navigate its host's page.
defer(
installRenderFailureNotice(viewer, { allowReload: !isEmbeddedInline() }),
);
defer(installTrackpadPinchZoom(viewer));
defer(
followWorldTime(viewer, worldTimeFor('sky'), {
+16 -4
View File
@@ -375,8 +375,15 @@ export function installGlobeInViewRecovery(
};
}
/** Cesium.Viewer options of the standard globe viewer. */
export function applicationViewerOptions(creditContainer) {
/**
* Cesium.Viewer options of the standard globe viewer. `showRenderLoopErrors`
* false leaves render errors to the caller's own notice instead of Cesium's
* panel.
*/
export function applicationViewerOptions(
creditContainer,
{ showRenderLoopErrors = true } = {},
) {
return {
timeline: false,
animation: false,
@@ -396,16 +403,21 @@ export function applicationViewerOptions(creditContainer) {
creditContainer,
msaaSamples: 4,
contextOptions: { webgl: { preserveDrawingBuffer: true } },
showRenderLoopErrors,
};
}
/** Create the standard globe viewer in caller-owned, visible containers. */
export function createApplicationViewer({ container, creditContainer }) {
export function createApplicationViewer({
container,
creditContainer,
showRenderLoopErrors,
}) {
if (!container || !creditContainer)
throw new TypeError('Viewer and credit containers are required');
const viewer = new Cesium.Viewer(
container,
applicationViewerOptions(creditContainer),
applicationViewerOptions(creditContainer, { showRenderLoopErrors }),
);
try {
viewer.targetFrameRate = 60;
-142
View File
@@ -1,142 +0,0 @@
/**
* Coverage points for the Transit layer's globe view: one soft spot per
* registered feed, hotter where networks cluster.
*
* Built from the registry alone. No feed is polled to draw it, so it says
* where vehicles CAN be shown, not where any are right now. Only feeds whose
* live positions have been seen count: a registry row marked
* `liveSeen: false` (added while its services were not running) gets no
* spot until it is confirmed.
*
* Pure: no Cesium, no DOM.
*/
import { greatCircleKm } from '../geoDistance.js';
/** Feeds within this distance of a feed count as its neighbours. */
export const COVERAGE_NEIGHBOUR_KM = 150;
/**
* Radius of a spot from orbit, km: a lone feed, and the densest cluster.
* Far larger than any network: at whole-globe scale a real service area is
* under a pixel, and the point is to be seen.
*/
export const COVERAGE_FAR_RADIUS_KM = Object.freeze({ lone: 150, dense: 230 });
/**
* Radius of a spot at regional zoom, km: the feed's own polling radius,
* held between these bounds so a village network is still a visible patch
* and a regional coach network does not paint a whole country.
*/
export const COVERAGE_NEAR_RADIUS_KM = Object.freeze({ min: 22, max: 75 });
/** Heat is drawn in this many steps, one draw batch each. */
export const COVERAGE_HEAT_BUCKETS = 6;
/**
* Share of full strength one spot carries: 1 for a lone network, less for one
* in a crowd. Spots in a cluster overlap, and at full strength each they
* pile into a flat white blob with visible disc edges; thinned by the square
* root of the crowd they add up to one smooth field that still brightens
* toward its centre.
* @param {number} neighbours
* @returns {number} 0..1
*/
export function coverageSpotStrength(neighbours) {
const crowd = Math.max(0, Number(neighbours) || 0);
return Number((0.3 + 0.7 / Math.sqrt(1 + crowd)).toFixed(3));
}
/**
* The bus green of the vehicle markers, from a deep green for a lone network
* to a pale core where many overlap. The lone end is saturated on purpose: a
* light translucent green vanishes into pale basemaps and desert imagery.
*/
export const COVERAGE_PALETTE = Object.freeze([
{ at: 0.0, rgb: [24, 196, 106] },
{ at: 0.5, rgb: [94, 240, 138] },
{ at: 1.0, rgb: [206, 255, 196] },
]);
/**
* Palette colour at a normalized heat.
* @param {number} t 0..1 (clamped)
* @returns {[number, number, number]} RGB, 0..255.
*/
export function coverageColorAt(t) {
const v = Math.min(1, Math.max(0, Number(t) || 0));
let i = 1;
while (i < COVERAGE_PALETTE.length - 1 && COVERAGE_PALETTE[i].at < v) i += 1;
const a = COVERAGE_PALETTE[i - 1];
const b = COVERAGE_PALETTE[i];
const k = (v - a.at) / (b.at - a.at || 1);
return [0, 1, 2].map((c) => Math.round(a.rgb[c] + (b.rgb[c] - a.rgb[c]) * k));
}
/**
* One coverage point per feed that has a centre and has been seen live.
*
* `heat` is logarithmic in the number of neighbours and normalized against
* the densest feed, so a lone network is always 0 (the same deep green and
* the same size) however crowded the busiest region is, and a hundred French
* networks do not wash out one in Bismarck. Heat is then stepped into
* COVERAGE_HEAT_BUCKETS buckets, which set the spot's colour and strength.
*
* @param {ReadonlyArray<{id?: string, center: {lat: number, lon: number}}>} feeds
* @param {{neighbourKm?: number}} [options]
* @returns {Array<{id: string|null, lat: number, lon: number, neighbours: number,
* heat: number, bucket: number, farRadiusKm: number, nearRadiusKm: number,
* strength: number, rgb: [number, number, number]}>}
*/
export function buildTransitCoveragePoints(
feeds,
{ neighbourKm = COVERAGE_NEIGHBOUR_KM } = {},
) {
const centred = (feeds || []).filter(
(feed) =>
feed?.liveSeen !== false &&
Number.isFinite(feed?.center?.lat) &&
Number.isFinite(feed?.center?.lon),
);
const neighbours = centred.map(
(feed) =>
centred.filter(
(other) =>
other !== feed &&
greatCircleKm(feed.center, other.center) <= neighbourKm,
).length,
);
const densest = Math.max(0, ...neighbours);
return centred.map((feed, index) => {
const heat = densest
? Math.log1p(neighbours[index]) / Math.log1p(densest)
: 0;
const top = COVERAGE_HEAT_BUCKETS - 1;
const bucket = Math.round(heat * top);
return {
id: feed.id ?? null,
lat: feed.center.lat,
lon: feed.center.lon,
neighbours: neighbours[index],
heat,
bucket,
farRadiusKm: Math.round(
COVERAGE_FAR_RADIUS_KM.lone +
(COVERAGE_FAR_RADIUS_KM.dense - COVERAGE_FAR_RADIUS_KM.lone) * heat,
),
nearRadiusKm: Math.min(
COVERAGE_NEAR_RADIUS_KM.max,
Math.max(
COVERAGE_NEAR_RADIUS_KM.min,
Number(feed.loadRadiusKm) || COVERAGE_NEAR_RADIUS_KM.min,
),
),
// Colour and strength follow the bucket, so every spot in a batch
// shares one material.
strength: coverageSpotStrength(
Math.expm1((bucket / top) * Math.log1p(densest || 1)),
),
rgb: coverageColorAt(bucket / top),
};
});
}
-190
View File
@@ -1,190 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
COVERAGE_FAR_RADIUS_KM,
COVERAGE_HEAT_BUCKETS,
COVERAGE_NEAR_RADIUS_KM,
buildTransitCoveragePoints,
coverageColorAt,
coverageSpotStrength,
} from './transitCoverage.js';
import { TRANSIT_ENABLED_FEEDS } from './transitFeeds.js';
import {
ACTIVATION_ENTER_ALTITUDE_M,
ACTIVATION_EXIT_ALTITUDE_M,
COVERAGE_OPACITY_STOPS,
COVERAGE_PEAK_ALPHA,
coverageOpacityAt,
} from '../layers/transit/policy.js';
const feed = (id, lat, lon) => ({ id, center: { lat, lon } });
test('an empty registry gives no points, and a feed without a centre is skipped', () => {
assert.deepEqual(buildTransitCoveragePoints([]), []);
assert.deepEqual(buildTransitCoveragePoints(null), []);
const points = buildTransitCoveragePoints([
feed('a', 10, 10),
{ id: 'no-centre' },
{ id: 'bad', center: { lat: NaN, lon: 3 } },
]);
assert.deepEqual(
points.map((p) => p.id),
['a'],
);
});
test('a lone feed is the smallest, deepest spot', () => {
const [point] = buildTransitCoveragePoints([feed('lone', 46.8, -100.8)]);
assert.equal(point.neighbours, 0);
assert.equal(point.heat, 0);
assert.equal(point.bucket, 0);
assert.equal(point.strength, 1);
assert.equal(point.farRadiusKm, COVERAGE_FAR_RADIUS_KM.lone);
assert.deepEqual(point.rgb, coverageColorAt(0));
});
test('a lone feed looks the same however dense the busiest region is', () => {
const lone = feed('lone', 46.8, -100.8);
const cluster = Array.from({ length: 40 }, (_, i) =>
feed(`c${i}`, 48 + (i % 8) * 0.1, 2 + Math.floor(i / 8) * 0.1),
);
const alone = buildTransitCoveragePoints([lone])[0];
const crowded = buildTransitCoveragePoints([lone, ...cluster]);
assert.deepEqual(
crowded.find((p) => p.id === 'lone'),
alone,
);
const hottest = crowded.find((p) => p.id === 'c0');
assert.equal(hottest.neighbours, 39);
assert.equal(hottest.heat, 1);
assert.equal(hottest.bucket, COVERAGE_HEAT_BUCKETS - 1);
assert.equal(hottest.farRadiusKm, COVERAGE_FAR_RADIUS_KM.dense);
assert.ok(hottest.strength < 1, 'a spot in a crowd is thinner');
});
test('a spot in a crowd is thinner, so a cluster sums smoothly instead of clipping', () => {
assert.equal(coverageSpotStrength(0), 1);
assert.equal(coverageSpotStrength(-3), 1);
assert.equal(coverageSpotStrength(NaN), 1);
let previous = 1;
for (const crowd of [1, 3, 8, 15, 28, 100]) {
const strength = coverageSpotStrength(crowd);
assert.ok(strength < previous, `thinner with ${crowd} neighbours`);
assert.ok(strength > 0.3, 'never invisible');
// The crowd as a whole still outshines a lone feed.
assert.ok(strength * (1 + crowd) > 1);
previous = strength;
}
const [lone] = buildTransitCoveragePoints([feed('lone', 0, 0)]);
assert.equal(lone.strength, 1);
});
test('neighbours are counted by distance, across the antimeridian too', () => {
const points = buildTransitCoveragePoints([
feed('west', 0, 179.8),
feed('east', 0, -179.8),
feed('far', 0, 170),
]);
const by = Object.fromEntries(points.map((p) => [p.id, p.neighbours]));
assert.deepEqual(by, { west: 1, east: 1, far: 0 });
});
test('the palette runs from the lone green to a pale core and clamps', () => {
assert.deepEqual(coverageColorAt(-1), coverageColorAt(0));
assert.deepEqual(coverageColorAt(2), coverageColorAt(1));
assert.deepEqual(coverageColorAt(NaN), coverageColorAt(0));
const brightness = (t) => coverageColorAt(t).reduce((a, b) => a + b, 0);
let previous = 0;
for (let t = 0; t <= 1.0001; t += 0.1) {
assert.ok(brightness(t) >= previous, `does not darken at ${t.toFixed(1)}`);
previous = brightness(t);
}
});
test('a feed whose live positions have not been seen gets no spot', () => {
const points = buildTransitCoveragePoints([
feed('seen', 10, 10),
{ ...feed('unseen', 10.1, 10.1), liveSeen: false },
]);
assert.deepEqual(
points.map((p) => p.id),
['seen'],
);
assert.equal(points[0].neighbours, 0, 'an unseen feed is not a neighbour');
// The registry's own unconfirmed rows stay out; the confirmed ones are in.
const unseen = TRANSIT_ENABLED_FEEDS.filter((f) => f.liveSeen === false);
assert.ok(unseen.length > 0);
const real = new Set(
buildTransitCoveragePoints(TRANSIT_ENABLED_FEEDS).map((p) => p.id),
);
for (const row of unseen) assert.equal(real.has(row.id), false, row.id);
assert.ok(real.has('kumamoto-toshi-bus'));
assert.ok(real.has('sendai-city-bus'));
});
test('every feed seen live gets exactly one spot, and dense regions read hotter', () => {
const points = buildTransitCoveragePoints(TRANSIT_ENABLED_FEEDS);
assert.equal(
points.length,
TRANSIT_ENABLED_FEEDS.filter((f) => f.liveSeen !== false).length,
);
assert.equal(new Set(points.map((p) => p.id)).size, points.length);
const heat = (id) => points.find((p) => p.id === id).heat;
assert.ok(heat('axo-creil') > heat('mbta'), 'the Oise outranks Boston');
for (const point of points) {
assert.ok(point.heat >= 0 && point.heat <= 1);
assert.ok(
point.farRadiusKm >= COVERAGE_FAR_RADIUS_KM.lone &&
point.farRadiusKm <= COVERAGE_FAR_RADIUS_KM.dense,
);
assert.ok(
point.nearRadiusKm >= COVERAGE_NEAR_RADIUS_KM.min &&
point.nearRadiusKm <= COVERAGE_NEAR_RADIUS_KM.max,
);
assert.ok(point.bucket >= 0 && point.bucket < COVERAGE_HEAT_BUCKETS);
}
});
test('a regional patch is the feed’s own service area, within bounds', () => {
const at = (loadRadiusKm) =>
buildTransitCoveragePoints([{ ...feed('f', 0, 0), loadRadiusKm }])[0]
.nearRadiusKm;
assert.equal(at(30), 30);
assert.equal(at(5), COVERAGE_NEAR_RADIUS_KM.min);
assert.equal(at(210), COVERAGE_NEAR_RADIUS_KM.max);
assert.equal(at(undefined), COVERAGE_NEAR_RADIUS_KM.min);
});
test('wide spots own the globe view, service-area patches the regional view, and they overlap', () => {
const far = (h) => coverageOpacityAt(h, 'far');
const near = (h) => coverageOpacityAt(h, 'near');
assert.equal(far(20_000_000), 1);
assert.equal(far(Infinity), 1);
assert.equal(near(20_000_000), 0);
assert.equal(far(500_000), 0);
assert.ok(near(500_000) >= 0.5);
assert.equal(far(0), 0);
assert.equal(near(0), 0);
assert.equal(near(60_000), 0, 'off close in, where vehicles are the picture');
assert.equal(far(NaN), 0);
assert.equal(near(NaN), 0);
assert.equal(coverageOpacityAt(20_000_000), 1, 'the default set is far');
// From 250 km up there is never a height with nothing drawn, and the
// cross-fade has no dip: the two sets together stay well visible.
for (let h = 250_000; h <= 6_000_000; h += 25_000) {
assert.ok(far(h) + near(h) >= 0.45, `visible at ${h / 1000} km`);
}
// Both sides of the altitude gate are covered.
assert.ok(
far(ACTIVATION_ENTER_ALTITUDE_M) + near(ACTIVATION_ENTER_ALTITUDE_M) > 0.5,
);
assert.ok(far(ACTIVATION_EXIT_ALTITUDE_M) > 0.5);
// The far set only grows with height.
let previous = 0;
for (let h = 0; h <= 4_000_000; h += 20_000) {
assert.ok(far(h) >= previous, `far opacity does not fall at ${h} m`);
previous = far(h);
}
assert.ok(COVERAGE_PEAK_ALPHA > 0.5 && COVERAGE_PEAK_ALPHA <= 1);
assert.equal(COVERAGE_OPACITY_STOPS.far.at(-1)[1], 1);
});
-64
View File
@@ -1,9 +1,5 @@
import { greatCircleKm } from '../geoDistance.js';
import { RIO_ROUTE_NAMES } from './rioRouteNames.js';
import { CATALOGUE_TRANSIT_FEED_ROWS } from './transitFeedsCatalogue.js';
import { FRANCE_TRANSIT_FEED_ROWS } from './transitFeedsFrance.js';
import { JAPAN_TRANSIT_FEED_ROWS } from './transitFeedsJapan.js';
import { FRANCE_ROUTE_NAMES } from './transitRouteNamesFrance.js';
/**
* @module transitFeeds
@@ -14,10 +10,6 @@ import { FRANCE_ROUTE_NAMES } from './transitRouteNamesFrance.js';
* `/api/transit/vehicles/<id>` for a registered id (see SECURITY.md: proxies
* never fetch client-supplied URLs). Adding a feed means adding a row here,
* a DATA_SOURCES.md row with its license, and a credit in dataCredits.js.
* The French networks listed on transport.data.gouv.fr are rows of the same
* shape in `transitFeedsFrance.js`, and feeds added from the open catalogues
* are in `transitFeedsCatalogue.js` and `transitFeedsJapan.js`; all are appended
* to the registry below.
*
* Admission rules for a feed:
* - No key, token, or registration required (identify-yourself headers are
@@ -570,58 +562,6 @@ function transitFeed(fields) {
});
}
/**
* A French network row (`transitFeedsFrance.js`) as a registry entry. Every
* one is a bus network unless its row says otherwise: `routeModes` names the
* route ids that are trams or boats, and a route id it does not name stays a
* default, never a resolved "bus". The licence name, record link and terms
* note are built from the row's `licence`, `record`, `vehicles` and `note`.
* A feed with a table in
* `transitRouteNamesFrance.js` reads its line names from it; an id missing
* from the table renders unchanged.
* @param {object} row
* @returns {Readonly<object>}
*/
function franceTransitFeed(row) {
const { routeModes, licence, record, vehicles, note, ...fields } = row;
const odbl = licence === 'odc-odbl';
const names = Object.hasOwn(FRANCE_ROUTE_NAMES, row.id)
? FRANCE_ROUTE_NAMES[row.id]
: null;
return transitFeed({
defaultMode: 'bus',
...fields,
license: odbl ? 'ODbL' : 'Licence Ouverte 2.0',
licenseUrl: `https://transport.data.gouv.fr/datasets/${record}`,
terms: {
quote: `licence: ${licence}`,
note: [
`Dataset record’s licence field quoted; this is the record’s own GTFS-RT vehicle feed (${vehicles} vehicles on 2026-11-13).`,
odbl
? 'ODbL asks for credit and share-alike on a derived database; the layer draws positions and redistributes no database.'
: '',
note ?? '',
]
.filter(Boolean)
.join(' '),
},
...(routeModes
? {
routeMode: (routeId) =>
routeId && Object.hasOwn(routeModes, routeId)
? routeModes[routeId]
: 'unknown',
}
: {}),
...(names
? {
routeLabel: (routeId) =>
routeId && Object.hasOwn(names, routeId) ? 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.
@@ -1646,10 +1586,6 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
},
defaultMode: 'bus',
}),
...FRANCE_TRANSIT_FEED_ROWS.map(franceTransitFeed),
...[...CATALOGUE_TRANSIT_FEED_ROWS, ...JAPAN_TRANSIT_FEED_ROWS].map((row) =>
transitFeed({ defaultMode: 'bus', ...row }),
),
]);
/**
-5
View File
@@ -713,13 +713,8 @@ test('feeds that publish km/h are marked for conversion, and only those', () =>
.map((f) => f.id)
.sort();
assert.deepEqual(kmh, [
'azalys-blois',
'carris-metropolitana-lisboa',
'divia-dijon',
'maelis-montlucon',
'mpm-toulon',
'rubis-bourg-en-bresse',
'setram-le-mans',
'tam-montpellier',
]);
});
File diff suppressed because it is too large. Load diff
-125
View File
@@ -1,125 +0,0 @@
import { readFileSync } from 'node:fs';
import test from 'node:test';
import assert from 'node:assert/strict';
import {
TRANSIT_FEED_REGISTRY,
TRANSIT_FEED_ID_PATTERN,
getTransitFeed,
publicTransitCatalog,
transitFeedsInRange,
transitModeFor,
} from './transitFeeds.js';
import { CATALOGUE_TRANSIT_FEED_ROWS } from './transitFeedsCatalogue.js';
test('every catalogue row becomes a live, keyless feed that covers its own centre', () => {
const ids = new Set();
for (const row of CATALOGUE_TRANSIT_FEED_ROWS) {
assert.match(row.id, TRANSIT_FEED_ID_PATTERN);
assert.equal(ids.has(row.id), false, `${row.id} is unique`);
ids.add(row.id);
const feed = getTransitFeed(row.id);
assert.ok(feed, `${row.id} is routable`);
assert.ok(
publicTransitCatalog().some((entry) => entry.id === row.id),
`${row.id} is offered to the browser`,
);
const url = new URL(row.url);
assert.equal(url.protocol, 'https:');
assert.equal(/key=/i.test(url.search), false, `${row.id} has no key`);
assert.match(
feed.terms.note,
/\d+ vehicles on 2026-11-13\./,
`${row.id} records the day it was seen live`,
);
assert.ok(
transitFeedsInRange(row.center.lat, row.center.lon).some(
(entry) => entry.id === row.id,
),
`${row.id} covers its own centre`,
);
}
});
test('a catalogue row either quotes terms that were read or links its catalogue record', () => {
let read = 0;
for (const row of CATALOGUE_TRANSIT_FEED_ROWS) {
if (row.license === 'Public realtime vehicle feed') {
assert.equal(row.terms.quote, undefined, `${row.id} quotes nothing`);
assert.match(
row.terms.note,
/Keyless feed on .+ Shipped on the same basis as Metro Transit and OVapi|Same server and basis/,
`${row.id} names the basis it ships on`,
);
assert.match(
row.licenseUrl,
/^https:\/\/(mobilitydatabase\.org|www\.transit\.land|gtfsrt\.io)\//,
`${row.id} links its catalogue record`,
);
} else {
read++;
assert.ok(row.terms.quote, `${row.id} quotes the grant`);
assert.match(
row.terms.note,
/read 2026-11-13|same click-through agreement/,
`${row.id} says when its terms were read`,
);
}
}
assert.equal(read, 10);
});
test('feeds whose terms forbid this use, or could not be read, are not registered', () => {
const hosts = new Set(
TRANSIT_FEED_REGISTRY.map((feed) => new URL(feed.url).hostname),
);
for (const host of [
// Terms exclude keyless public display.
'gtfsrt.prod.obanyc.com',
'www.myddotbus.com',
'everett.mapstrat.com',
'duluthtransit.com',
'ridetime.greaterdaytonrta.org',
'cttprdtmgtfs.ctttrpcloud.com',
'truetime.portauthority.org',
// Terms could not be read.
'gtfs.sofiatraffic.bg',
'track.ua-gis.com',
'www.ridedart.com',
'ride.smtd.org',
'knoxville.syncromatics.com',
'bustracker.pvta.com',
'bus.gocitybus.com',
// A third party's republication.
'proxy.busone.app',
]) {
assert.equal(hosts.has(host), false, `${host} is not fetched`);
}
});
test('licences that ask for specific credit get it', () => {
const bilbao = getTransitFeed('metro-bilbao');
assert.equal(bilbao.creditDated, true);
assert.equal(transitModeFor(bilbao, null), 'subway');
const fairfax = getTransitFeed('fairfax-connector');
assert.match(fairfax.attribution, /Visit fairfaxconnector\.com/);
assert.equal(fairfax.sourceUrl, 'https://www.fairfaxconnector.com');
assert.match(getTransitFeed('gcrta-cleveland').attribution, /GCRTA/);
assert.equal(transitModeFor(getTransitFeed('marc-train-md'), null), 'rail');
assert.equal(transitModeFor(getTransitFeed('ny-waterway'), null), 'ferry');
});
test('every catalogue feed is documented with its licence', () => {
const lines = readFileSync(
new URL('../../DATA_SOURCES.md', import.meta.url),
'utf8',
).split('\n');
for (const row of CATALOGUE_TRANSIT_FEED_ROWS) {
const line = lines.find((l) => l.startsWith(`| **${row.name}** `));
assert.ok(line, `${row.name} has a DATA_SOURCES row`);
assert.ok(line.includes(row.license), `${row.name} row names its licence`);
assert.ok(
line.includes(row.licenseUrl),
`${row.name} row links its source`,
);
}
});
File diff suppressed because it is too large. Load diff
-201
View File
@@ -1,201 +0,0 @@
import { readFileSync } from 'node:fs';
import test from 'node:test';
import assert from 'node:assert/strict';
import {
TRANSIT_FEED_REGISTRY,
TRANSIT_FEED_ID_PATTERN,
getTransitFeed,
publicTransitCatalog,
transitFeedsInRange,
transitModeFor,
transitModeResolved,
transitRouteLabel,
} from './transitFeeds.js';
import { FRANCE_TRANSIT_FEED_ROWS } from './transitFeedsFrance.js';
import { FRANCE_ROUTE_NAMES } from './transitRouteNamesFrance.js';
import {
TRANSIT_ADMISSION_MAX_GLOBAL,
TRANSIT_ADMISSION_WINDOW_MS,
TRANSIT_PROXY_TTL_MS,
} from './transitProxy.js';
const FRANCE_IDS = new Set(FRANCE_TRANSIT_FEED_ROWS.map((row) => row.id));
test('every French row becomes a live feed under its record’s open licence', () => {
assert.equal(FRANCE_IDS.size, FRANCE_TRANSIT_FEED_ROWS.length);
for (const row of FRANCE_TRANSIT_FEED_ROWS) {
assert.match(row.id, TRANSIT_FEED_ID_PATTERN);
const feed = getTransitFeed(row.id);
assert.ok(feed, `${row.id} is routable`);
assert.ok(
publicTransitCatalog().some((entry) => entry.id === row.id),
`${row.id} is offered to the browser`,
);
assert.ok(
['lov2', 'odc-odbl'].includes(row.licence),
`${row.id} states an open licence`,
);
assert.equal(
feed.license,
row.licence === 'lov2' ? 'Licence Ouverte 2.0' : 'ODbL',
);
assert.equal(
feed.licenseUrl,
`https://transport.data.gouv.fr/datasets/${row.record}`,
`${row.id} links its dataset record`,
);
assert.equal(feed.terms.quote, `licence: ${row.licence}`);
assert.match(
feed.terms.note,
/\(\d+ vehicles on 2026-11-13\)/,
`${row.id} records the day it was seen live`,
);
assert.ok(row.vehicles > 0, `${row.id} had vehicles when it was added`);
assert.ok(
transitFeedsInRange(row.center.lat, row.center.lon).some(
(entry) => entry.id === row.id,
),
`${row.id} covers its own centre`,
);
}
});
test('no French row repeats a feed that is already registered', () => {
const seen = new Map();
for (const feed of TRANSIT_FEED_REGISTRY) {
const url = new URL(feed.url);
const key = `${url.origin}${url.pathname.replace(/\/$/, '')}${url.search}`;
assert.equal(
seen.has(key),
false,
`${feed.id} and ${seen.get(key)} fetch the same upstream`,
);
seen.set(key, feed.id);
}
// The networks that were already here through the operator's own endpoint
// stay single: no French row points at a dataset record one of them cites.
const earlier = TRANSIT_FEED_REGISTRY.filter(
(feed) => !FRANCE_IDS.has(feed.id),
);
for (const id of [
'tbm-bordeaux',
'tam-montpellier',
'astuce-rouen',
'mpm-toulon',
'lia-le-havre',
'filbleu-tours',
]) {
assert.ok(earlier.some((feed) => feed.id === id));
}
const earlierRecords = new Set(earlier.map((feed) => feed.licenseUrl));
for (const row of FRANCE_TRANSIT_FEED_ROWS) {
assert.equal(
earlierRecords.has(getTransitFeed(row.id).licenseUrl),
false,
`${row.id} is not a second copy of an earlier feed`,
);
}
});
test('one viewpoint never asks for more feeds than the proxy admits', () => {
// The browser polls each in-range feed once per proxy TTL, and the proxy
// admits a fixed number of upstream requests a minute across all feeds.
const pollsPerFeed = TRANSIT_ADMISSION_WINDOW_MS / TRANSIT_PROXY_TTL_MS;
for (const feed of TRANSIT_FEED_REGISTRY) {
const inRange = transitFeedsInRange(feed.center.lat, feed.center.lon);
assert.ok(
inRange.length * pollsPerFeed <= TRANSIT_ADMISSION_MAX_GLOBAL,
`${inRange.length} feeds overlap at ${feed.id}`,
);
}
});
test('French trams and boats take their mode; every other route stays a default', () => {
const cases = [
['divia-dijon', '4-T1', 'tram'],
['bibus-brest', 'A', 'tram'],
['setram-le-mans', 'T2', 'tram'],
['lignes-dazur-nice', 'L2', 'tram'],
['irigo-angers', 'C', 'tram'],
['orizo-avignon', 'T1', 'tram'],
['transvilles-valenciennes', 'NAVINEO:Line::190:LOC', 'tram'],
['aleop-pays-de-la-loire', 'YEU', 'ferry'],
['martinique-sud', '4001', 'ferry'],
];
for (const [id, routeId, mode] of cases) {
const feed = getTransitFeed(id);
assert.equal(transitModeFor(feed, routeId), mode, `${id} ${routeId}`);
assert.equal(transitModeResolved(feed, routeId), true);
assert.equal(transitModeFor(feed, 'no-such-route'), 'bus');
assert.equal(
transitModeResolved(feed, 'no-such-route'),
false,
`${id} defaults rather than resolves an unlisted route`,
);
}
const busOnly = getTransitFeed('ametis-amiens');
assert.equal(transitModeFor(busOnly, '7'), 'bus');
assert.equal(transitModeResolved(busOnly, '7'), false);
});
test('French feeds that publish km/h are marked for conversion, and only those', () => {
assert.deepEqual(
FRANCE_TRANSIT_FEED_ROWS.filter((row) => row.speedUnit === 'km/h')
.map((row) => row.id)
.sort(),
[
'azalys-blois',
'divia-dijon',
'maelis-montlucon',
'rubis-bourg-en-bresse',
'setram-le-mans',
],
);
for (const row of FRANCE_TRANSIT_FEED_ROWS.filter((r) => r.speedUnit)) {
assert.match(getTransitFeed(row.id).terms.note, /Speeds are km\/h/);
}
});
test('French route-name tables belong to registered feeds and never blank a line', () => {
for (const [id, names] of Object.entries(FRANCE_ROUTE_NAMES)) {
assert.ok(FRANCE_IDS.has(id), `${id} is a registered French feed`);
const feed = getTransitFeed(id);
for (const [routeId, name] of Object.entries(names)) {
assert.ok(typeof name === 'string' && name.trim() === name && name);
assert.notEqual(name, routeId, `${id} ${routeId} is not a no-op`);
assert.equal(transitRouteLabel(feed, routeId), name);
}
assert.equal(transitRouteLabel(feed, 'no-such-route'), 'no-such-route');
assert.equal(transitRouteLabel(feed, null), null);
}
assert.equal(transitRouteLabel(getTransitFeed('divia-dijon'), '4-T1'), 'T1');
assert.equal(
transitRouteLabel(
getTransitFeed('transvilles-valenciennes'),
'NAVINEO:Line::190:LOC',
),
'T1',
);
// A feed whose ids already read as the line has no table and is left alone.
assert.equal(transitRouteLabel(getTransitFeed('ametis-amiens'), '7'), '7');
});
test('every French feed is documented with its licence', () => {
const lines = readFileSync(
new URL('../../DATA_SOURCES.md', import.meta.url),
'utf8',
).split('\n');
for (const row of FRANCE_TRANSIT_FEED_ROWS) {
const feed = getTransitFeed(row.id);
const line = lines.find((l) => l.startsWith(`| **${feed.name}** `));
assert.ok(line, `${feed.name} has a DATA_SOURCES row`);
assert.ok(
line.includes(feed.licenseUrl),
`${feed.name} row links its record`,
);
assert.ok(
line.includes(feed.license),
`${feed.name} row names its licence`,
);
}
});
File diff suppressed because it is too large. Load diff
-74
View File
@@ -1,74 +0,0 @@
import { readFileSync } from 'node:fs';
import test from 'node:test';
import assert from 'node:assert/strict';
import {
TRANSIT_FEED_ID_PATTERN,
getTransitFeed,
publicTransitCatalog,
transitFeedsInRange,
} from './transitFeeds.js';
import { JAPAN_TRANSIT_FEED_ROWS } from './transitFeedsJapan.js';
test('every Japanese row is a live, keyless feed under CC0 or CC BY', () => {
const ids = new Set();
for (const row of JAPAN_TRANSIT_FEED_ROWS) {
assert.match(row.id, TRANSIT_FEED_ID_PATTERN);
assert.equal(ids.has(row.id), false, `${row.id} is unique`);
ids.add(row.id);
assert.ok(getTransitFeed(row.id), `${row.id} is routable`);
assert.ok(
publicTransitCatalog().some((entry) => entry.id === row.id),
`${row.id} is offered to the browser`,
);
assert.ok(['CC0 1.0', 'CC BY 4.0'].includes(row.license));
assert.match(
row.licenseUrl,
/^https:\/\/mobilitydatabase\.org\/feeds\/gtfs_rt\//,
`${row.id} links its catalogue record`,
);
assert.match(
row.terms.quote,
row.license === 'CC0 1.0'
? /creativecommons\.org\/publicdomain\/zero\/1\.0/
: /creativecommons\.org\/licenses\/by\/4\.0/,
`${row.id} quotes the licence it names`,
);
assert.match(
row.terms.note,
/Vehicle positions were seen on|its positions have not been seen yet/,
`${row.id} says whether positions were seen`,
);
assert.equal(
row.liveSeen === false,
/have not been seen yet/.test(row.terms.note),
`${row.id} is flagged exactly when its positions are unseen`,
);
assert.equal(new URL(row.url).protocol, 'https:');
assert.ok(
// Japan, Hokkaidō to Okinawa.
row.center.lat > 24 &&
row.center.lat < 46 &&
row.center.lon > 122 &&
row.center.lon < 146,
`${row.id} is centred in Japan`,
);
assert.ok(
transitFeedsInRange(row.center.lat, row.center.lon).some(
(entry) => entry.id === row.id,
),
`${row.id} covers its own centre`,
);
}
});
test('every Japanese feed is documented with its licence', () => {
const lines = readFileSync(
new URL('../../DATA_SOURCES.md', import.meta.url),
'utf8',
).split('\n');
for (const row of JAPAN_TRANSIT_FEED_ROWS) {
const line = lines.find((l) => l.startsWith(`| **${row.name}** `));
assert.ok(line, `${row.name} has a DATA_SOURCES row`);
assert.ok(line.includes(row.license) && line.includes(row.licenseUrl));
}
});
File diff suppressed because it is too large. Load diff
+8 -1
View File
@@ -3953,5 +3953,12 @@
"Confirm: remove every added entry from Data Layers": "تأكيد: إزالة كل الإدخالات المُضافة من طبقات البيانات",
"Remove every added entry from Data Layers": "إزالة كل الإدخالات المُضافة من طبقات البيانات",
"Showing {shown} of {total}": "عرض {shown} من {total}",
"BROWSE DATA LIBRARY": "تصفّح مكتبة البيانات"
"BROWSE DATA LIBRARY": "تصفّح مكتبة البيانات",
"The globe stopped drawing": "توقفت الكرة الأرضية عن الرسم",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "استعاد المتصفح رسومات الكرة الأرضية ثلاثية الأبعاد. لا تزال الطبقات وعناصر التحكم تعمل، لكن الخريطة لن تُرسم حتى تُعاد تحميل الصفحة.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "توقف العرض بعد خطأ. لا تزال الطبقات وعناصر التحكم تعمل، لكن الخريطة لا يتم تحديثها.",
"Reset view reloads the app without the view restored from the address bar.": "تعيد «إعادة ضبط العرض» تحميل التطبيق دون العرض المستعاد من شريط العنوان.",
"Reload": "إعادة التحميل",
"Retry": "إعادة المحاولة",
"Reset view": "إعادة ضبط العرض"
}
+8 -1
View File
@@ -3605,5 +3605,12 @@
"Confirm: remove every added entry from Data Layers": "Bestätigen: alle hinzugefügten Einträge aus Datenebenen entfernen",
"Remove every added entry from Data Layers": "Alle hinzugefügten Einträge aus Datenebenen entfernen",
"Showing {shown} of {total}": "{shown} von {total} angezeigt",
"BROWSE DATA LIBRARY": "DATENBIBLIOTHEK DURCHSUCHEN"
"BROWSE DATA LIBRARY": "DATENBIBLIOTHEK DURCHSUCHEN",
"The globe stopped drawing": "Der Globus zeichnet nicht mehr",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Der Browser hat dem Globus die 3D-Grafik entzogen. Ebenen und Bedienelemente funktionieren weiter, aber die Karte kann erst nach dem Neuladen der Seite wieder zeichnen.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Das Rendern wurde nach einem Fehler angehalten. Ebenen und Bedienelemente funktionieren weiter, aber die Karte wird nicht aktualisiert.",
"Reset view reloads the app without the view restored from the address bar.": "Ansicht zurücksetzen lädt die App ohne die aus der Adressleiste wiederhergestellte Ansicht neu.",
"Reload": "Neu laden",
"Retry": "Erneut versuchen",
"Reset view": "Ansicht zurücksetzen"
}
+8 -1
View File
@@ -3692,5 +3692,12 @@
"500–1,000 m": "500–1000 m",
"1,000–2,000 m": "1000–2000 m",
"2,000–3,000 m": "2000–3000 m",
"3,000 m and above": "3000 m o más"
"3,000 m and above": "3000 m o más",
"The globe stopped drawing": "El globo dejó de dibujarse",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "El navegador retiró los gráficos 3D del globo. Las capas y los controles siguen funcionando, pero el mapa no se dibujará hasta que se recargue la página.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "El renderizado se detuvo tras un error. Las capas y los controles siguen funcionando, pero el mapa no se actualiza.",
"Reset view reloads the app without the view restored from the address bar.": "Restablecer la vista recarga la app sin la vista restaurada desde la barra de direcciones.",
"Reload": "Recargar",
"Retry": "Reintentar",
"Reset view": "Restablecer la vista"
}
+8 -1
View File
@@ -3692,5 +3692,12 @@
"Confirm: remove every added entry from Data Layers": "Confirmer : retirer du panneau Couches toutes les entrées ajoutées",
"Remove every added entry from Data Layers": "Retirer du panneau Couches toutes les entrées ajoutées",
"Showing {shown} of {total}": "Affichées : {shown} sur {total}",
"BROWSE DATA LIBRARY": "PARCOURIR LA BIBLIOTHÈQUE DE DONNÉES"
"BROWSE DATA LIBRARY": "PARCOURIR LA BIBLIOTHÈQUE DE DONNÉES",
"The globe stopped drawing": "Le globe ne s’affiche plus",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Le navigateur a retiré au globe ses graphismes 3D. Les calques et les commandes fonctionnent toujours, mais la carte ne pourra s’afficher qu’après le rechargement de la page.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Le rendu s’est arrêté après une erreur. Les calques et les commandes fonctionnent toujours, mais la carte ne se met plus à jour.",
"Reset view reloads the app without the view restored from the address bar.": "Réinitialiser la vue recharge l’app sans la vue restaurée depuis la barre d’adresse.",
"Reload": "Recharger",
"Retry": "Réessayer",
"Reset view": "Réinitialiser la vue"
}
+8 -1
View File
@@ -3692,5 +3692,12 @@
"Confirm: remove every added entry from Data Layers": "אישור: הסרת כל הפריטים שנוספו משכבות נתונים",
"Remove every added entry from Data Layers": "הסרת כל הפריטים שנוספו משכבות נתונים",
"Showing {shown} of {total}": "מוצגים {shown} מתוך {total}",
"BROWSE DATA LIBRARY": "עיון בספריית הנתונים"
"BROWSE DATA LIBRARY": "עיון בספריית הנתונים",
"The globe stopped drawing": "הגלובוס הפסיק להצטייר",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "הדפדפן לקח בחזרה את הגרפיקה התלת-ממדית של הגלובוס. השכבות והפקדים עדיין פועלים, אבל המפה לא תוכל להצטייר עד שהעמוד ייטען מחדש.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "הרינדור נעצר בעקבות שגיאה. השכבות והפקדים עדיין פועלים, אבל המפה אינה מתעדכנת.",
"Reset view reloads the app without the view restored from the address bar.": "\"איפוס תצוגה\" טוען מחדש את האפליקציה בלי התצוגה ששוחזרה משורת הכתובת.",
"Reload": "טען מחדש",
"Retry": "נסה שוב",
"Reset view": "איפוס תצוגה"
}
+8 -1
View File
@@ -3692,5 +3692,12 @@
"Confirm: remove every added entry from Data Layers": "Conferma: rimuovi da Livelli dati tutte le voci aggiunte",
"Remove every added entry from Data Layers": "Rimuovi da Livelli dati tutte le voci aggiunte",
"Showing {shown} of {total}": "Mostrati: {shown} su {total}",
"BROWSE DATA LIBRARY": "ESPLORA LIBRERIA DATI"
"BROWSE DATA LIBRARY": "ESPLORA LIBRERIA DATI",
"The globe stopped drawing": "Il globo ha smesso di disegnare",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Il browser ha ritirato la grafica 3D del globo. Livelli e controlli funzionano ancora, ma la mappa non può essere disegnata finché la pagina non viene ricaricata.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Il rendering si è fermato dopo un errore. Livelli e controlli funzionano ancora, ma la mappa non si aggiorna.",
"Reset view reloads the app without the view restored from the address bar.": "Ripristina vista ricarica l’app senza la vista ripristinata dalla barra degli indirizzi.",
"Reload": "Ricarica",
"Retry": "Riprova",
"Reset view": "Ripristina vista"
}
+8 -1
View File
@@ -3518,5 +3518,12 @@
"Confirm: remove every added entry from Data Layers": "確認:追加したすべての項目をデータレイヤーから削除",
"Remove every added entry from Data Layers": "追加したすべての項目をデータレイヤーから削除",
"Showing {shown} of {total}": "{total}件中{shown}件を表示",
"BROWSE DATA LIBRARY": "データライブラリを見る"
"BROWSE DATA LIBRARY": "データライブラリを見る",
"The globe stopped drawing": "地球儀の描画が止まりました",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "ブラウザーが地球儀の 3D グラフィックスを解放しました。レイヤーと操作は引き続き使えますが、ページを再読み込みするまで地図は描画されません。",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "エラーのため描画が停止しました。レイヤーと操作は引き続き使えますが、地図は更新されません。",
"Reset view reloads the app without the view restored from the address bar.": "「ビューをリセット」は、アドレスバーから復元したビューを使わずにアプリを再読み込みします。",
"Reload": "再読み込み",
"Retry": "再試行",
"Reset view": "ビューをリセット"
}
+8 -1
View File
@@ -3692,5 +3692,12 @@
"Confirm: remove every added entry from Data Layers": "Confirmar: remover de \"Camadas de dados\" todas as entradas adicionadas",
"Remove every added entry from Data Layers": "Remover de \"Camadas de dados\" todas as entradas adicionadas",
"Showing {shown} of {total}": "Mostrando {shown} de {total}",
"BROWSE DATA LIBRARY": "EXPLORAR BIBLIOTECA DE DADOS"
"BROWSE DATA LIBRARY": "EXPLORAR BIBLIOTECA DE DADOS",
"The globe stopped drawing": "O globo parou de desenhar",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "O navegador retirou os gráficos 3D do globo. As camadas e os controles continuam funcionando, mas o mapa só volta a ser desenhado depois de recarregar a página.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "A renderização parou após um erro. As camadas e os controles continuam funcionando, mas o mapa não está sendo atualizado.",
"Reset view reloads the app without the view restored from the address bar.": "Redefinir vista recarrega o app sem a vista restaurada da barra de endereços.",
"Reload": "Recarregar",
"Retry": "Tentar novamente",
"Reset view": "Redefinir vista"
}
+8 -1
View File
@@ -3779,5 +3779,12 @@
"500–1,000 m": "500–1000 м",
"1,000–2,000 m": "1000–2000 м",
"2,000–3,000 m": "2000–3000 м",
"3,000 m and above": "3000 м и выше"
"3,000 m and above": "3000 м и выше",
"The globe stopped drawing": "Глобус перестал отрисовываться",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Браузер отключил 3D-графику глобуса. Слои и элементы управления работают, но карта не отрисуется, пока страница не будет перезагружена.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Отрисовка остановилась из-за ошибки. Слои и элементы управления работают, но карта не обновляется.",
"Reset view reloads the app without the view restored from the address bar.": "«Сбросить вид» перезагружает приложение без вида, восстановленного из адресной строки.",
"Reload": "Перезагрузить",
"Retry": "Повторить",
"Reset view": "Сбросить вид"
}
+8 -1
View File
@@ -3605,5 +3605,12 @@
"Confirm: remove every added entry from Data Layers": "Bekräfta: ta bort alla tillagda poster från Datalager",
"Remove every added entry from Data Layers": "Ta bort alla tillagda poster från Datalager",
"Showing {shown} of {total}": "Visar {shown} av {total}",
"BROWSE DATA LIBRARY": "BLÄDDRA I DATABIBLIOTEKET"
"BROWSE DATA LIBRARY": "BLÄDDRA I DATABIBLIOTEKET",
"The globe stopped drawing": "Globen ritas inte längre",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Webbläsaren tog tillbaka globens 3D-grafik. Lager och reglage fungerar fortfarande, men kartan kan inte ritas förrän sidan laddas om.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Renderingen stoppades efter ett fel. Lager och reglage fungerar fortfarande, men kartan uppdateras inte.",
"Reset view reloads the app without the view restored from the address bar.": "”Återställ vy” laddar om appen utan vyn som återställdes från adressfältet.",
"Reload": "Ladda om",
"Retry": "Försök igen",
"Reset view": "Återställ vy"
}
+8 -1
View File
@@ -3779,5 +3779,12 @@
"Confirm: remove every added entry from Data Layers": "Підтвердити: видалити всі додані записи з «Шарів даних»",
"Remove every added entry from Data Layers": "Видалити всі додані записи з «Шарів даних»",
"Showing {shown} of {total}": "Показано {shown} з {total}",
"BROWSE DATA LIBRARY": "ВІДКРИТИ БІБЛІОТЕКУ ДАНИХ"
"BROWSE DATA LIBRARY": "ВІДКРИТИ БІБЛІОТЕКУ ДАНИХ",
"The globe stopped drawing": "Глобус перестав відмальовуватися",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Браузер вимкнув 3D-графіку глобуса. Шари та елементи керування працюють, але карта не відмалюється, доки сторінку не буде перезавантажено.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Відмальовування зупинилося через помилку. Шари та елементи керування працюють, але карта не оновлюється.",
"Reset view reloads the app without the view restored from the address bar.": "«Скинути вигляд» перезавантажує застосунок без вигляду, відновленого з адресного рядка.",
"Reload": "Перезавантажити",
"Retry": "Повторити",
"Reset view": "Скинути вигляд"
}
+8 -1
View File
@@ -3518,5 +3518,12 @@
"Confirm: remove every added entry from Data Layers": "确认:从数据图层中移除所有已添加条目",
"Remove every added entry from Data Layers": "从数据图层中移除所有已添加条目",
"Showing {shown} of {total}": "显示 {shown} / {total}",
"BROWSE DATA LIBRARY": "浏览数据资源库"
"BROWSE DATA LIBRARY": "浏览数据资源库",
"The globe stopped drawing": "地球已停止绘制",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "浏览器收回了地球的 3D 图形。图层和控件仍可使用,但在重新加载页面之前地图无法绘制。",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "渲染因错误而停止。图层和控件仍可使用,但地图不再更新。",
"Reset view reloads the app without the view restored from the address bar.": "“重置视图”会重新加载应用,不使用从地址栏恢复的视图。",
"Reload": "重新加载",
"Retry": "重试",
"Reset view": "重置视图"
}
+8 -1
View File
@@ -3518,5 +3518,12 @@
"Confirm: remove every added entry from Data Layers": "確認:從數據圖層中移除所有已添加條目",
"Remove every added entry from Data Layers": "從數據圖層中移除所有已添加條目",
"Showing {shown} of {total}": "顯示 {shown} / {total}",
"BROWSE DATA LIBRARY": "瀏覽數據資源庫"
"BROWSE DATA LIBRARY": "瀏覽數據資源庫",
"The globe stopped drawing": "地球已停止繪製",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "瀏覽器收回了地球的 3D 圖形。圖層和控件仍可使用,但在重新加載頁面之前地圖無法繪製。",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "渲染因錯誤而停止。圖層和控件仍可使用,但地圖不再更新。",
"Reset view reloads the app without the view restored from the address bar.": "「重置視圖」會重新加載應用,不使用從地址欄恢復的視圖。",
"Reload": "重新加載",
"Retry": "重試",
"Reset view": "重置視圖"
}
-167
View File
@@ -1,167 +0,0 @@
import * as Cesium from 'cesium';
import { TRANSIT_ENABLED_FEEDS } from '../../data/transitFeeds.js';
import { buildTransitCoveragePoints } from '../../data/transitCoverage.js';
import { COVERAGE_PEAK_ALPHA, coverageOpacityAt } from './policy.js';
/** Height of the spots above the ellipsoid, metres: on the surface. */
const SPOT_HEIGHT_M = 0;
/**
* A Gaussian falloff from the centre of each spot to exactly zero at its
* rim. No plateau and no edge, so overlapping spots sum into one smooth
* field instead of showing where each circle ends.
*/
const GLOW_SHADER = `
czm_material czm_getMaterial(czm_materialInput materialInput) {
czm_material material = czm_getDefaultMaterial(materialInput);
vec2 p = (materialInput.st - 0.5) * 2.0;
float sigma2 = 2.0 * 0.46 * 0.46;
float rim = exp(-1.0 / sigma2);
float glow = clamp((exp(-dot(p, p) / sigma2) - rim) / (1.0 - rim), 0.0, 1.0);
material.diffuse = color.rgb;
material.alpha = color.a * glow;
return material;
}`;
/**
* The coverage glow.
*
* Above the altitude gate the layer polls nothing and draws no vehicles, so
* a person looking at the whole Earth had no way to tell where Transit has
* anything to show. This draws one soft spot per feed seen live, built once
* from the registry: wide spots from orbit, cross-fading into patches the
* size of each feed's service area as a backdrop under the vehicles at
* continental and regional zoom, and off close in. Overlapping spots blend,
* so regions with many networks read hotter. It never polls a feed.
*
* Spots are flat discs drawn in the OPAQUE pass with ordinary alpha
* blending, not billboards and not draped imagery. Translucent billboards
* go through Cesium's order-independent transparency, whose weights clamp at
* orbital distances and turn a faint glow into a dark disc with a rim; and
* the photoreal tiles do not take a draped raster. A disc blended in the
* opaque pass is drawn over the tiles and the globe alike, under the vehicle
* markers, and is smooth at any strength.
*
* @param {object} context
* @returns {object}
*/
export function createCoverage({ state }) {
function batch(points, radiusKm, set) {
const [r, g, b] = points[0].rgb;
const material = new Cesium.Material({
translucent: false,
fabric: {
uniforms: { color: Cesium.Color.fromBytes(r, g, b, 0) },
source: GLOW_SHADER,
},
});
const vertexFormat =
Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat;
const primitive = new Cesium.Primitive({
geometryInstances: points.map(
(point) =>
new Cesium.GeometryInstance({
geometry: new Cesium.EllipseGeometry({
center: Cesium.Cartesian3.fromDegrees(point.lon, point.lat),
semiMajorAxis: radiusKm(point) * 1000,
semiMinorAxis: radiusKm(point) * 1000,
height: SPOT_HEIGHT_M,
vertexFormat,
}),
}),
),
appearance: new Cesium.MaterialAppearance({
material,
flat: true,
translucent: false,
materialSupport: Cesium.MaterialAppearance.MaterialSupport.TEXTURED,
renderState: {
// No depth test: at orbital distances the photoreal mesh is a few
// coarse facets that bulge kilometres off the ellipsoid, and a
// disc tested against it comes out speckled. Back-face culling
// does the hiding instead. A disc lies along the surface, so it
// faces away from the camera exactly when it is over the horizon.
depthTest: { enabled: false },
depthMask: false,
cull: { enabled: true, face: Cesium.CullFace.BACK },
blending: Cesium.BlendingState.ALPHA_BLEND,
},
}),
asynchronous: false,
allowPicking: false,
show: false,
});
state._viewer.scene.primitives.add(primitive);
// A regional patch is the network's real footprint, and neighbours there
// overlap far less than the wide orbit spots do, so it is not thinned.
const strength = set === 'near' ? 1 : points[0].strength;
return { primitive, material, strength, set };
}
function build() {
if (!state._viewer?.scene?.primitives) return null;
const buckets = new Map();
for (const point of buildTransitCoveragePoints(TRANSIT_ENABLED_FEEDS)) {
if (!buckets.has(point.bucket)) buckets.set(point.bucket, []);
buckets.get(point.bucket).push(point);
}
const batches = [];
for (const points of buckets.values()) {
batches.push(batch(points, (point) => point.farRadiusKm, 'far'));
batches.push(batch(points, (point) => point.nearRadiusKm, 'near'));
}
return batches;
}
/**
* Match the glow to the camera. Cheap enough for every camera event: the
* spots are built on the first call that needs them and never again, and a
* zoom only moves one colour uniform per batch.
* @param {number} altitude Camera height above the ellipsoid, metres.
*/
function sync(altitude) {
if (!state._enabled || !state._viewer) return;
const opacity = {
far: coverageOpacityAt(altitude, 'far'),
near: coverageOpacityAt(altitude, 'near'),
};
const key = `${opacity.far}/${opacity.near}`;
if (key === state._coverageOpacity) return;
state._coverageOpacity = key;
// Nothing built and nothing to show: do not build for a session that
// never leaves street level.
if (!state._coverageSpots) {
if (opacity.far === 0 && opacity.near === 0) return;
try {
state._coverageSpots = build();
} catch (error) {
// A host that cannot build geometry (headless tests) has no glow.
state._coverageSpots = [];
console.warn('[Data:Transit] Coverage glow unavailable:', error);
}
}
for (const entry of state._coverageSpots || []) {
const value = opacity[entry.set];
entry.primitive.show = value > 0;
entry.material.uniforms.color = Cesium.Color.clone(
entry.material.uniforms.color,
).withAlpha(value * COVERAGE_PEAK_ALPHA * entry.strength);
}
}
function hide() {
state._coverageOpacity = null;
for (const entry of state._coverageSpots || [])
entry.primitive.show = false;
}
function destroy() {
state._coverageOpacity = null;
for (const entry of state._coverageSpots || []) {
state._viewer?.scene?.primitives?.remove(entry.primitive);
}
state._coverageSpots = null;
}
return { sync, hide, destroy };
}
-2
View File
@@ -6,7 +6,6 @@ import { createRendering } from './rendering.js';
import { createSelection } from './selection.js';
import { createIngestion } from './ingestion.js';
import { createViewport } from './viewport.js';
import { createCoverage } from './coverage.js';
import { createQueries } from './queries.js';
import { createControls } from './controls.js';
import { createLifecycle } from './lifecycle.js';
@@ -50,7 +49,6 @@ export function createTransitLayer({
parts.selection = createSelection(context);
parts.rendering = createRendering(context);
parts.ingestion = createIngestion(context);
parts.coverage = createCoverage(context);
parts.viewport = createViewport(context);
parts.queries = createQueries(context);
parts.controls = createControls(context);
-3
View File
@@ -234,7 +234,6 @@ export function createLifecycle({ state, services, parts }) {
state._preRenderRemove = null;
}
parts.ingestion.abortAllInFlight();
parts.coverage.hide();
state._activeFeeds.clear();
state._feedStatus.clear();
for (const entry of state._vehicles.values()) {
@@ -268,7 +267,6 @@ export function createLifecycle({ state, services, parts }) {
async update() {
if (!state._enabled) return;
parts.ingestion.sweepAgedVehicles(Date.now());
parts.coverage.sync(parts.viewport.getCameraAltitude());
if (state._activeFeeds.size === 0) return;
const generation = state._generation;
await Promise.all(
@@ -284,7 +282,6 @@ export function createLifecycle({ state, services, parts }) {
*/
destroy(viewer) {
this.disable(viewer);
parts.coverage.destroy();
if (state._markers) {
unregisterSpriteCollection('transit', state._markers);
viewer?.scene?.primitives?.remove(state._markers);
-56
View File
@@ -31,62 +31,6 @@ export const TRANSIT_POLL_MS = 15_000;
*/
/** Camera altitude (m) above which the layer idles: a national fleet still reads at 3,000 km. */
export const ACTIVATION_ALTITUDE_M = 3_000_000;
/**
* How strongly the coverage glow shows at each camera height (m), as
* [height, opacity] stops joined by straight lines. There are two sets of
* spots and they cross-fade:
*
* - `far`: wide spots, far larger than any network, so coverage reads from
* orbit. Full above the altitude gate, gone by regional zoom.
* - `near`: patches the size of each feed's own service area, drawn as a
* backdrop under the vehicles at continental and regional zoom so a person
* can see which neighbouring cities are covered. Off close in, where a
* city's own vehicles are the picture.
*
* Between them there is no height from 250 km up with nothing drawn.
*/
export const COVERAGE_OPACITY_STOPS = Object.freeze({
far: Object.freeze([
Object.freeze([1_200_000, 0]),
Object.freeze([3_400_000, 1]),
]),
near: Object.freeze([
Object.freeze([60_000, 0]),
Object.freeze([250_000, 0.8]),
Object.freeze([1_500_000, 0.9]),
Object.freeze([3_400_000, 0]),
]),
});
/** Fine enough that a zoom reads as a continuous fade, not as steps. */
const COVERAGE_FADE_STEPS = 100;
/** Alpha at the centre of a lone spot at full strength. */
export const COVERAGE_PEAK_ALPHA = 0.9;
/**
* Opacity of one set of coverage spots at a camera height, quantized so a
* zoom does not restyle the glow on every camera event.
* @param {number} altitude Camera height above the ellipsoid, metres.
* @param {'far'|'near'} [set='far']
* @returns {number} 0..1
*/
export function coverageOpacityAt(altitude, set = 'far') {
const stops = COVERAGE_OPACITY_STOPS[set] || COVERAGE_OPACITY_STOPS.far;
const last = stops[stops.length - 1][1];
if (!Number.isFinite(altitude)) return altitude === Infinity ? last : 0;
let value = last;
if (altitude <= stops[0][0]) value = stops[0][1];
else {
for (let i = 1; i < stops.length; i++) {
if (altitude <= stops[i][0]) {
const [h0, v0] = stops[i - 1];
const [h1, v1] = stops[i];
value = v0 + ((v1 - v0) * (altitude - h0)) / (h1 - h0);
break;
}
}
}
return Math.round(value * COVERAGE_FADE_STEPS) / COVERAGE_FADE_STEPS;
}
export const ACTIVATION_ENTER_ALTITUDE_M = ACTIVATION_ALTITUDE_M - 150_000;
export const ACTIVATION_EXIT_ALTITUDE_M = ACTIVATION_ALTITUDE_M + 150_000;
/** Debounce (ms) for camera-change proximity checks. */
@@ -56,7 +56,6 @@ test('leaving a feed whose tick failed repaints its error away', async () => {
sweepAgedVehicles() {},
},
rendering: { requestVisibility() {}, syncRenderHold() {} },
coverage: { sync() {} },
};
parts.viewport = createViewport({ state, services, parts });
const queries = createQueries({ state, parts }).methods;
-3
View File
@@ -42,9 +42,6 @@ export function createState({ services }) {
state._cameraChangedAttached = false;
state._cameraDebounceTimer = null;
state._altitudeGateOpen = false;
/** Coverage glow: its draw batches and the last opacities drawn. */
state._coverageSpots = null;
state._coverageOpacity = null;
/** Camera sensitivity as we found it, so disable() can hand it back. */
state._priorPercentageChanged = null;
/** The value this layer actually wrote, for an identity-guarded restore. */
-3
View File
@@ -83,7 +83,6 @@ export function createViewport({ state, services, parts }) {
if (!state._enabled || !state._viewer) return;
refreshViewBounds();
const altitude = getCameraAltitude();
parts.coverage.sync(altitude);
const wasGateOpen = state._altitudeGateOpen;
state._altitudeGateOpen = altitudeGateOpen(altitude);
let changed = state._altitudeGateOpen !== wasGateOpen;
@@ -133,8 +132,6 @@ export function createViewport({ state, services, parts }) {
// A person dragging the globe should not wait a third of a second for the
// vehicles under the cursor to start moving again.
refreshViewBounds();
// The glow follows the zoom as it happens, not a third of a second later.
parts.coverage.sync(getCameraAltitude());
clearTimeout(state._cameraDebounceTimer);
state._cameraDebounceTimer = setTimeout(() => {
state._cameraDebounceTimer = null;
+205
View File
@@ -0,0 +1,205 @@
import { useStatusStore } from './stores/status.js';
import { applicationPinia } from './vueRuntime.js';
import { describeError } from '../app/errors.js';
import { appRootUrl } from '../share/shareLinkPath.js';
/**
* Show a recoverable notice when the globe stops drawing, instead of a black
* globe behind working controls. Retry restarts the render loop, or reloads
* after a lost WebGL context; Reset opens the app's root address. With
* `allowReload: false`, Reset is dropped, and so is Retry after a context loss.
* Returns a function that removes the notice.
*/
export function installRenderFailureNotice(
viewer,
{
store = useStatusStore(applicationPinia()),
locationRef = globalThis.location,
allowReload = true,
logError = (error) =>
console.error('[Render] The globe stopped drawing:', error),
} = {},
) {
const { scene } = viewer;
const { canvas } = viewer;
let notice = null;
let contextLost = false;
// With rethrowRenderErrors, Scene throws the error it has just raised;
// the render() wrapper must not raise it again.
let raisedByScene = null;
let frameFailed = false;
let removeFrameListeners = null;
function retry() {
if (contextLost) {
locationRef.reload();
return;
}
viewer.useDefaultRenderLoop = true;
scene.requestRender();
}
function reset() {
locationRef.assign(appRootUrl(locationRef.href));
}
function stopWatchingFrames() {
removeFrameListeners?.();
removeFrameListeners = null;
}
function clear() {
stopWatchingFrames();
if (store.renderFailure === notice) store.publishRenderFailure(null);
notice = null;
}
// Cesium raises postRender after a failed frame too, so only a frame that
// starts and ends without a render error clears the notice.
function watchFrames() {
if (removeFrameListeners || contextLost) return;
const removeStart = scene.preUpdate.addEventListener(() => {
frameFailed = false;
raisedByScene = null;
});
const removeEnd = scene.postRender.addEventListener(() => {
if (!frameFailed) clear();
});
removeFrameListeners = () => {
removeStart();
removeEnd();
};
}
function present(detail) {
const reason = contextLost ? 'context-lost' : 'render-error';
// A loop restarted into the same error keeps the notice as it was.
if (notice?.reason === reason && notice.detail === detail) return;
notice = {
reason,
detail,
retry: contextLost && !allowReload ? null : retry,
reset: allowReload ? reset : null,
};
store.publishRenderFailure(notice);
watchFrames();
}
const onRenderError = (_scene, error) => {
raisedByScene = { error };
frameFailed = true;
logError(error);
// A lost context fails every frame after it; the notice keeps saying why.
if (contextLost) return;
present(error ? describeError(error) : '');
};
const onContextLost = () => {
contextLost = true;
// No frame can clear this one; only a reload draws again.
stopWatchingFrames();
present('');
};
const removeRenderError = scene.renderError.addEventListener(onRenderError);
// An error thrown outside Scene's error handling (a frame listener, onTick,
// resize) stops the loop without a renderError. Raise one, so Cesium, an
// inline panel's timer and this notice all see it. First finish the event
// raises it cut short, so listeners added or removed meanwhile apply.
const unguard = ['resize', 'render'].map((name) =>
reportThrown(viewer.cesiumWidget, name, (error) => {
frameEvents(viewer).forEach(finishRaise);
if (!raisedByScene || raisedByScene.error !== error)
scene.renderError.raiseEvent(scene, error);
raisedByScene = null;
}),
);
canvas.addEventListener('webglcontextlost', onContextLost);
return () => {
removeRenderError();
unguard.forEach((restore) => restore());
canvas.removeEventListener('webglcontextlost', onContextLost);
clear();
};
}
/**
* Events Cesium raises in a frame outside Scene's error handling, which a
* listener's error can leave mid-raise. Only top-level tilesets are reached.
*/
function frameEvents(viewer) {
const { scene } = viewer;
const events = [
scene.preUpdate,
scene.postUpdate,
scene.preRender,
scene.postRender,
scene.camera?.changed,
scene.camera?.moveStart,
scene.camera?.moveEnd,
viewer.clock?.onTick,
viewer.clock?.onStop,
scene.globe?.tileLoadProgressEvent,
];
const primitives = scene.primitives;
for (let i = 0; i < (primitives?.length ?? 0); i += 1) {
const tileset = primitives.get(i);
events.push(
tileset?.loadProgress,
tileset?.allTilesLoaded,
tileset?.initialTilesLoaded,
);
}
return events;
}
/**
* Finish a raise a throwing listener cut short. Until a raise ends, Cesium's
* `Event` only queues added and removed listeners; apply that queue.
*/
function finishRaise(event) {
if (!event?._invokingListeners) return;
event._invokingListeners = false;
for (const [listener, scopes] of event._toAdd) {
const added = event._listeners.get(listener) ?? new Set();
scopes.forEach((scope) => added.add(scope));
event._listeners.set(listener, added);
}
event._toAdd.clear();
for (const [listener, scopes] of event._toRemove) {
const left = event._listeners.get(listener);
scopes.forEach((scope) => left?.delete(scope));
if (left?.size === 0) event._listeners.delete(listener);
}
event._toRemove.clear();
}
/**
* Report an error the widget's `name` method throws, and rethrow it.
*
* A render error marks the loop stopped, but a call made while it ran leaves
* its next frame queued, and that frame stops it. Keep the loop marked
* running, so a Retry or a shown tab before that frame starts no second loop.
* After a throw, the loop's own catch still marks it stopped.
*/
function reportThrown(widget, name, report) {
const original = widget?.[name];
if (typeof original !== 'function') return () => {};
const own = Object.hasOwn(widget, name);
const guarded = function (...args) {
const looping = widget._renderLoopRunning;
try {
return original.apply(this, args);
} catch (error) {
report(error);
throw error;
} finally {
if (looping) widget._renderLoopRunning = true;
}
};
widget[name] = guarded;
return () => {
if (widget[name] !== guarded) return;
if (own) widget[name] = original;
else delete widget[name];
};
}
+174
View File
@@ -0,0 +1,174 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { installRenderFailureNotice } from './renderFailure.js';
function fixture(t, options = {}) {
const canvasListeners = new Map();
const scene = {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
postRender: new Cesium.Event(),
requestRender() {
this.requests = (this.requests ?? 0) + 1;
},
};
const viewer = {
scene,
useDefaultRenderLoop: true,
canvas: {
addEventListener: (name, listener) => canvasListeners.set(name, listener),
removeEventListener: (name, listener) => {
if (canvasListeners.get(name) === listener)
canvasListeners.delete(name);
},
},
};
const store = {
renderFailure: null,
published: [],
publishRenderFailure(next) {
this.renderFailure = next;
this.published.push(next);
},
};
const visits = [];
const locationRef = {
href: 'https://gev.example/share/AQc?lang=ru#lat=48.8584&lon=2.2945',
reload: () => visits.push('reload'),
assign: (url) => visits.push(String(url)),
};
const logged = [];
const uninstall = installRenderFailureNotice(viewer, {
store,
locationRef,
logError: (error) => logged.push(error),
...options,
});
t.after(uninstall);
// One Cesium frame: preUpdate, an optional render error, then postRender,
// which Cesium raises after a failed frame too.
const frame = (error) => {
scene.preUpdate.raiseEvent(scene);
if (error) {
viewer.useDefaultRenderLoop = false;
scene.renderError.raiseEvent(scene, error);
}
scene.postRender.raiseEvent(scene);
};
return {
viewer,
scene,
store,
visits,
logged,
uninstall,
frame,
loseContext: () => canvasListeners.get('webglcontextlost')?.({}),
canvasListeners,
};
}
test('a render error shows a notice that survives the failed frame', (t) => {
const f = fixture(t);
assert.equal(f.store.renderFailure, null);
const error = new Error('Expected width to be greater than 0');
f.frame(error);
const notice = f.store.renderFailure;
assert.equal(notice.reason, 'render-error');
assert.equal(notice.detail, 'Expected width to be greater than 0');
assert.equal(typeof notice.retry, 'function');
assert.equal(typeof notice.reset, 'function');
assert.deepEqual(f.logged, [error]);
});
test('Retry restarts the render loop and a clean frame clears the notice', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
f.store.renderFailure.retry();
assert.equal(f.viewer.useDefaultRenderLoop, true);
assert.equal(f.scene.requests, 1);
assert.ok(f.store.renderFailure, 'the notice waits for a frame');
f.frame();
assert.equal(f.store.renderFailure, null);
// Clearing stops the per-frame listeners.
assert.equal(f.scene.preUpdate.numberOfListeners, 0);
assert.equal(f.scene.postRender.numberOfListeners, 0);
});
test('a loop restarted into the same error keeps one notice', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
const first = f.store.renderFailure;
f.store.renderFailure.retry();
f.frame(new Error('boom'));
assert.equal(f.store.renderFailure, first);
assert.equal(f.store.published.length, 1);
f.frame(new Error('other'));
assert.equal(f.store.renderFailure.detail, 'other');
assert.ok(!f.store.published.includes(null), 'no new appearance');
});
test('a new failure after recovery is a new appearance', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
f.store.renderFailure.retry();
f.frame();
f.frame(new Error('boom'));
assert.deepEqual(
f.store.published.map((notice) => notice?.reason ?? null),
['render-error', null, 'render-error'],
);
});
test('a render error without a value shows no detail', (t) => {
const f = fixture(t);
f.viewer.useDefaultRenderLoop = false;
f.scene.renderError.raiseEvent(f.scene, undefined);
assert.equal(f.store.renderFailure.reason, 'render-error');
assert.equal(f.store.renderFailure.detail, '');
});
test('Reset opens the app root without the restored view', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
f.store.renderFailure.reset();
assert.deepEqual(f.visits, ['https://gev.example/?lang=ru']);
});
test('a lost context offers a reload and no frame clears it', (t) => {
const f = fixture(t);
f.loseContext();
const notice = f.store.renderFailure;
assert.equal(notice.reason, 'context-lost');
assert.equal(notice.detail, '');
// Cesium's next frame then fails on the dead drawing buffer.
f.frame(new Error('Expected width to be greater than 0'));
assert.equal(f.store.renderFailure, notice, 'the notice keeps saying why');
f.frame();
assert.ok(f.store.renderFailure, 'a lost context never draws again');
f.store.renderFailure.retry();
assert.deepEqual(f.visits, ['reload']);
});
test('a page that must not reload offers neither reload nor reset', (t) => {
const f = fixture(t, { allowReload: false });
f.frame(new Error('boom'));
assert.equal(typeof f.store.renderFailure.retry, 'function');
assert.equal(f.store.renderFailure.reset, null);
f.loseContext();
assert.equal(f.store.renderFailure.retry, null);
assert.equal(f.store.renderFailure.reset, null);
});
test('uninstall removes the listeners and the notice', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
f.uninstall();
assert.equal(f.store.renderFailure, null);
assert.equal(f.scene.renderError.numberOfListeners, 0);
assert.equal(f.scene.postRender.numberOfListeners, 0);
assert.equal(f.canvasListeners.size, 0);
f.frame(new Error('later'));
assert.equal(f.store.renderFailure, null);
});
+207
View File
@@ -0,0 +1,207 @@
// Retry after a scene event listener throws. Cesium's Event.raiseEvent has no
// try/finally, so the event stays mid-raise and only queues listener changes.
// Drives Cesium's real Event and startRenderLoop.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { installRenderFailureNotice } from './renderFailure.js';
import { renderLoopFixture } from './renderLoopFixture.mjs';
function setup() {
// Scene.render as in Scene.js:4369: preUpdate always, preRender/postRender
// only when a frame is rendered (requestRenderMode idle skips them).
const scene = {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
preRender: new Cesium.Event(),
postRender: new Cesium.Event(),
requestRenderMode: true,
_renderRequested: false,
rendered: 0,
requestRender() {
this._renderRequested = true;
},
initializeFrame() {},
render(time) {
this.preUpdate.raiseEvent(this, time);
const shouldRender = !this.requestRenderMode || this._renderRequested;
if (!shouldRender) return;
this._renderRequested = false;
this.preRender.raiseEvent(this, time);
this.rendered += 1;
this.postRender.raiseEvent(this, time);
},
};
return { scene, ...renderLoopFixture(scene) };
}
for (const eventName of ['postRender', 'preUpdate']) {
test(`idle: a ${eventName} listener that threw once; a successful Retry clears the notice`, () => {
const { scene, viewer, store, runFrames, restore } = setup();
try {
let broken = true;
scene[eventName].addEventListener(() => {
if (broken) throw new TypeError('listener failed');
});
scene.requestRender();
viewer.useDefaultRenderLoop = true;
runFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
assert.equal(viewer.useDefaultRenderLoop, false);
broken = false; // the cause goes away
store.renderFailure.retry();
const before = scene.rendered;
runFrames(10); // idle: no camera move, no render request
assert.ok(scene.rendered > before, 'the globe drew again');
assert.equal(store.renderFailure, null, 'the notice is cleared');
} finally {
restore();
}
});
}
test('a throwing postRender listener removed before Retry is not called again', () => {
const { scene, viewer, store, runFrames, restore } = setup();
try {
let calls = 0;
const remove = scene.postRender.addEventListener(() => {
calls += 1;
throw new TypeError('layer listener failed');
});
scene.requestRender();
viewer.useDefaultRenderLoop = true;
runFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
remove(); // the user turns the layer off
store.renderFailure.retry();
runFrames(5);
assert.equal(calls, 1, 'the removed listener ran again after Retry');
assert.equal(viewer.useDefaultRenderLoop, true, 'the loop keeps running');
assert.equal(store.renderFailure, null, 'the notice is cleared');
} finally {
restore();
}
});
const listenerCount = (event) =>
[...event._listeners.values()].reduce((sum, scopes) => sum + scopes.size, 0);
// While the loop is stopped, a layer turned off and on again, or a ground
// task in worldAnnotationRenderer that starts, times out and starts again.
const RESUBSCRIBE = {
'turned off and on': (event, listener) => {
event.removeEventListener(listener);
event.addEventListener(listener);
},
'added, removed and added again': (event, listener) => {
event.addEventListener(listener);
event.removeEventListener(listener);
event.addEventListener(listener);
},
};
for (const [name, resubscribe] of Object.entries(RESUBSCRIBE)) {
test(`a listener ${name} while the loop is stopped runs after Retry`, () => {
const { scene, viewer, store, runFrames, restore } = setup();
try {
let broken = true;
scene.postRender.addEventListener(() => {
if (broken) throw new TypeError('layer listener failed');
});
let calls = 0;
const listener = () => {
calls += 1;
};
if (name === 'turned off and on')
scene.postRender.addEventListener(listener);
scene.requestRender();
viewer.useDefaultRenderLoop = true;
runFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
resubscribe(scene.postRender, listener);
broken = false;
store.renderFailure.retry();
calls = 0;
scene.requestRender();
runFrames(3);
assert.ok(calls > 0, 'the re-added listener runs after Retry');
assert.equal(
scene.postRender.numberOfListeners,
listenerCount(scene.postRender),
);
} finally {
restore();
}
});
}
// Every event a listener's error can cut short is finished when the error
// surfaces, so a listener added at once is subscribed.
const FRAME_EVENTS = {
'scene.preUpdate': (v) => v.scene.preUpdate,
'scene.postUpdate': (v) => v.scene.postUpdate,
'scene.preRender': (v) => v.scene.preRender,
'scene.postRender': (v) => v.scene.postRender,
'camera.changed': (v) => v.scene.camera.changed,
'camera.moveStart': (v) => v.scene.camera.moveStart,
'camera.moveEnd': (v) => v.scene.camera.moveEnd,
'clock.onTick': (v) => v.clock.onTick,
'clock.onStop': (v) => v.clock.onStop,
'globe.tileLoadProgressEvent': (v) => v.scene.globe.tileLoadProgressEvent,
'tileset.loadProgress': (v) => v.scene.primitives.get(0).loadProgress,
'tileset.allTilesLoaded': (v) => v.scene.primitives.get(0).allTilesLoaded,
'tileset.initialTilesLoaded': (v) =>
v.scene.primitives.get(0).initialTilesLoaded,
};
for (const [name, pick] of Object.entries(FRAME_EVENTS)) {
test(`a listener that throws in ${name} leaves it finished`, () => {
const events = () => ({
changed: new Cesium.Event(),
moveStart: new Cesium.Event(),
moveEnd: new Cesium.Event(),
});
const tileset = {
loadProgress: new Cesium.Event(),
allTilesLoaded: new Cesium.Event(),
initialTilesLoaded: new Cesium.Event(),
};
const viewer = {
scene: {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
postUpdate: new Cesium.Event(),
preRender: new Cesium.Event(),
postRender: new Cesium.Event(),
camera: events(),
globe: { tileLoadProgressEvent: new Cesium.Event() },
primitives: { length: 1, get: () => tileset },
},
clock: { onTick: new Cesium.Event(), onStop: new Cesium.Event() },
canvas: { addEventListener() {}, removeEventListener() {} },
cesiumWidget: {
_renderLoopRunning: false,
render: () => pick(viewer).raiseEvent(),
},
};
const store = { publishRenderFailure() {} };
const uninstall = installRenderFailureNotice(viewer, {
store,
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
logError() {},
});
try {
const event = pick(viewer);
event.addEventListener(() => {
throw new TypeError('listener failed');
});
assert.throws(() => viewer.cesiumWidget.render(), TypeError);
assert.equal(event._invokingListeners, false);
const listener = () => {};
event.addEventListener(listener);
assert.ok(event._listeners.has(listener));
} finally {
uninstall();
}
});
}
+46
View File
@@ -0,0 +1,46 @@
// Errors thrown in the render loop outside Scene's tryAndCatchError. Drives
// Cesium's real startRenderLoop through the useDefaultRenderLoop setter.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { renderLoopFixture } from './renderLoopFixture.mjs';
function runLoop(render) {
const scene = {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
postRender: new Cesium.Event(),
requestRender() {},
initializeFrame() {},
render,
};
const fixture = renderLoopFixture(scene);
fixture.viewer.useDefaultRenderLoop = true;
fixture.runFrames(3);
return { ...fixture, uninstall: fixture.restore };
}
test('an error thrown outside tryAndCatchError still shows the notice', () => {
// Scene.render raises preRender outside tryAndCatchError (Scene.js:4439),
// so a listener's exception propagates.
const { widget, store, logged, uninstall } = runLoop(function () {
this.preUpdate.raiseEvent(this);
throw new TypeError("Cannot read properties of undefined (reading 'x')");
});
assert.equal(widget.useDefaultRenderLoop, false);
assert.equal(store.renderFailure?.reason, 'render-error');
assert.equal(logged.length, 1);
uninstall();
});
test('an error Scene raised and threw on with rethrowRenderErrors is reported once', () => {
const { store, logged, uninstall } = runLoop(function () {
this.preUpdate.raiseEvent(this);
const error = new Error('boom');
this.renderError.raiseEvent(this, error);
throw error;
});
assert.equal(store.renderFailure?.detail, 'boom');
assert.equal(logged.length, 1);
uninstall();
});
+95
View File
@@ -0,0 +1,95 @@
// Retry after an error Scene catches itself: the loop is marked stopped but
// its next frame is still queued, so an early Retry must not start a second
// loop. Drives Cesium's real startRenderLoop and useDefaultRenderLoop setter.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { renderLoopFixture } from './renderLoopFixture.mjs';
function setup({ targetFrameRate } = {}) {
const scene = {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
preRender: new Cesium.Event(),
postRender: new Cesium.Event(),
broken: false,
drawn: 0,
requestRender() {},
initializeFrame() {},
// Scene.render (Scene.js:4369): an update error is caught by
// tryAndCatchError, which raises renderError and returns normally.
render(time) {
this.drawn += 1;
this.preUpdate.raiseEvent(this, time);
if (this.broken) this.renderError.raiseEvent(this, new Error('boom'));
this.preRender.raiseEvent(this, time);
this.postRender.raiseEvent(this, time);
},
};
return { scene, ...renderLoopFixture(scene, { targetFrameRate }) };
}
for (const targetFrameRate of [undefined, 60]) {
test(`targetFrameRate ${targetFrameRate}: Retry before the next frame after an error Scene caught keeps one render loop`, () => {
const { frames, scene, viewer, store, runFrames, restore } = setup({
targetFrameRate,
});
try {
viewer.useDefaultRenderLoop = true;
runFrames(2);
scene.broken = true;
runFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
// The cause is gone; Retry runs (a queued Enter on the focused Retry)
// before the loop's next animation frame.
scene.broken = false;
store.renderFailure.retry();
assert.equal(frames.length, 1, 'one animation frame queued after Retry');
scene.drawn = 0;
runFrames(3);
assert.equal(store.renderFailure, null);
assert.equal(scene.drawn, 3, 'the globe draws once a frame');
} finally {
restore();
}
});
}
test('control: Retry after the next frame ran keeps one render loop', () => {
const { frames, scene, viewer, store, runFrames, restore } = setup();
try {
viewer.useDefaultRenderLoop = true;
runFrames(2);
scene.broken = true;
runFrames(1);
scene.broken = false;
runFrames(1);
assert.equal(frames.length, 0, 'the stopped loop queued nothing');
store.renderFailure.retry();
scene.drawn = 0;
runFrames(3);
assert.equal(scene.drawn, 3);
} finally {
restore();
}
});
test('a tab hidden and shown again before the next frame after an error Scene caught keeps one render loop', () => {
const { scene, viewer, runFrames, restore } = setup();
try {
viewer.useDefaultRenderLoop = true;
runFrames(2);
scene.broken = true;
runFrames(1);
scene.broken = false;
// src/app/tools.js turns the loop off while the tab is hidden and back on
// when it is shown, before the queued frame runs.
viewer.useDefaultRenderLoop = false;
viewer.useDefaultRenderLoop = true;
scene.drawn = 0;
runFrames(3);
assert.equal(scene.drawn, 3, 'the globe draws once a frame');
} finally {
restore();
}
});
+173
View File
@@ -0,0 +1,173 @@
// The scene chrome renders the render failure notice and its actions.
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createApp, nextTick } from 'vue';
import { createPinia } from 'pinia';
import { useStatusStore } from './stores/status.js';
import SceneChromeView from './views/SceneChromeView.vue';
function mount(t) {
document.body.replaceChildren();
const pinia = createPinia();
const app = createApp(SceneChromeView).use(pinia);
app.mount(document.body.appendChild(document.createElement('div')));
t.after(() => app.unmount());
return useStatusStore(pinia);
}
const notice = () => document.getElementById('render-failure');
const buttons = () =>
[...(notice()?.querySelectorAll('button') ?? [])].map((button) =>
button.textContent.trim(),
);
test('a render failure shows an alert dialog that moves focus to Retry', async (t) => {
const store = mount(t);
assert.equal(notice(), null, 'no notice while the globe draws');
const calls = [];
store.publishRenderFailure({
reason: 'render-error',
detail: 'Expected width to be greater than 0',
retry: () => calls.push('retry'),
reset: () => calls.push('reset'),
});
await nextTick();
await nextTick();
const element = notice();
assert.equal(element.getAttribute('role'), 'alertdialog');
assert.equal(
document.getElementById(element.getAttribute('aria-labelledby'))
.textContent,
'The globe stopped drawing',
);
assert.match(
document.getElementById(element.getAttribute('aria-describedby'))
.textContent,
/^Rendering stopped after an error\./,
);
assert.equal(
element.querySelector('.render-failure-detail').textContent,
'Expected width to be greater than 0',
);
assert.deepEqual(buttons(), ['Retry', 'Reset view']);
assert.equal(document.activeElement?.textContent.trim(), 'Retry');
for (const button of element.querySelectorAll('button')) button.click();
assert.deepEqual(calls, ['retry', 'reset']);
store.publishRenderFailure(null);
await nextTick();
assert.equal(notice(), null);
});
test('a lost context offers Reload, and a panel that cannot reload offers nothing', async (t) => {
const store = mount(t);
store.publishRenderFailure({
reason: 'context-lost',
detail: '',
retry: () => {},
reset: () => {},
});
await nextTick();
assert.match(
document.getElementById('render-failure-message').textContent,
/3D graphics/,
);
assert.equal(notice().querySelector('.render-failure-detail'), null);
assert.deepEqual(buttons(), ['Reload', 'Reset view']);
store.publishRenderFailure({
reason: 'context-lost',
detail: '',
retry: null,
reset: null,
});
await nextTick();
assert.deepEqual(buttons(), []);
assert.equal(notice().querySelector('.render-failure-hint'), null);
});
const failure = () => ({
reason: 'render-error',
detail: '',
retry: () => {},
reset: null,
});
const settle = async () => {
await nextTick();
await nextTick();
};
test('focus goes back where it was when the globe draws again', async (t) => {
const store = mount(t);
const search = document.body.appendChild(document.createElement('input'));
search.focus();
store.publishRenderFailure(failure());
await settle();
assert.equal(document.activeElement?.textContent.trim(), 'Retry');
store.publishRenderFailure(null);
await settle();
assert.equal(document.activeElement, search);
});
// The globe's canvas, as CesiumWidget builds it inside the container.
const addGlobeCanvas = () => {
const widget = document.createElement('div');
widget.className = 'cesium-widget';
document.getElementById('cesiumContainer').appendChild(widget);
return widget.appendChild(document.createElement('canvas'));
};
test('focus goes to the globe canvas when the element it came from is gone', async (t) => {
const store = mount(t);
const canvas = addGlobeCanvas();
const search = document.body.appendChild(document.createElement('input'));
search.focus();
store.publishRenderFailure(failure());
await settle();
search.remove();
store.publishRenderFailure(null);
await settle();
// The map's keys, Enter to finish a drawing among them, read the canvas
// as the map (src/annotations/drawTool.js).
assert.equal(document.activeElement, canvas);
assert.equal(canvas.getAttribute('tabindex'), '-1');
canvas.blur();
assert.equal(canvas.hasAttribute('tabindex'), false, 'only this once');
});
test('focus goes to the globe canvas when the element it came from cannot take it', async (t) => {
const store = mount(t);
const canvas = addGlobeCanvas();
const button = document.body.appendChild(document.createElement('button'));
button.focus();
store.publishRenderFailure(failure());
await settle();
button.disabled = true;
store.publishRenderFailure(null);
await settle();
assert.equal(document.activeElement, canvas);
// A press on the globe leaves focus on the page again, as Cesium has it.
canvas.dispatchEvent(new Event('pointerdown'));
assert.equal(document.activeElement, document.body);
assert.equal(canvas.hasAttribute('tabindex'), false);
});
test('focus the user moved while the notice was up stays put', async (t) => {
const store = mount(t);
store.publishRenderFailure(failure());
await settle();
const chat = document.body.appendChild(document.createElement('textarea'));
chat.focus();
store.publishRenderFailure(null);
await settle();
assert.equal(document.activeElement, chat);
});
test('the globe drawing from the start leaves focus alone', async (t) => {
mount(t);
await settle();
assert.equal(document.activeElement, document.body);
assert.equal(
document.getElementById('cesiumContainer').hasAttribute('tabindex'),
false,
);
});
+72
View File
@@ -0,0 +1,72 @@
// A widget on Cesium's real CesiumWidget.prototype (startRenderLoop, the
// useDefaultRenderLoop setter, render) around a scene double, with animation
// frames queued by hand and the render failure notice installed.
import * as Cesium from 'cesium';
import { installRenderFailureNotice } from './renderFailure.js';
export function renderLoopFixture(
scene,
{ targetFrameRate, viewer: viewerExtras = {}, notice = {} } = {},
) {
const frames = [];
const previousRaf = globalThis.requestAnimationFrame;
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
const widget = Object.create(Cesium.CesiumWidget.prototype);
Object.assign(widget, {
_scene: scene,
_clock: { tick() {} },
_canRender: true,
_useDefaultRenderLoop: false,
_renderLoopRunning: false,
// What src/app/scene.js passes to the viewer.
_showRenderLoopErrors: false,
_targetFrameRate: targetFrameRate,
isDestroyed: () => false,
resize() {},
});
// CesiumWidget's own renderError handler, which marks the loop stopped.
scene.renderError.addEventListener(() => {
widget._useDefaultRenderLoop = false;
widget._renderLoopRunning = false;
});
const viewer = Object.assign(
{
scene,
cesiumWidget: widget,
canvas: { addEventListener() {}, removeEventListener() {} },
get useDefaultRenderLoop() {
return widget.useDefaultRenderLoop;
},
set useDefaultRenderLoop(value) {
widget.useDefaultRenderLoop = value;
},
},
viewerExtras,
);
const store = {
renderFailure: null,
publishRenderFailure(next) {
this.renderFailure = next;
},
};
const logged = [];
const uninstall = installRenderFailureNotice(viewer, {
store,
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
logError: (error) => logged.push(error),
...notice,
});
let time = 0;
const runFrames = (n) => {
for (let i = 0; i < n; i += 1) {
// Above a 60 fps target's interval, so every frame draws.
time += 17;
for (const fn of frames.splice(0)) fn(time);
}
};
const restore = () => {
uninstall();
globalThis.requestAnimationFrame = previousRaf;
};
return { frames, widget, viewer, store, logged, runFrames, restore };
}
+17 -1
View File
@@ -7,7 +7,9 @@ import { markRaw, shallowRef } from 'vue';
* road network chip, CctvControls the camera grid chip; the toast shows
* what any owner's createToast() (src/ui/toast.js) publishes, and the title
* bar the FPS readout createFrameRateMonitor() publishes. Each label keeps its
* last text while hidden, so the next change flaps from what was shown.
* last text while hidden, so the next change flaps from what was shown. The
* render failure notice is what installRenderFailureNotice()
* (src/ui/renderFailure.js) publishes while the globe cannot draw.
*/
export const useStatusStore = defineStore('gev-status', () => {
/**
@@ -32,6 +34,13 @@ export const useStatusStore = defineStore('gev-status', () => {
*/
const fps = shallowRef(null);
/**
* The globe's render failure, `{reason, detail, retry, reset}`; null while
* the globe draws. `reason` is 'render-error' or 'context-lost', and `retry`
* and `reset` are null where that action is not offered.
*/
const renderFailure = shallowRef(null);
/** @param {?object} next A fresh presentation of the loading status. */
function publishLoading(next) {
loading.value = next && markRaw(next);
@@ -57,6 +66,11 @@ export const useStatusStore = defineStore('gev-status', () => {
fps.value = next && markRaw(next);
}
/** @param {?object} next A fresh presentation of the render failure. */
function publishRenderFailure(next) {
renderFailure.value = next && markRaw(next);
}
return {
fps,
publishFps,
@@ -68,5 +82,7 @@ export const useStatusStore = defineStore('gev-status', () => {
publishTraffic,
cctv,
publishCctv,
renderFailure,
publishRenderFailure,
};
});
+70
View File
@@ -405,6 +405,76 @@ html:not([lang='en']) #global-loading-detail {
}
}
/* ── Render failure notice ───────────────────── */
/* Above the loading screen, so a globe that fails while it loads says so too,
and above floating panels such as the Québec camera viewer (2000). */
#render-failure {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2001;
box-sizing: border-box;
width: min(420px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow-y: auto;
padding: 20px 22px;
background: var(--glass-bg);
border: 1px solid #ff7272;
border-radius: 12px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 0 24px rgba(255, 88, 88, 0.18);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 13px;
line-height: 1.5;
}
#render-failure-title {
margin: 0 0 8px;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
color: #ff7272;
}
#render-failure p {
margin: 0 0 10px;
}
#render-failure .render-failure-detail {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-secondary);
overflow-wrap: anywhere;
}
#render-failure .render-failure-hint {
font-size: 12px;
color: var(--text-secondary);
}
#render-failure .render-failure-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
#render-failure .gev-action-button {
font-size: 11px;
padding: 8px 14px;
}
@media (pointer: coarse) {
#render-failure .gev-action-button {
min-height: 44px;
}
}
/* ── Toast Notification ──────────────────────── */
#toast {
position: fixed;
+2
View File
@@ -18,6 +18,7 @@ import ScreenAnnotations from './panels/ScreenAnnotations.vue';
import AwarenessDirection from './panels/AwarenessDirection.vue';
import UiButton from '../kit/UiButton.vue';
import GlobeActions from './panels/GlobeActions.vue';
import RenderFailure from './panels/RenderFailure.vue';
import WorldTimeline from './panels/WorldTimeline.vue';
// The ACTIVE STYLE corner shows the style VisualSettings publishes.
@@ -61,6 +62,7 @@ const vToastText = {
<template>
<div id="cesiumContainer"></div>
<RenderFailure />
<div id="world-overlay-root" aria-hidden="true">
<canvas id="world-overlay-canvas"></canvas>
</div>
+87
View File
@@ -0,0 +1,87 @@
<script setup>
import { nextTick, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { msg, t } from '../../../i18n/index.js';
import { useStatusStore } from '../../stores/status.js';
import UiButton from '../../kit/UiButton.vue';
// Published by installRenderFailureNotice() (src/ui/renderFailure.js).
const { renderFailure: failure } = storeToRefs(useStatusStore());
const panel = ref(null);
const MESSAGES = {
'context-lost': msg(
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.",
),
'render-error': msg(
'Rendering stopped after an error. Layers and controls still work, but the map is not updating.',
),
};
// Focus goes to the first action when the notice appears. When the globe
// draws again, it goes back, or to the map, unless the user moved it.
let focusBefore;
watch(
() => Boolean(failure.value),
async (shown) => {
if (shown) {
focusBefore = document.activeElement;
await nextTick();
panel.value?.querySelector('button')?.focus();
return;
}
if (focusBefore === undefined) return;
const previous = focusBefore;
focusBefore = undefined;
await nextTick();
const active = document.activeElement;
if (active && active !== document.body) return;
if (previous?.isConnected && previous !== document.body) {
previous.focus({ preventScroll: true });
// A control disabled, hidden or made inert meanwhile does not take it.
if (document.activeElement === previous) return;
}
focusMap();
},
{ immediate: true },
);
// The map's keys (Enter to finish a drawing, Space to talk) read the canvas
// as the map. It stays focusable only until focus leaves or the globe is
// pressed; a press leaves focus on the page, as in Cesium.
function focusMap() {
const canvas = document.querySelector(
'#cesiumContainer .cesium-widget canvas',
);
if (!canvas) return;
if (!canvas.hasAttribute('tabindex')) {
canvas.tabIndex = -1;
const done = new AbortController();
const release = () => {
done.abort();
canvas.removeAttribute('tabindex');
};
canvas.addEventListener('blur', release, { signal: done.signal });
canvas.addEventListener(
'pointerdown',
() => {
release();
canvas.blur();
},
{ capture: true, signal: done.signal },
);
}
canvas.focus({ preventScroll: true });
}
</script>
<template>
<div v-if="failure" id="render-failure" ref="panel" role="alertdialog" aria-labelledby="render-failure-title" aria-describedby="render-failure-message">
<h2 id="render-failure-title">{{ t('The globe stopped drawing') }}</h2>
<p id="render-failure-message">{{ t(MESSAGES[failure.reason] ?? MESSAGES['render-error']) }}</p>
<p v-if="failure.detail" class="render-failure-detail" translate="no">{{ failure.detail }}</p>
<p v-if="failure.reset" class="render-failure-hint">{{ t('Reset view reloads the app without the view restored from the address bar.') }}</p>
<div v-if="failure.retry || failure.reset" class="render-failure-actions">
<UiButton v-if="failure.retry" class="gev-action-button render-failure-retry" :onClick="failure.retry">{{ failure.reason === 'context-lost' ? t('Reload') : t('Retry') }}</UiButton>
<UiButton v-if="failure.reset" class="gev-action-button render-failure-reset" :onClick="failure.reset">{{ t('Reset view') }}</UiButton>
</div>
</div>
</template>