Building a train departure board using the OpenLDBWS

So in my first post of programming stuff, I am going to cover on how to integrate with National Rail’s OpenLDBWS to make a departure board.

What is the OpenLDBWS
The OpenLDBWS is the Live Departure Boards Web Service. It provides a web service (vs an API) that returns real-time train information from Darwin. Darwin is the official train information engine. Since 2014 the open-access licence came into effect, meaning that individuals and organisations are now free to develop websites, apps and other digital tools with the data.

Pre-Reqs
1. To gain access to the OpenLDBWS data you need to request an access token here
2. You’ll also need a way to develop, and host your project. e.g. a webhosting provider or use something like Heroku.

Let’s Begin.
As I am developing a departure board I am going to need to use the GetDepBoardWithDetails operation. The list of them is here.

I am using the WithDetails endpoint VS the standard getDepBoard, as I would also like to list out extra information such as calling points to display on my output.

Firstly, we need to build a handler to make the SOAP connection, my example is here:

<?php

class train
{
	// Params
	private $soap = NULL; // soap connection
	private $accessKey = NULL; // api key
	private $wsdl = 'http://lite.realtime.nationalrail.co.uk/OpenLDBWS/wsdl.aspx';
	
	
	//  PHP will automatically call this function when you create an object from a class
	function __construct($accessKey)
	{
		$this->accessKey = $accessKey; // Retreive accessKey passed when calls are made
		$soapOptions = array(); // Any options to pass to Soap connection here
		$this->soapClient = new SoapClient($this->wsdl,$soapOptions); // create new SoapClient connection
		
		$soapVar = new SoapVar(array("ns2:TokenValue"=>$this->accessKey),SOAP_ENC_OBJECT);
		$soapHeader = new SoapHeader("http://thalesgroup.com/RTTI/2010-11-01/ldb/commontypes","AccessToken",$soapVar);
		$this->soapClient->__setSoapHeaders($soapHeader);
	}
	
	// Make call for train data
	function GetDepBoardWithDetails($numRows, $crs, $filterCrs="", $filterType="", $timeOffset="", $timeWindow="")
	{
		// Params for the connection
		$params = array();
		$params["numRows"] = $numRows;
		$params["crs"] = $crs;

		if ($filterCrs) $params["filterCrs"] = $filterCrs;
		if ($filterType) $params["filterType"] = $filterType;
		if ($timeOffset) $params["timeOffset"] = $timeOffset;
		if ($timeWindow) $params["timeWindow"] = $timeWindow;
		
		// Make the connection
		try
		{
			$response = $this->soapClient->GetDepBoardWithDetails($params);
		}
		catch(SoapFault $soapFault)
		{
			trigger_error("Something's wrong", E_USER_ERROR);
		}

		// pass data back
		return (isset($response) ? $response : false);
	}
}

Once we have this file we can interface with the SOAP service with the applicable WSDL file, and I can begin to handle train data.

I am going to return all data and all fields to see what we have to play with, to do this I am going to do the below code…

