← Back to Posts

Generating over 9000 journeys for the Manchester Trams app

February 5, 2024

I haven't touched the Manchester Trams app in a while and its been running smoothly, however I left one feature in a buggy state and this week I returned to finish what I started.

That feature was/is the Journey Planner feature and it works by taking two stations on the Metrolink and showing the user how to get from one to the other. The originally code for this feature used a graph data structure and connected stations to each other, this worked for some station combinations but not others, particularly stations on different lines.

This time around I realised my original method of connecting stations was flawed as I was only connecting stations in one direction (i.e St Peters Square -> Piccadilly). This meant that if a user wanted to go from Piccadilly to St Peters Square, it would fail or return the opposite results. So with that in mind, I opted to connect stations by their platforms instead, allowing for bi-directional connections (edges in graph-data-land).

By doing this, a station such as Village would now have two platforms, the first platform named Village-1 would connect to Parkway-1 and would be on the Trafford Centre line, whilst Village-2 would be connected to the Imperial War Museum-1 platform on the "Deansgate-Castlefield" line.

This solution showed potential with a few initial tests of two stations on the same line, but it did have it had interesting quirks that I had to write some extra code for. The first quirk came from figuring out which platform the user meant when selecting stations as I didn't want users to have to enter "Parkway-1" to "Village-2". The quirk happened when the system automatically picked platform 1 which would result in the following

Parkway -> Barton Dock Road -> Trafford Centre -> Barton Dock Road -> Parkway -> Village

The system, in order to get a user from Parkway to Village, would tell the user to go to the end of the current line and then come back on themselves until they reached their destination. To fix this, I wrote a function that would "brute force" every combination of platforms from both Station A and B and then figure out the quickest route based on lowest amount of connecting systems. In code, that kinda looks like this

const combinations = [
    [`${start}-1`, `${end}-1`],
    [`${start}-1`, `${end}-2`],
    [`${start}-2`, `${end}-1`],
    [`${start}-2`, `${end}-2`],
];

const results = await Promise.all(
	combinations.map(async combination => {
	    const result = await findShortestPath(graph, combination[0], combination[1]);
        return { result, combination };
    })
);

...

This took a lot of trial and error to get over but I eventually got it working.

Stations on different lines

The second quirk in the system came from getting directions from Station A on Line A to Station D on Line B. The code I wrote was great for figuring out points on a single Line, but once the query involved finding stations on two different lines, the code would error pretty much every time.

To fix this, I made an array of crossover stations (these are stations that have 2 or more lines running through them, i.e Piccadilly) and then called a function called xLineFunction for each of them using a map.

Within this xLineFunction, the code does the following:

  • Find the mutual Line for Station A to Crossover Station
  • Calculate the shortest route for Station A to Crossover Station
  • Build an array of stations in order for Station A to Crossover Station

The same is then done for Crossover Station to Station B in that order. The function returns an object with origin, destination, instructions and steps array. Finally, the code finds the combination of Station A - Crossover Station - Station B which has the lowest steps (stations) length.

For example, if I want to go from Village to Manchester Airport. The code will check the following combinations:

Village -> Pomona -> Manchester Airport (Not possible)
Village -> Cornbrook -> Manchester Airport (Possible, 20 steps)
Village -> Deansgate-Castlefield -> Manchester Airport (Possible, 21 steps)

The final result from this would be the middle result as it has the fewest steps. Once tested, this method seemed to be very reliable in returning accurate results for cross-line stations.

Saving Responses

It was about halfway through this project that I realised I could save each response to a query in a database so that I didn't have to run the expensive function above every time someone wanted to go from Piccadilly to Village for example.

With this great epiphany, I changed some of the code to that each query would save the response to Station-A.json and either create a new array or append to it in said JSON file. Each response looked like so:

{
    start: "Station A",
    end: "Station Z",
    line: "Trafford Centre",
    instructions: "Take the Trafford Centre tram from Station A to Station Z",
    steps: [
	    "Station A",
	    "Station C",
	    "Station E",
	    "Station Z"
	]
}

However, this kept running into issues so I eventually settled on saving each query to its own JSON file...

This resulted in nearly 10,000 JSON files...

However, it worked! Almost! There is still a bug somewhere in the system that means the instructions don't properly make sense and telling users to take null line. But for the vast majority of station combinations, this worked!

API & App Updates

Finally, there's some pending API and App updates which will implement the new journey finder endpoints. For the API, I plan to create two endpoints, one for returning a Journey (Station A to Station B) and one for returning popular Journeys for a Station.

Happy coding! 🏄‍♂️

Leave a comment!