Ride comfortable, every time.
A mobile-first Singapore bus app with real-time arrivals, full route exploration, private bus services, and a unique sun seat recommendation — so you always know which side of the bus to sit on to stay in the shade.
Live at → shiokbus.web.app
ShiokBus.Demo.mp4
- Live arrival times for all LTA public bus services with passenger load indicators (seats available / standing / limited) and bus type icons (single deck, double deck, bendy)
- Per-stop arrival rows within route view — expand any stop while browsing a route to see live arrivals without leaving the page
- 2-hour rain forecast badge on stop cards, sourced from data.gov.sg, showing current weather conditions near each stop
Coverage beyond LTA's public network — fully integrated alongside public buses with the same UI:
| Network | Type | Services |
|---|---|---|
| PBS (Private Bus Services) | Scheduled timetable | 550, 555, 560, 565, 593, 722, 728, 736, 740, 742, 753, 762, 767, 776, LCS2 and more |
| Sentosa Island Buses | Frequency-based | Bus A (Beach Station loop), Bus B (Sentosa Cove loop), Bus D (on-demand Fri/Sat/Sun evenings) |
Scheduled services show synthesised arrival times computed from timetable data. Frequency-based services (Sentosa buses) compute upcoming arrivals from headway and per-stop travel offsets, including buses already mid-route.
The unique proposition of this project. Two modes:
Live Arrivals View — for the next arriving bus, calculates which side of the bus faces the sun and recommends left or right. Takes into account bus type (upper deck of a double-decker gets a separate recommendation).
Plan Trip (route sun analysis) — select a boarding and alighting stop on the route map. The app scores every road segment along your journey by perpendicular_sun_component × altitude_factor × segment_distance, then recommends the consistently shadier side for the full trip. The route map is colour-coded from shade (blue) through mild to strong sun (red/orange). A live sun marker and direction ray are drawn on the map.
For night rides or when the sun is below 3° altitude the app returns "any seat is fine."
- Full stop-by-stop route view with expandable per-stop arrivals, first/last bus timings, and service frequency formatted by time-of-day
- Direction tabs for services with two directions; loop services show their midpoint via label
- Dual-loop services (e.g. 291, 293, 358, 359, 812) correctly labelled per visit — outbound and inbound legs distinguished at each stop
- Road-snapped route geometry via OSRM, colour-coded by sun exposure per segment
- Operator colour coding: SBS Transit (purple), SMRT (red), Tower Transit Singapore (green), Go-Ahead Singapore (gold), unknown private operators (navy)
- Unified search across stop names, road names, and stop codes
- Near Me — GPS-based discovery of the nearest bus stops with walking distance
- Popular Interchanges quick-access grid (12 major interchanges)
- MRT line pills on stop and route cards — visually flags MRT-adjacent stops with colour-coded line badges (NS, EW, NE, CC, DT, TE, JR, CR, LRT)
Full Singapore MRT/LRT network map overlay with pinch-to-zoom and scroll-to-zoom. Tap any station hotspot to instantly jump to that interchange's bus arrivals.
- Point-to-point routing via OneMap API with multiple itinerary options
- Each bus leg in the itinerary shows a sun seat pill — which side to sit on for that specific leg's geometry and current sun position
- Save frequently used origin–destination pairs as favourite plans
- Star stops, services, and planned routes — synced to your account via Firestore
- Drag-and-drop reordering within each favourites section (stops, services, plans), persisted per user
- Tap any favourite card to jump directly to that stop, route, or plan
- Live LTA train disruption alerts polled every minute
- Sticky banner at the top of the app when any MRT line is disrupted
- Inline alert cards shown on relevant stop cards (e.g. an NSL disruption shows on stops near NSL stations)
- Full alert modal with per-line breakdown and affected station details
- Toggle the banner on/off in Settings
- Light and dark themes, persisted to localStorage; map tiles switch automatically
- Configurable default landing tab (Stop or Service)
- Google Sign-In for cross-device favourites sync
- Toggle train disruption banner visibility
| Layer | Technology |
|---|---|
| Frontend | Vanilla HTML / CSS / JavaScript |
| Fonts | Barlow Condensed + Barlow (Google Fonts) |
| Maps | Leaflet.js + OneMap tiles + OSRM road geometry |
| Backend Proxy | Cloudflare Workers |
| Static Data Storage | Cloudflare R2 |
| Authentication | Firebase Auth (Google Sign-In) |
| Database | Firestore (per-user favourites & order) |
| Hosting | Cloudflare R2 |
| Public bus data | LTA DataMall API |
| Journey planning | OneMap Routing API |
| Rain forecast | data.gov.sg 2-hour forecast API |
| Road geometry | OSRM public routing API |
Browser (index.html served from Cloudflare R2)
│
├── Static data (stops, routes, services, MRT stations)
│ └──► Cloudflare Worker ──► Cloudflare R2
│ (nightly cron syncs full LTA dataset)
│
├── Real-time arrivals
│ └──► Cloudflare Worker ──► LTA DataMall BusArrival v3
│
├── Journey planning
│ └──► Cloudflare Worker ──► OneMap Routing API
│ (OneMap token cached in-worker, refreshed every ~3 days)
│
├── Train service alerts
│ └──► Cloudflare Worker ──► LTA DataMall TrainServiceAlerts
│
├── Rain forecast ──────────────────► data.gov.sg (direct from browser)
│
├── Road geometry ──────────────────► OSRM public API (direct from browser)
│
├── Private bus timetables ─────────► ./assets/ (pbs-services.json,
│ sentosa-services.json)
│
└── Auth + Favourites ──────────────► Firebase Auth + Firestore
- CORS origin check — only
shiokbus.web.appandshiokbus.firebaseapp.comare allowed; preflight OPTIONS requests are validated before any other processing - LTA API key — stored only in Worker environment variables, never exposed to the browser
| Mode | Endpoint param | Description |
|---|---|---|
| Static | static |
Streams a file directly from R2 (stops, services, routes, MRT stations) |
| Route | route |
Filters bus-routes.json by service number; result cached 5 hours in-worker |
| Arrivals | BusArrival |
Live pass-through to LTA DataMall BusArrival v3 |
| Journey | onemap |
Proxies OneMap routing API with cached auth token |
| Alerts | TrainServiceAlerts |
Live pass-through to LTA DataMall train alerts |
Runs nightly across four staggered cron triggers (02:00–02:45 SGT). Each trigger fetches up to 30 pages (500 records each) and stores progress in R2, allowing the full LTA dataset to be accumulated across multiple runs without hitting Worker CPU limits.
| Cron (UTC) | SGT | Dataset |
|---|---|---|
0 18 * * * |
02:00 | bus-routes.json |
15 18 * * * |
02:15 | bus-routes.json (continuation if needed) |
30 18 * * * |
02:30 | bus-services.json |
45 18 * * * |
02:45 | stops.json |
Private services (PBS, Sentosa) are loaded from local JSON assets at startup and injected into the same ALL_SERVICES map as LTA data. A PRIVATE_STOP_INDEX is built at inject time mapping each bus stop code to all private service stop entries that call there — enabling O(1) lookup when expanding a stop in the route view, regardless of how many services share that stop.
Two arrival synthesis modes:
- Scheduled (
DepartureTimespresent) — per-stopTimingsobjects map each departure to its arrival time at that stop; upcoming arrivals within a ±2–45 minute window are shown - Frequency-based (
DepartureTimesempty) — headway andStopOffsetMinsper stop compute the next three arrivals from the current time and operating hours. The synthesiser walks back enough intervals to include buses already en route, so mid-route stops correctly show the approaching bus rather than only the next departure from origin
Computed entirely client-side in two modes:
Live arrivals (single bus):
- Take the GPS coordinates of the next arriving bus and the one behind it
- Derive the bus's current bearing
- Compute the sun's azimuth for Singapore at the current time using the USNO solar position algorithm
- Calculate which side of the bus the sun is on; recommend the opposite side
Plan Trip (full journey):
- Fetch road-snapped geometry for the journey segment via OSRM
- For each road segment compute bearing, length, and the sun's perpendicular component relative to the bus
- Score each segment:
perpendicular_component × sin(altitude) × distance_metres - Sum scores per side; recommend the side with the lower cumulative score
- Colour-code the route polyline from blue (full shade) through amber to red (direct sun)
- Draw a live sun marker and bearing ray on the map
For night rides or sun altitude below 3°, the recommendation is suppressed.
shiokbus/
├── index.html # HTML structure and entry point
├── styles.css # All application styles
├── worker.js # Cloudflare Worker (proxy + cron R2 sync)
└── assets/
├── pbs-services.json # PBS timetable data (hand-curated)
├── sentosa-services.json # Sentosa bus service data
├── loop-midpoints.json # Loop service via-stop overrides
├── loop-desc-clear.json # Services to suppress loop labels for
├── dual-loops.json # Dual-loop service metadata (291, 293, 358, 359, 812)
├── leaflet.css
├── mrt-map.png # MRT network map image
└── shiokbus-logo-dark.png
└── scripts/
├── app.js # Main application logic (~4500 lines)
├── firebase.js # Firebase auth + Firestore (ES module)
└── leaflet.js # Leaflet (self-hosted)
- Serve from a local HTTP server:
python3 -m http.server 8000
- Add
http://localhost:8000to theALLOWED_ORIGINSarray inworker.js - No build step required
| Variable | Description |
|---|---|
LTA_KEY |
LTA DataMall API key |
ONEMAP_EMAIL |
OneMap account email (for token auth) |
ONEMAP_PASSWORD |
OneMap account password (for token auth) |
| Binding | Type | Description |
|---|---|---|
BUS_BUCKET |
R2 Bucket | Stores static bus data JSON files and sync state |
Four triggers are required — see the R2 data sync table above.
- Create a Firebase project and enable Google Sign-In under Authentication
- Enable Firestore — favourites stored at
users/{uid}/favourites/{key}, order atusers/{uid}/favOrder/{type} - Add your deployment domain to Firebase Authentication → Authorised Domains
| Data | Source |
|---|---|
| Public bus stops, services, routes | LTA DataMall |
| Real-time bus arrivals | LTA DataMall BusArrival v3 |
| Train service alerts | LTA DataMall TrainServiceAlerts |
| Journey planning | OneMap Routing API |
| Road-snapped geometry | OSRM |
| Rain forecast | data.gov.sg 2-hour forecast |
| PBS timetables | City Bus, MyBus, Diamond Coach, Ren Quan operator PDFs (hand-curated) |
| Sentosa bus schedules | Sentosa |
- LTA DataMall for Singapore public bus data
- Leaflet.js for maps
- OSRM for road-snapped routing geometry
- OneMap for map tiles and journey planning
- Barlow font by Jeremy Tribby
- LTAIdentity font by JinGen Lim
Built with ❤ for Singapore commuters who are tired of sitting in the sun.