Skip to content

Commit

Permalink
Merge pull request #33 from jpsingleton/station-codes
Browse files Browse the repository at this point in the history
Station codes
  • Loading branch information
jpsingleton committed May 2, 2015
2 parents 0359d7a + bbf7a46 commit cc4fac8
Show file tree
Hide file tree
Showing 16 changed files with 2,956 additions and 31 deletions.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,18 @@ There is an additional Python (v2) [example for a Raspberry Pi and Blinky Tape R

### URL Format

The URL format is `{board}/{crs}/{filtertype}/{filtercrs}/{numrows}` or `{board}/{crs}/{numrows}` where only board and CRS are required.
The URL format is `{board}/{CRS|StationName}/{filterType}/{filterCRS|StationName}/{numRows}` or `{board}/{CRS|StationName}/{numRows}` where only board and CRS (or a station name) are required. The filter type can be either `to` or `from` (case is not important).

A station name can be used in place of CRS codes if the name matches only one station (or matches one exactly) but case is not important. See the [CRS section](#crs-station-codes) below for more information.

Examples:

* 10 (default value) Arrivals and Departures at Clapham Junction: `/all/clj`
* 15 Arrivals and Departures at Clapham Junction: `/all/clj/15`
* 10 (default value) Departures at Clapham Junction to Waterloo: `/departures/clj/to/wat`
* 15 Arrivals at Clapham Junction from Waterloo: `/arrivals/clj/from/wat/15`
* 10 (default value) Arrivals and Departures at Wandsworth Common to Clapham Junction: `/all/wandsworth common/to/clapham junction`
* 20 Departures at East Croydon to London Victoria: `/departures/east croydon/to/london victoria/20`

### Departures

Expand Down Expand Up @@ -94,7 +99,26 @@ You can also pass in a comma separated list of 24 hour train times to filter on

## CRS Station Codes

CRS (Computer Reservation System) station codes are available [here](http://www.nationalrail.co.uk/static/documents/content/station_codes.csv).
CRS (Computer Reservation System) station codes are available from the following endpoint:

[`/crs/{query}`](https://huxley.apphb.com/crs)

If `query` is omitted then all CRS codes are returned along with their respective station names. If `query` is provided then only station names matching it will be returned along with their CRS codes.

Example response for `/crs/oswald`:
```javascript
[
{
"stationName": "Church & Oswaldtwistle",
"crsCode": "CTW"
},
{
"stationName": "Lazonby & Kirkoswald",
"crsCode": "LZB"
}
]
```

[More information on the wiki](https://github.com/jpsingleton/Huxley/wiki/CRS-station-codes).

## Hosting Quick Start
Expand All @@ -114,3 +138,5 @@ Made by [James Singleton](https://unop.uk)
This program is licensed under the terms of the GNU Affero General Public License. This means that you need to share any changes (even if only running on a public server).

If you would like another license (such as a commercial license with an invoice) then this can be provided. Please get in touch (send an email to jpsingleton at gmail dot com).

Contains public sector information licensed under the Open Government Licence v3.0.
43 changes: 32 additions & 11 deletions src/Huxley/Controllers/BaseController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,46 @@ You should have received a copy of the GNU Affero General Public License
*/

using System;
using System.Configuration;
using System.Linq;
using System.Web.Http;
using Huxley.ldbServiceReference;

namespace Huxley.Controllers {
public class BaseController : ApiController {
protected static AccessToken MakeAccessToken(Guid accessToken) {
var darwinAccessToken = ConfigurationManager.AppSettings["DarwinAccessToken"];
var clientAccessToken = ConfigurationManager.AppSettings["ClientAccessToken"];
Guid dat;
Guid cat;
if (Guid.TryParse(darwinAccessToken, out dat) &&
Guid.TryParse(clientAccessToken, out cat) &&
cat == accessToken) {
accessToken = dat;
// If ClientAccessToken is an empty GUID then no token is required in the Huxley URL.
// If ClientAccessToken matches the token in the URL then the DarwinAccessToken will be used instead in the SOAP call.
// Otherwise the URL token is passed straight through
if (HuxleyApi.Settings.ClientAccessToken == accessToken) {
accessToken = HuxleyApi.Settings.DarwinAccessToken;
}
var token = new AccessToken { TokenValue = accessToken.ToString() };
return token;
return new AccessToken { TokenValue = accessToken.ToString() };
}

protected static string MakeCrsCode(string query) {
// Process CRS codes if query is present
if (!string.IsNullOrWhiteSpace(query) &&
// If query is not in the list of CRS codes
!HuxleyApi.CrsCodes.Any(c =>
c.CrsCode.Equals(query, StringComparison.InvariantCultureIgnoreCase))) {
// And query matches a single station name
var results = HuxleyApi.CrsCodes.Where(c =>
c.StationName.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0).ToList();
if (results.Count == 1) {
// Return the only possible CRS code
return results[0].CrsCode;
}
// If more than one match then return one if it matches exactly
if (results.Count > 1) {
var bestMatch = results.FirstOrDefault(r =>
r.StationName.Equals(query, StringComparison.InvariantCultureIgnoreCase));
if (null != bestMatch) {
return bestMatch.CrsCode;
}
}
}
// Otherwise return the query as is
return query;
}
}
}
40 changes: 40 additions & 0 deletions src/Huxley/Controllers/CrsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Huxley - a JSON proxy for the UK National Rail Live Departure Board SOAP API
Copyright (C) 2015 James Singleton
* http://huxley.unop.uk
* https://github.com/jpsingleton/Huxley
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace Huxley.Controllers {
public class CrsController : ApiController {
// GET /crs
public IEnumerable<CrsRecord> Get() {
return HuxleyApi.CrsCodes;
}

// GET /crs/{query}
public IEnumerable<CrsRecord> Get(string query) {
// Could use a RegEx here but putting user input into a RegEx can be dangerous
var results = HuxleyApi.CrsCodes.Where(c => c.StationName.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0);
return results;
}
}
}
9 changes: 5 additions & 4 deletions src/Huxley/Controllers/DelaysController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public class DelaysController : BaseController {
// GET /delays/{crs}/{filtertype}/{filtercrs}/{numrows}/{stds}?accessToken=[your token]
public async Task<DelaysResponse> Get([FromUri] StationBoardRequest request) {

// Process CRS codes
request.Crs = MakeCrsCode(request.Crs);
request.FilterCrs = MakeCrsCode(request.FilterCrs);

// Parse the list of comma separated STDs if provided (e.g. /btn/to/lon/50/0729,0744,0748)
var stds = new List<string>();
if (!string.IsNullOrWhiteSpace(request.Std)) {
Expand Down Expand Up @@ -67,9 +71,6 @@ public async Task<DelaysResponse> Get([FromUri] StationBoardRequest request) {
var totalDelayMinutes = 0;
var totalTrainsDelayed = 0;

dynamic config = new Formo.Configuration();
int delayMinutesThreshold = config.DelayMinutesThreshold<int>(5);

var token = MakeAccessToken(request.AccessToken);

var filterCrs = request.FilterCrs;
Expand Down Expand Up @@ -119,7 +120,7 @@ public async Task<DelaysResponse> Get([FromUri] StationBoardRequest request) {
if (DateTime.TryParse(si.std, out std)) {
var late = etd.Subtract(std);
totalDelayMinutes += (int)late.TotalMinutes;
if (late.TotalMinutes > delayMinutesThreshold) {
if (late.TotalMinutes > HuxleyApi.Settings.DelayMinutesThreshold) {
totalTrainsDelayed++;
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/Huxley/Controllers/StationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public class StationController : BaseController {
// GET /{board}/CRS?accessToken=[your token]
public async Task<StationBoard> Get([FromUri] StationBoardRequest request) {

// Process CRS codes
request.Crs = MakeCrsCode(request.Crs);
request.FilterCrs = MakeCrsCode(request.FilterCrs);

var client = new LDBServiceSoapClient();

// Avoiding Problems with the Using Statement in WCF clients
Expand Down
26 changes: 26 additions & 0 deletions src/Huxley/CrsRecord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
Huxley - a JSON proxy for the UK National Rail Live Departure Board SOAP API
Copyright (C) 2015 James Singleton
* http://huxley.unop.uk
* https://github.com/jpsingleton/Huxley
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace Huxley {
public class CrsRecord {
public string StationName { get; set; }
public string CrsCode { get; set; }
}
}
2 changes: 1 addition & 1 deletion src/Huxley/Global.asax
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<%@ Application Codebehind="Global.asax.cs" Inherits="Huxley.WebApiApplication" Language="C#" %>
<%@ Application Codebehind="Global.asax.cs" Inherits="Huxley.HuxleyApi" Language="C#" %>
85 changes: 84 additions & 1 deletion src/Huxley/Global.asax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,27 @@ You should have received a copy of the GNU Affero General Public License
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using CsvHelper;
using Formo;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace Huxley {
public class WebApiApplication : HttpApplication {
public class HuxleyApi : HttpApplication {

// Singleton to store the station name to CRS lookup
public static IList<CrsRecord> CrsCodes { get; private set; }

// Singleton to store the Huxley settings
public static HuxleySettings Settings { get; private set; }

protected void Application_Start() {
// Makes the JSON easier to read in a browser without installing an extension like JSONview
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Formatting = Formatting.Indented;
Expand All @@ -38,6 +52,13 @@ protected void Application_Start() {

// Pass Register into Configure to support attribute routing in the future
GlobalConfiguration.Configure(WebApiConfig.Register);

// Load settings
dynamic config = new Configuration();
Settings = config.Bind<HuxleySettings>();

// Set the CRS dictionary passing in embedded CRS path
CrsCodes = GetCrsCodes(Server.MapPath("~/RailReferences.csv")).Result;
}

protected void Application_BeginRequest(object sender, EventArgs e) {
Expand All @@ -46,5 +67,67 @@ protected void Application_BeginRequest(object sender, EventArgs e) {
application.Context.Response.Headers.Remove("Server");
}
}

private static async Task<IList<CrsRecord>> GetCrsCodes(string embeddedCrsPath) {
var codes = new List<CrsRecord>();

// NRE list - incomplete / old (some codes only in NaPTAN work against the Darwin web service)
const string crsUrl = "http://www.nationalrail.co.uk/static/documents/content/station_codes.csv";
try {
using (var client = new HttpClient()) {
var stream = await client.GetStreamAsync(crsUrl);
using (var csvReader = new CsvReader(new StreamReader(stream))) {
// Need a custom map as NRE headers are different to NaPTAN
csvReader.Configuration.RegisterClassMap<NreCrsRecordMap>();
AddCodesToList(codes, csvReader);
}
}
// ReSharper disable EmptyGeneralCatchClause
} catch {
// Don't do anything if this fails as we try to load from NaPTAN next
// ReSharper restore EmptyGeneralCatchClause
}

// NaPTAN - has better data than the NRE list but is missing some entries (updated weekly)
// Part of this archive https://www.dft.gov.uk/NaPTAN/snapshot/NaPTANcsv.zip along with other modes of transport
// Contains public sector information licensed under the Open Government Licence v3.0.
const string naptanRailUrl = "https://raw.githubusercontent.com/jpsingleton/Huxley/master/src/Huxley/RailReferences.csv";
try {
// First try to get the latest version
using (var client = new HttpClient()) {
var stream = await client.GetStreamAsync(naptanRailUrl);
using (var csvReader = new CsvReader(new StreamReader(stream))) {
AddCodesToList(codes, csvReader);
}
}
} catch {
try {
// If we can't get the latest version then use the embedded version
// Might be a little bit out of date but probably good enough
using (var stream = File.OpenRead(embeddedCrsPath)) {
using (var csvReader = new CsvReader(new StreamReader(stream))) {
AddCodesToList(codes, csvReader);
}
}
// ReSharper disable EmptyGeneralCatchClause
} catch {
// If this doesn't work continue to start up
// ReSharper restore EmptyGeneralCatchClause
}
}

return codes;
}

private static void AddCodesToList(List<CrsRecord> codes, CsvReader csvReader) {
// Enumerate results and add to a list as reader can only be enumerated once
// Only missing codes are added to the list (first pass will add all codes)
codes.AddRange(csvReader.GetRecords<CrsRecord>().Where(c => codes.All(code => code.CrsCode != c.CrsCode))
.Select(c => new CrsRecord {
// NaPTAN suffixes most station names with "Rail Station" which we don't want
StationName = c.StationName.Replace("Rail Station", "").Trim(),
CrsCode = c.CrsCode,
}));
}
}
}
9 changes: 9 additions & 0 deletions src/Huxley/Huxley.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="CsvHelper, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8c4959082be5c823, processorArchitecture=MSIL">
<HintPath>..\packages\CsvHelper.2.13.0.0\lib\net40-client\CsvHelper.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Formo, Version=1.4.3.392, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Formo.1.4.3.392\lib\net40\Formo.dll</HintPath>
<Private>True</Private>
Expand Down Expand Up @@ -85,15 +89,19 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Controllers\CrsController.cs" />
<Compile Include="Controllers\DelaysController.cs" />
<Compile Include="Controllers\StationController.cs" />
<Compile Include="Controllers\BaseController.cs" />
<Compile Include="Controllers\ServiceController.cs" />
<Compile Include="CrsRecord.cs" />
<Compile Include="HuxleySettings.cs" />
<Compile Include="Models\BaseRequest.cs" />
<Compile Include="Models\Board.cs" />
<Compile Include="Models\DelaysResponse.cs" />
<Compile Include="Models\ServiceRequest.cs" />
<Compile Include="Models\StationBoardRequest.cs" />
<Compile Include="NreCrsRecordMap.cs" />
<Compile Include="WebApiConfig.cs" />
<Compile Include="Global.asax.cs">
<DependentUpon>Global.asax</DependentUpon>
Expand Down Expand Up @@ -138,6 +146,7 @@
<SubType>Designer</SubType>
</Content>
<Content Include="Service References\ldbServiceReference\darwin_token_types_2013-11-28.wsdl" />
<Content Include="RailReferences.csv" />
<None Include="Service References\ldbServiceReference\darwin_token_types_2013-11-28.xsd">
<SubType>Designer</SubType>
</None>
Expand Down
Loading

0 comments on commit cc4fac8

Please sign in to comment.