Expo · TypeScript · Node.js · GTFS
A cross-platform real-time map for Chicago's CTA buses and 'L' trains. Live vehicle positions are polled from the CTA Bus and Train Tracker APIs, smoothly interpolated between updates, and rendered over official route geometries pulled from the GTFS feed.
// flow
// capabilities
Every active CTA bus and 'L' train appears on the map with its current position, refreshed on a 15-second cadence from the official trackers.
Buses use heading- and speed-aware dead-reckoning via Turf.js; trains fall back to linear interpolation. Either way, vehicles move instead of jumping.
Route polylines are drawn straight from the GTFS shapes feed in the actual CTA branding colors — Red, Blue, Brown, Green, Orange, Purple, Pink, Yellow.
An expandable overlay panel lets riders show or hide individual routes, with bulk select-all / clear-all controls for quickly focusing the map.
Independent visibility toggles for the bus layer, train layer, and route-line layer make it easy to declutter and see only what you care about.
One Expo codebase targets all three platforms. The same map, store, and API client run identically in the browser and on mobile.
// under the hood
The project is split into an Express backend that proxies and caches the CTA feeds, and an Expo frontend (React Native for Web + native) that renders the map. The split keeps CTA API keys and request budgets on the server side, out of the app bundle.
On the backend, ctaProxy.js
wraps the Bus and Train Tracker endpoints in a
node-cache layer so repeated
frontend requests collapse into a single upstream fetch every
15 seconds. gtfsLoader.js
and the generateGeoJSON.js
script bake the static GTFS feed into a compact route-shapes bundle.
On the frontend, global state lives in a Zustand store — vehicles, route shapes, and toggle state — and the map reads from it directly. A movement interpolation utility uses Turf.js to tween bus positions between polls based on heading and speed vectors.
// Cache CTA vehicle positions for 15s const cache = new NodeCache({ stdTTL: 15 }); export async function getVehicles(mode) { const key = `vehicles:${mode}`; const hit = cache.get(key); if (hit) return hit; const { data } = await axios.get( endpointFor(mode), { params: { key: process.env.CTA_KEY, format: 'json' } } ); const normalized = normalize(data, mode); cache.set(key, normalized); return normalized; }
// Dead-reckon a bus forward from its last sample export function project(v: Vehicle, dtSec: number) { const speedKmh = v.speed ?? 25; const distKm = (speedKmh / 3600) * dtSec; const from = turf.point([v.lon, v.lat]); const next = turf.destination(from, distKm, v.heading); const [lon, lat] = next.geometry.coordinates; return { ...v, lon, lat }; }
// stack
// lines
// roadmap
// source
The full source for both the Expo app and the Express backend is on GitHub — clone it, plug in a CTA API key, and you're tracking every bus and train in Chicago.