Back to Blog

Building the Lanka Metro Transit Journey Planner: An Engineering Postmortem

When the new Lanka Metro Transit routes for Colombo were announced, the city was buzzing. We were getting a massive upgrade to our public transit network. But a familiar problem immediately surfaced: there was no unified, digital tool to help commuters figure out how to navigate these new lines. I wanted a journey planner. I didn't want to download a heavy app, I didn't want to create an account, and I didn't want to wait for an official release that might take months. So, I decided to build one. This is the story of how an afternoon experiment turned into a robust, single-file HTML application, the constraints that forced me to get creative, the day the architecture completely broke, and the lessons learned building a client-side routing engine.

Achala Weerasooriya

Achala Weerasooriya

CEO & Head of Engineering

August 5, 2026
5 min read
5.0
Building the Lanka Metro Transit Journey Planner: An Engineering Postmortem

When the new Lanka Metro Transit routes for Colombo were announced, the city was buzzing. We were getting a massive upgrade to our public transit network. But a familiar problem immediately surfaced: there was no unified, digital tool to help commuters figure out how to navigate these new lines. I wanted a journey planner. I didn't want to download a heavy app, I didn't want to create an account, and I didn't want to wait for an official release that might take months. So, I decided to build one. This is the story of how an afternoon experiment turned into a robust, single-file HTML application, the constraints that forced me to get creative, the day the architecture completely broke, and the lessons learned building a client-side routing engine.

The Problem

Colombo's new transit routes form a web of intersecting lines. Commuters need to know exactly which bus to board, where to transfer, and how to bridge the gap between their front door and the nearest halt.

The Goal

To build a web-based Journey Planner that is:

  • Frictionless: No logins, no downloads.
  • Fast: Immediate routing results.
  • Resilient: Must work even on spotty mobile connections.

The Constraints

To keep hosting costs at exactly zero and deployment as simple as sharing a link, the entire application had to exist within a single .html file.

  • No backend server (Node.js/Python).
  • No spatial databases (PostGIS).
  • No complex build tools (Webpack/Vite).

Everything the UI, the routing logic, the geospatial math, and the data had to run in the client's browser.

Iteration 1: The Naive Approach

My first attempt was entirely brute force. I mapped out the bus halts into a local JSON object and drew straight lines between them on a Leaflet map.

Native approach

If a user wanted to go from Kottawa to Fort, the script just checked if both stops were on the same array. It worked for direct routes, but the moment someone needed to transfer, the logic fell apart. Furthermore, the map looked terrible. Buses don't fly; they follow curving roads.

Iteration 2: Chasing Perfection

To fix the visual issue, I integrated the Open Source Routing Machine (OSRM) API. Instead of straight lines, the app dynamically sent the coordinates of the bus halts to OSRM, which returned beautiful, perfectly snapped GeoJSON road paths.

Real Road geometry

For the transfer problem, I realized I needed a graph structure.

The Routing Graph

Once every route became a graph, finding a journey stopped being a map problem and became a graph traversal problem.

Makumbura ->Kottawa -> Pannipitiya -> Thalawathugoda

By connecting these nodes, a modified Dijkstra's algorithm could find the shortest path from origin to destination.

Everything looked beautiful. The code was elegant. The map looked professional.

And then...

The Day OSRM Beat Me

I loaded the app into an online code sandbox to test it on a mobile view.

The map froze. The screen went white. Then, a massive red error filled the console: “Unhandled Promise Rejection: Error: Preview stopped: a loop ran longer than 400ms – check for an infinite loop.”

Because Sri Lanka has complex one-way road systems (like Galle Road vs. Duplication Road), the inbound and outbound buses take different physical streets. To map the network accurately, the browser was firing 14 simultaneous routing requests to the OSRM server (7 routes × 2 directions) inside a for loop.

The sandbox detected this massive spike in network requests as an infinite loop and aggressively killed the application. My beautiful routing engine never even got a chance to run.

Rethinking the Architecture

I had to face a harsh reality: relying on heavy, concurrent API calls on initial page load was a terrible idea for a zero-backend app. I needed a compromise.

Version 3: Precomputed Geometry

  • Fast ⚡
  • Offline-First 💾
  • Stable 🛡️

Instead of asking OSRM for directions on the fly, I pre-baked the complex coordinate arrays directly into the JavaScript file. For the most complex routes, the exact path was hardcoded. For simpler routes, I reverted to straight lines. It was a slight visual downgrade, but it guaranteed 100% stability and sub-second load times.

How the Final System Works

With the API nightmare solved, the architecture finally locked into place. Here are the core systems that make the journey planner tick.

1. Unified Search (Graceful Degradation)

