An interactive map that simulates Milan's public transport network (metro lines M1–M5, trams, trolleybuses and buses) in real time, using the service levels published in the City of Milan open data.
▶ Live demo · Time-travel mode · Versione italiana
ATM does not publish real-time vehicle positions as open data. What is public is a summary of every route: how many trips run in the peak, off-peak and evening bands, and when the first and last runs are. Milano Transit Simulator turns those numbers into a synthetic, deterministic timetable and animates every vehicle on a map, so you can see what the network looks like at 08:00 on a weekday, at 00:30 on a Saturday night, or right now.
Everything runs in the browser: there is no backend, no API key and no build step. The base map comes from OpenFreeMap, an open, key-free vector tile service built on OpenStreetMap. The whole app is a static site served by GitHub Pages.
⚠️ Positions are simulated, not live. This is an independent portfolio project and is not affiliated with ATM S.p.A.
- Real-time simulation — the simulation clock follows the current time in Milan (whatever your time zone), including the right timetable for weekdays, Saturdays, Sundays and Italian public holidays (Easter Monday and Milan's patron saint included).
- Branching lines — M1 (Rho Fieramilano / Bisceglie) and M2 (Gessate / Cologno Nord, Assago / Abbiategrasso) alternate trains between their branches.
- Surface network — 142 tram, trolleybus and bus lines and 4,690 stops, filterable by mode and searchable.
- Departure boards — click any station or stop for the next simulated departures, grouped by line and destination.
- Vehicle tracking — follow a train or bus: current segment, average speed, terminus arrival and upcoming stops.
- Time travel — with
?simulationin the URL you can pause, play at up to 60×, pick a day type and jump to preset scenarios (morning rush, after midnight…). - Bilingual UI — Italian and English, switchable at runtime (
?lang=it/?lang=en). - Three visual styles — Noorda light and dark, inspired by Bob Noorda's 1964 wayfinding for the Milan metro, and Fiord, a control-room look. Automatic follows the device's light/dark setting.
- Built for phones — bottom tab bar, draggable bottom sheets with snap points, 44 px touch targets and safe-area support; two-panel layout on tablets.
- Accessible — WCAG AA contrast for text and line badges (checked by a unit test), keyboard navigation, visible focus and reduced motion.
graph LR
subgraph pipeline [Offline data pipeline · Python]
A[Comune di Milano<br/>open data CSV] --> C(build_metro_frequencies.py)
A --> D(build_surface_network.py)
B[OpenStreetMap<br/>Overpass API] --> E(build_metro_network.py)
end
C --> F[(js/data/*.js)]
D --> F
E --> F
subgraph browser [Browser]
F --> G(TransitScheduler<br/>synthetic timetable)
G --> H(TransitSimulation<br/>clock + vehicle positions)
H --> I(TransitMap<br/>Leaflet + MapLibre GL)
H --> J(app.js<br/>panels, boards, i18n)
end
The scheduler turns service levels into stop-by-stop trips, the simulation computes where every vehicle is at a given second, and the UI renders the map at 60 fps while refreshing panels four times per second. See ARCHITECTURE.md for the details.
- Frontend: vanilla JavaScript (no framework, no bundler), HTML, CSS
- Maps: Leaflet for the network layers, MapLibre GL JS (via maplibre-gl-leaflet) for the OpenFreeMap vector base map, with an OpenStreetMap raster fallback when WebGL is unavailable
- Data pipeline: Python 3 (standard library only)
- Testing: Node.js built-in test runner (
node:test) and Pythonunittest - CI/CD: GitHub Actions (tests on every push, automatic deploy to GitHub Pages)
- Architecture — modules, data flow, rendering loop, state
- Technical design — datasets, timetable synthesis, service-day model, assumptions and limitations
- Roadmap — GTFS timetables and the Flutter app for web, Android and iOS
- Architecture decision records — why Flutter, why GTFS, the future of this JS version
- Development setup — toolchain per phase (Ubuntu, no Mac required)
No installation is needed. Clone the repository and open index.html, or serve the folder:
git clone https://github.com/DeveloperMatt02/milano-transit-simulator.git
cd milano-transit-simulator
python3 -m http.server 8000 # or: npm startThen open http://localhost:8000. An internet connection is required for the map tiles, the map libraries and the web fonts.
| Parameter | Effect |
|---|---|
?simulation |
Shows the time controls: real-time toggle, play/pause, speed (1×–60×), day type and timeline |
?lang=it / ?lang=en |
Forces the interface language (otherwise the browser language is used and the choice is remembered) |
npm test # JavaScript: data integrity, scheduler, simulation, i18n
python3 -m unittest discover -s tests/python -v # Python: data pipeline helpers
npm run bench # engine micro-benchmark (informational, not a test)The generated files in js/data/ are committed, so this is only needed to refresh the data.
python3 scripts/download_open_data.py # -> data/raw/ (git-ignored)
python3 scripts/build_metro_network.py # -> js/data/metro-network.js
python3 scripts/build_metro_frequencies.py # -> js/data/metro-frequencies.js
python3 scripts/build_surface_network.py # -> js/data/surface-network.jsThe GTFS datasets (real timetables, used by the upcoming Flutter app) are not committed: CI rebuilds them twice a week. To build them locally:
python3 scripts/download_open_data.py --gtfs # -> data/raw/gtfs.zip (+ the CSVs used for names)
python3 scripts/build_gtfs.py # -> data/gtfs/*.json, 14 days from today (+ the previous service day)milano-transit-simulator/
├── index.html # Single page: map, header, side panels, timeline
├── css/style.css # Design system, light/dark themes, responsive layout
├── js/
│ ├── utils.js # Geo maths, service-day time, holidays, escaping
│ ├── i18n.js # Italian / English dictionaries
│ ├── audio.js # Synthesised UI sounds (Web Audio API)
│ ├── themes.js # Noorda light/dark and Fiord: base map style, line colours
│ ├── sheet.js # Draggable bottom sheets for phones
│ ├── scheduler.js # Service levels -> stop-by-stop timetable
│ ├── simulation-engine.js # Clock, vehicle positions, departure boards
│ ├── map-engine.js # Base map, network layers and markers
│ ├── app.js # UI controller
│ └── data/ # Generated datasets (metro network, frequencies, surface network)
├── scripts/ # Python data pipeline
├── tests/
│ ├── js/ # node:test suites
│ └── python/ # unittest suites
├── docs/ # Architecture, technical design, screenshots
└── .github/workflows/ # CI and GitHub Pages deployment
| Noorda (light) | Fiord |
|---|---|
![]() |
![]() |
- Timetable summaries, surface stops and routes: Comune di Milano – Open Data, licensed under CC BY 4.0 (timetable INV2024-2025).
- GTFS timetables: Comune di Milano – Open Data, dataset ds929, generated by AMAT from ATM's planned service, CC BY.
- Metro station coordinates: © OpenStreetMap contributors, ODbL.
- Basemap: OpenFreeMap © OpenMapTiles, data © OpenStreetMap contributors.
The simulation reproduces service levels, not the real timetable: trains are evenly spaced within each time band, running times are estimated from the distance between stations, and there are no delays, disruptions or short-turn services. Surface routes are drawn as straight lines between stops. See Technical design § Assumptions.
Developed by Matteo Trossi — @DeveloperMatt02
The source code is released under the MIT License — see LICENSE. Datasets remain subject to their original licenses (CC BY 4.0 and ODbL).





