sqlalchemy + flask + leaflet api/web app with realtime mbta data, including subway, commuter rail (keolis) and buses.tracks live locations, routes and alerts of all mbta lines.
- use html templates for 404/index
- add api ref
- add debugging raw js back
- rate limit API
- remove
explode_df - use bundles for custom js
- add light on dark and dark on light
- do the google transit things for predictions with svgs (maybe)
- compute bearings client side (maybe)
- figure out way to compute vehicle positions either client or server-side
- add option to view past stoptimes/trips for each paine (includes adding an options drawer)
- include shuttles trips within routes
- WIP: departure board
requires python 3.12+ and node 18+
-
clone the repo
git clone https://github.com/tandyx/mbtamapper.git cd mbtamapper -
install python requirements in a virtual environment
python3 -m venv .venv source .venv/bin/activate # windows: . .venv/Scripts/Activate.ps1 pip3 install --upgrade -r requirements.txt
-
install frontend dependencies
npm install
debug
python3 app.py -iproduction
# see ./deploy.sh
sudo .venv/bin/python3 -m waitress --listen=*:80 --threads=50 --call app:create_main_app &
waitcalling app.py with no arguments triggers a build process if there's no geojson data and the database doesn't exist. the -i (--import-data) flag forces a rebuild.
every night at 3am est, the database rebuilds. at 3:30am est, map layers are updated (this is the process that takes a while).
check out /.github/workflows
python: black, pylint, isortjavascript: prettierhtml/css: prettier, webhint
- python:
/requirements.txt - javascript:
/package.json
this frontend uses vanilla javascript/css using esbuild, built around leaflet. until september 2025, it was using completely vanilla javascript with individually-loaded node-modules, but it's faster and less server-load to bundle all the javascript. the disadvantage is that it's harder to debug.
each vehicle-type-specific route has these query parameters:
navless|navbarless: if this parameter exists and is truthy, hide the navbar. iframes are already hidden. this is automatically done if this website is in an iframe.sideless|sidebarless: if this parameter exists and is truthy, don't auto-show the sidebar. this is automatically done if this website is in an iframe.
setting these to true: eg. https://mbtamapper.com/commuter_rail?navless=true&sideless=true, may be optimal for digital signage.
when a marker is clicked, the id of that object is set as the window hash. this enables sharing of a specific location or returning to one after the page has been dormaint.
leaflet-realtime is used to query the database at a certain interval and automatically refresh the layer. however, because the popup content, etc also has to be refreshed, the codebase became a messy disaster. thus, each realtime layer is wrapped within an encompassing class that limits how many functions I put into the global scope.
this website operates on a nightly rebuild and persistent scheduler that runs in the background to the flask app. (see feed_loader.py). more documentation to come
self.scheduler.add_job(
self.import_realtime, "interval", args=[Alert], minutes=1
)
self.scheduler.add_job(
self.import_realtime, "interval", args=[Vehicle], seconds=11
)
self.scheduler.add_job(
self.import_realtime, "interval", args=[Prediction], seconds=21
)
self.scheduler.add_job(self.geojson_exports, "cron", hour=4, minute=0)
self.scheduler.add_job(self.nightly_import, "cron", hour=3, minute=30)
self.scheduler.add_job(self.clear_caches, "cron", day="*/4", hour=3, minute=40)
self.scheduler.start()users could query the database directly (please don't abuse it)
/api/<orm>?{param}&{param}&... - query the orm name with parameters
include: comma separated list of relational fields to includegeojson={Any}: return data in geojson format (default: false); to switch to true, set to any value (e.g. geojson=1)kwargs: columns/on-load-attrs to filter by; supported:=,<,>,<=,>=,!=,=null,!=nullcache: seconds to cache (int, float untested but should work)
Note
2025-07-17: nulls are now culled from the feed
/{route_type}/{vehicles|stops|shapes|parking} - api used by each route (geojson format only)
this data is already filtered out based on route_type; see /route_keys.json.
-
/vehicles?include=...,...&cache=...: realtime vehicle datainclude: optional comma separated list of relational fields to includecache: seconds to cache (int, float untested but should work)
-
/{stops|shapes|parking}; doesn't take params and redirects to a static.geojsonfile
/api/stop?stop_id=place-NEC-2108&include=child_stops,routes
[
{
"child_stops": [
{
"location_type": "0",
"municipality": "Sharon",
"parent_station": "place-NEC-2108",
"platform_name": "Commuter Rail",
"stop_desc": "Sharon - Commuter Rail",
"stop_id": "NEC-2108",
"stop_lat": 42.124553,
"stop_lon": -71.184468,
"stop_name": "Sharon",
"stop_url": "https://www.mbta.com/stops/place-NEC-2108",
"vehicle_type": "2",
"wheelchair_boarding": "1",
"zone_id": "CR-zone-4"
},
{
"location_type": "0",
"municipality": "Sharon",
"parent_station": "place-NEC-2108",
"platform_code": "1",
"platform_name": "Track 1 (Outbound)",
"stop_desc": "Sharon - Commuter Rail - Track 1 (Outbound)",
"stop_id": "NEC-2108-01",
"stop_lat": 42.124553,
"stop_lon": -71.184468,
"stop_name": "Sharon",
"stop_url": "https://www.mbta.com/stops/place-NEC-2108",
"vehicle_type": "2",
"wheelchair_boarding": "1",
"zone_id": "CR-zone-4"
},
{
"location_type": "0",
"municipality": "Sharon",
"parent_station": "place-NEC-2108",
"platform_code": "2",
"platform_name": "Track 2 (Boston)",
"stop_desc": "Sharon - Commuter Rail - Track 2 (Boston)",
"stop_id": "NEC-2108-02",
"stop_lat": 42.124553,
"stop_lon": -71.184468,
"stop_name": "Sharon",
"stop_url": "https://www.mbta.com/stops/place-NEC-2108",
"vehicle_type": "2",
"wheelchair_boarding": "1",
"zone_id": "CR-zone-4"
}
],
"location_type": "1",
"municipality": "Sharon",
"routes": [
{
"agency_id": "1",
"line_id": "line-Providence",
"network_id": "commuter_rail",
"route_color": "80276C",
"route_desc": "Regional Rail",
"route_fare_class": "Commuter Rail",
"route_id": "CR-Providence",
"route_long_name": "Providence/Stoughton Line",
"route_name": "Providence / Stoughton Line",
"route_sort_order": 20013,
"route_text_color": "FFFFFF",
"route_type": "2",
"route_url": "https://www.mbta.com/schedules/CR-Providence"
}
],
"stop_address": "1 Upland Rd, Sharon, MA 02067",
"stop_id": "place-NEC-2108",
"stop_lat": 42.124553,
"stop_lon": -71.184468,
"stop_name": "Sharon",
"stop_url": "https://www.mbta.com/stops/place-NEC-2108",
"wheelchair_boarding": "1",
"zone_id": "CR-zone-4"
}
]