Users rarely know the exact, official name of a bus halt. They search for landmarks. But what if they have a terrible 3G connection and the geocoding API times out?

Search Flow diagram

The search input checks a local, offline dictionary of halts first, immediately returning results tagged with a HALT badge. Simultaneously, it pings the OpenStreetMap Nominatim API for addresses. If the API fails, the user can still route themselves using the local data.

Search UI showing drop down with halts and addresses

2. The Transfer Graph (The "Walking" Illusion)

Real-world transfers rarely happen at the exact same GPS coordinate. You usually have to get off a bus and walk 50 meters down the street to a different platform.

To solve this, the application isn't looking for identical stops. It looks for stops within a specific radius.

Transfer graph diagram

I introduced a constant: var TRANSFER_M = 200;. During initialization, the app builds a graph of nearby Halts, linking any two nodes that are less than 200 meters apart. The routing algorithm treats these short walks as valid transfer points, perfectly mimicking real human transit behavior.

Journey result panel

3. The Resilient Bootloader

Single-file HTML apps are fragile. If the CDN hosting your map library goes down, your users just see a broken white page.

To prevent this, the app uses a custom bootloader that acts like a tiny dependency manager.

Resilient Bootloader diagram

The script maintains an array of three different Leaflet CDNs. It attempts to load the first one. If the onload event fails, it seamlessly tries the next one, updating the user interface with a friendly loading spinner.

Lessons Learned

  • The UI is just the tip of the iceberg. Drawing a map is easy. Calculating the logic of how human beings traverse a city is incredibly difficult.
  • APIs are a liability. Every time you call a third-party API from the frontend, you introduce a point of failure. Caching and pre-computing data is almost always the safer bet for resilient apps.

What I'd Do Differently in v2

Web Workers: Running a graph traversal algorithm on the main thread is fine for 100 nodes. If this expanded to thousands of nodes, it would cause UI stuttering. V2 would offload the routing engine to a Web Worker.

GTFS Integration: Right now, routing is based purely on distance. Integrating General Transit Feed Specification (GTFS) data would allow for time-based routing (e.g., "Take CM01 because the next bus arrives in 3 minutes").

Thoughts on AI Pair Programming

I built this with heavy assistance from an AI coding agent. But the reality of AI pair programming is very different from the hype.

AI wasn't writing the application for me. It was shortening the feedback loop.

When I encountered the OSRM failure, I didn't spend three hours reading documentation to figure out a fix. I bounced ideas off the agent. We tested an architectural pivot in minutes instead of hours. Most of its initial code suggestions were discarded. Some were refined. A few fundamentally changed the application.

The biggest productivity gain wasn't code generation it was rapid, fearless experimentation.

Final Architecture Summary

Here is how all the pieces finally fit together to get a commuter from their home to their office.

Final Architecture Diagram

Enjoyed this article?

Rate this article

5.0 / 5

Keep reading

More from the Blog

See All Articles
AI-Powered Vehicle Damage Detection Platform
5m

AI-Powered Vehicle Damage Detection Platform

The automotive and insurance industries have long struggled with a slow, subjective, and costly problem: assessing vehicle damage accurately. Manual inspections are time-consuming, prone to human error, and often inconsistent from one assessor to the next. Recognizing this gap, Liveroom Technologies partnered with an innovative automotive-tech company to help build an AI-based vehicle damage detection platform, a solution designed to bring speed, precision, and consistency to vehicle condition assessments.

Isini De Silva

Isini De Silva

Aug 25, 2026

From Manuals to Motion: An AR Training & Maintenance Assistant
5m

From Manuals to Motion: An AR Training & Maintenance Assistant

Every machine operator or maintenance technician who has ever paused mid-task to flip through a dog-eared manual knows the problem: the knowledge you need is rarely where you need it. It's buried in a PDF, a binder, or someone else's memory - anywhere but right in front of you, on the machine, in the moment.

Isini De Silva

Isini De Silva

Aug 25, 2026

Easy AR: Build Augmented Reality Apps in Unity Without Writing Code
5m

Easy AR: Build Augmented Reality Apps in Unity Without Writing Code

Easy AR by Render Island is a plug-and-play, no-code AR toolkit for Unity. Set up ARKit/ARCore projects in the inspector, drop in ready-made packs, and ship AR faster. If you've ever tried to start an AR project in Unity, you know the pattern. Before you get anywhere near the actual experience you set out to build, you're deep in AR Foundation setup configuring ARKit and ARCore, wiring up image tracking, plane detection, object spawning, session management. It's the same plumbing every single time, and it stands between you and the part that's actually interesting. We built Easy AR because we were tired of rebuilding that plumbing on every project.

Achala Weerasooriya

Achala Weerasooriya

Jul 21, 2026