<?php
	require("soapHandler.php");
	$train = new train("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
	$response = $train->GetDepBoardWithDetails(1,"MBR");
	header("Content-Type: text/plain");
	print_r($response->GetStationBoardResult->trainServices->service);

The script above will be the basis for the trainData.php file, with the first script which I have named soapHandler.php. You will need to provide the token key which you would have already requested when making a request to the soap service.

For the 2nd parameter, as I live in Middlesbrough, I have provided the CRS (Computer Reservation System) which is the 3 letter station code, as MBR. e.g. for Hull use HUL – for other stations, there are lists everywhere in order to find their 3 letter codes. I have also passed in the 1st parameter as 1, this is so it will return a maximum of 1 result.

The data I get back includes scheduled times, on-time/delayed arrival time, platform, calling points, and if there are cancellations and the reason why.

I did have to change my lookup to use Edinburgh as Middlesbrough had no more departures when I was coding this. I figured this out when I got the below response. As the GetStationBoardResult object was empty.

stdClass Object
(
    [generatedAt] => 2020-02-21T23:46:29.3118451+00:00
    [locationName] => Middlesbrough
    [crs] => MBR
    [nrccMessages] => stdClass Object
        (
            [message] => Array
                (
                    [0] => stdClass Object
                        (
                            [_] => The ticket office at this station is currently closed. 
                        )

                    [1] => stdClass Object
                        (
                            [_] => <p>The Ticket Vending Machines are currently out of order at this station.</p>
                        )

                )

        )

    [platformAvailable] => 1
)

As the data is returned as an object, if for example I wanted to output a list of calling points I can do this:

<?php
	require("soapHandler.php");
	$train = new train("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
	$response = $train->GetDepBoardWithDetails(1,"EDB");
	header("Content-Type: text/plain");

	$callingPoints = $response->GetStationBoardResult->trainServices->service->subsequentCallingPoints->callingPointList->callingPoint;
	$callingPoints = json_encode($callingPoints);
	$callingPoints = json_decode($callingPoints, true);


	foreach ($callingPoints as $callPoint)
	{
		print $callPoint['locationName'] . PHP_EOL;
	}

This resulted in:

Haymarket
Edinburgh Park
Uphall
Livingston North
Bathgate

When it came to checking for nrccMessages messages, interestingly Edinburgh did have a message

    [nrccMessages] => stdClass Object
        (
            [message] => stdClass Object
                (
                    [_] => <p></p>
<p>Poor weather is affecting journeys in Scotland. More details and the impact to your journey can be found in <A href="http://nationalrail.co.uk/service_disruptions/243304.aspx">Latest Travel News</A>.</p>
                )

        )

So we could consider seeing if a value for nrccMessages exists, and capture the message to display elsewhere.

Next up is a way of displaying the train information. I am not going to go into details on how to design something pretty, I will knock something basic up – based off a generic platform information screen. Essentially we just need to pass in details of the calling points, destination, platforms, and if the training is running on time to this template.

In order to generate the needed data from the info-overload, we can do something like:

<?php
	require("soapHandler.php");
	$train = new train("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
	$response = $train->GetDepBoardWithDetails(10,"MBR");
	header("Content-Type: text/plain");

	$response = json_encode($response);	
	$response = json_decode($response, true);

	$trainData = array();

	foreach ($response['GetStationBoardResult']['trainServices']['service'] as $train)
	{
		$temp = array();
		$temp['destination'] = $train['destination']['location']['locationName'];
		
		$temp['platform'] = $train['platform'];
		$temp['toc'] = $train['operator'];
		$temp['carriages'] = $train['length'];
		
		// Work out times
		$temp['schd'] = $train['std'];
		$temp['etd'] = $train['etd'];
		
		if ($train['etd'] != 'On time')
		{
			// Train is late, lol.
			$schd = strtotime($train['std']);
			$etd = strtotime($train['etd']);
			$delay = ($etd - $schd) / 60;
			
			$temp['delay'] = $delay;
			
			// Is a delay reason given?
			if (isset($train['delayReason']))
			{
				$temp['delayReason'] = $train['delayReason'];
			}
		}
		
		// is train cancelled
		$trainCancelled = isset($train['cancelReason']);
		
		if ($trainCancelled)
		{
			if ($train['cancelReason'] != '')
			{
				$temp['cancelReason'] = $train['cancelReason'];
			}
			
			$temp['cancelled'] = true;
		}
		
		// Get callpoints and times
		$temp['callingPoints'] = array();
		
		if (!$trainCancelled)
		{
			$trainCallingPoints = $train['subsequentCallingPoints']['callingPointList']['callingPoint'];

			// Direct train or multiple call points?
			if (isAssocArray($trainCallingPoints))
			{
				$temp['callingPoints'] = array(
					'location' => $trainCallingPoints['locationName'],
					'st' => $trainCallingPoints['st'],
					'et' => $trainCallingPoints['et']);
			}
			else
			{
				foreach ($trainCallingPoints as $callingPoints)
				{
					$callPoint = array(
						'location' => $callingPoints['locationName'],
						'st' => $callingPoints['st'],
						'et' => $callingPoints['et']);

					$temp['callingPoints'][] = $callPoint;
				}
			}
		}
		
		// push data to array
		$trainData[] = $temp;
	}

print_r(json_encode($trainData));

function isAssocArray($arr)
{
    if (array() === $arr) return false;
    return array_keys($arr) !== range(0, count($arr) - 1);
}

With the $trainData variable, if I was to do a print_r, I would see meaningful data which can be passed into the template…
Now I’ve had to add a “hacky” function because some trains were not returning as an associative array, I assume because it was a direct train, as the only stopping listed was the destination.

Piecing it together
Now we have some form of template; and the data we need – we can use AJAX to query the data and update the screen.

I won’t go into the nitty-gritty of how to do this, essentially we need a basic HTML file and a JavaScript file.
The JavaScript file will have a setTimeout() function which will do an AJAX call to this PHP file – the success handler will then parse the JSON response and loop through it. I am then generating the HTML of the table row, then appending it to the table by doing.

$("#trainTable").append(html);

My setTimeout function works like this, loadData() is called on pageload.

function loadData() 
{
	setTimeout(function () {
			$.ajax({
				url: "trainData.php",
				type: "GET",
				success: function(result) {
					handleResponse(result); 
					timeout = 60000;
				},
				complete: function(){
					loadData();
				},
				error : function(){
				alert('something went wrong');
			}
		});
	}, timeout);
}

With my handleResults looking like this

function handleResponse(result)
{
	var data = JSON.parse(result);
	var trainData = data['train'];
	
	$("#trainTable").html('');
		
	var index;
	
	for (index = 0; index < trainData.length; ++index)
	{
		var borderStyle = '';
		var carriageInfo = '';
		
		if (index != (trainData.length-1))
		{
			borderStyle = 'border-bottom: 1px solid white;';
		}
		
		if (trainData[index]['carriages'] != '-')
		{
			carriageInfo = ', this train has ' + trainData[index]['carriages'] + ' carriages';
		}
		
		var html = `
			<div class="row">
				<div class="col-sm-1">` + trainData[index]['schd'] + `</div>
				<div class="col-sm-8 TrainDestination">` + trainData[index]['destination'] +`</div>
				<div class="col-sm-1 text-md-center">` + trainData[index]['platform'] + `</div>
				<div class="col-sm-2 text-md-center">` + trainData[index]['etd'] + `</div>
			</div>
			<div  class="row">
			</div>
			<div class="row no-gutters" style="padding-left: 94px;` + borderStyle + `">
				<div class="col-sm-12 col-md-1 trainfont">Callling at:</div>
				<div class="col-sm-8 col-md-8 trainfont">
				  <marquee>` + trainData[index]['callingPointsString'] + `</marquee>
				</div>
				<span class="trainfont">Operated by ` + trainData[index]['toc'] + ``+
				carriageInfo +` </span>
			</div>`;
		
		
		$("#trainTable").append(html);
	}
	
	$("#nrccMessages").html(data['nrccMessages']);
	
}

You can see my implementation on github here.

For now that is all, but I have something cool we can do with the JSON train data.

Something that could be done if you was that way inclined, is hooking it up to a Raspberry Pi and run it off a screen… Pretty nifty if you wanted to sell it as a product to a cafe near a railway station.

Example of the board showing data from Kings Cross.

Anyhoo,

That covers me creating a basic departures board. Pretty easy to implement, however, the responses back from the OpenLDBWS can be inconsistent so you do need to do a few isset checks. For instance, if no platform information is available it doesn’t return anything vs. returning at least a key with an empty value or; NULL, ‘TBD’, etc..

Related Post

Leave a Reply

Your email address will not be published.