From 777519ff5ddfb2dc2135703a56a4c14e99777294 Mon Sep 17 00:00:00 2001 From: Fred Boniface Date: Mon, 30 Mar 2026 23:34:12 +0100 Subject: [PATCH] Add NearestStations Card & Location monitor --- package-lock.json | 54 ++-- package.json | 2 +- .../components/ui/LocationSearchBox.svelte | 3 +- src/lib/components/ui/TocStyle.svelte | 81 +++--- .../ui/cards/NearbyStationsCard.svelte | 69 +++++ .../components/ui/cards/pis/PisCode.svelte | 72 ++--- .../ui/cards/pis/PisStartEndCard.svelte | 77 +++-- src/lib/geohash.svelte.ts | 90 ++++++ src/lib/locations-object.svelte.ts | 6 +- src/lib/owlClient.ts | 20 +- src/routes/+layout.svelte | 1 + src/routes/+page.svelte | 2 + src/routes/about/+page.svelte | 6 +- src/routes/pis/+page.svelte | 264 +++++++++--------- src/routes/pis/+page.ts | 2 +- static/api/tiplocs | 106 ------- 16 files changed, 465 insertions(+), 390 deletions(-) create mode 100644 src/lib/components/ui/cards/NearbyStationsCard.svelte create mode 100644 src/lib/geohash.svelte.ts delete mode 100644 static/api/tiplocs diff --git a/package-lock.json b/package-lock.json index 3202b3a..7ab1ce7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "devDependencies": { "@eslint/compat": "^2.0.2", "@eslint/js": "^9.39.2", - "@owlboard/owlboard-ts": "^3.0.0-dev.20260325T1023", + "@owlboard/owlboard-ts": "^3.0.0-dev.202603302258", "@playwright/test": "^1.58.1", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.50.2", @@ -780,20 +780,21 @@ } }, "node_modules/@owlboard/api-schema-types": { - "version": "3.0.2-alpha2", - "resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fapi-schema-types/-/3.0.2-alpha2/api-schema-types-3.0.2-alpha2.tgz", - "integrity": "sha512-KyX4QcOCzVqYpiXY+WfhM1soXduMt2ldG6JSBK2WBxXWokS+keZshOHWHGTZvPLoZEWsuPznMAdzytI03/D3Ag==", + "version": "3.0.2-alpha3", + "resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fapi-schema-types/-/3.0.2-alpha3/api-schema-types-3.0.2-alpha3.tgz", + "integrity": "sha512-3NFP21QdSfjziwlGQixlNnUWC55HlKZGyWANIOimWu0FZejWQWExJiaAVfb6m3Sbv+zvQMu3B8mzcMCcGadZCQ==", "dev": true, "license": "MIT" }, "node_modules/@owlboard/owlboard-ts": { - "version": "3.0.0-dev.20260325T1023", - "resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fowlboard-ts/-/3.0.0-dev.20260325T1023/owlboard-ts-3.0.0-dev.20260325t1023.tgz", - "integrity": "sha512-h5jAO9MYmYUToXvw3gCohUbG3oVl7h+PKrZ94I1NahXOLEd+CaQzXbXk5+KCsnojgkqf0I1FavaBbADgb2ZKkQ==", + "version": "3.0.0-dev.202603302258", + "resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fowlboard-ts/-/3.0.0-dev.202603302258/owlboard-ts-3.0.0-dev.202603302258.tgz", + "integrity": "sha512-uJRoahtqnkmkPg8QsIWhtxhAibWY8xG25fhjzzdDFNE/2+uCj1ev5iD+t0O1dkE7ic3yTcMZqDitEspW216bjw==", "dev": true, "license": "GPL-3.0", "dependencies": { - "@owlboard/api-schema-types": "^3.0.2-alpha2" + "@owlboard/api-schema-types": "^3.0.2-alpha3", + "latlon-geohash": "^2.0.0" } }, "node_modules/@playwright/test": { @@ -1558,9 +1559,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -1881,9 +1882,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2664,6 +2665,13 @@ "dev": true, "license": "MIT" }, + "node_modules/latlon-geohash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/latlon-geohash/-/latlon-geohash-2.0.0.tgz", + "integrity": "sha512-OKBswTwrvTdtenV+9C9euBmvgGuqyjJNAzpQCarRz1m8/pYD2nz9fKkXmLs2S3jeXaLi3Ry76twQplKKUlgS/g==", + "dev": true, + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2901,9 +2909,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3028,9 +3036,9 @@ } }, "node_modules/postcss-load-config/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", "engines": { @@ -3843,9 +3851,9 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "optional": true, diff --git a/package.json b/package.json index c6bda83..b1612b0 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@eslint/compat": "^2.0.2", "@eslint/js": "^9.39.2", - "@owlboard/owlboard-ts": "^3.0.0-dev.20260325T1023", + "@owlboard/owlboard-ts": "^3.0.0-dev.202603302258", "@playwright/test": "^1.58.1", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.50.2", diff --git a/src/lib/components/ui/LocationSearchBox.svelte b/src/lib/components/ui/LocationSearchBox.svelte index f997ce7..31ca9e5 100644 --- a/src/lib/components/ui/LocationSearchBox.svelte +++ b/src/lib/components/ui/LocationSearchBox.svelte @@ -27,7 +27,6 @@ return LOCATIONS.data .filter((r) => tokens.every((t) => r.s.includes(t))) .sort((a, b) => { - // Priority One - Exact CRS Match const aExactCrs = a.c?.toLowerCase() === lowerQuery; const bExactCrs = b.c?.toLowerCase() === lowerQuery; @@ -36,7 +35,7 @@ // Priority Two - 'Stations' with CRS if (!!a.c && !b.c) return -1; - if (!a.c & !! b.c) return 1; + if (!a.c & !!b.c) return 1; // Alphabetical Sort return a.n.localeCompare(b.n); diff --git a/src/lib/components/ui/TocStyle.svelte b/src/lib/components/ui/TocStyle.svelte index 3349838..1b4c7df 100644 --- a/src/lib/components/ui/TocStyle.svelte +++ b/src/lib/components/ui/TocStyle.svelte @@ -1,53 +1,56 @@
- {code} + {code}
\ No newline at end of file + .XC { + /* CrossCountry */ + background-color: #660000; + color: #e4d5b1; + } + diff --git a/src/lib/components/ui/cards/NearbyStationsCard.svelte b/src/lib/components/ui/cards/NearbyStationsCard.svelte new file mode 100644 index 0000000..602bde0 --- /dev/null +++ b/src/lib/components/ui/cards/NearbyStationsCard.svelte @@ -0,0 +1,69 @@ + + + +
+ {#if nearestStationsState.error && nearestStationsState.list.length === 0} +

{nearestStationsState.error}

+ {:else if nearestStationsState.loading && nearestStationsState.list.length === 0} +

Locating stations...

+ {:else} +
+ {#each nearestStationsState.list as station (station.c)} +
+ +
+ {/each} +
+ {/if} +
+
+ + diff --git a/src/lib/components/ui/cards/pis/PisCode.svelte b/src/lib/components/ui/cards/pis/PisCode.svelte index 968a103..cde44c6 100644 --- a/src/lib/components/ui/cards/pis/PisCode.svelte +++ b/src/lib/components/ui/cards/pis/PisCode.svelte @@ -1,28 +1,34 @@
-
-
- -
-
-
- - -
+
+
+ +
+
+
+ + +
@@ -34,21 +40,21 @@ function resetValues(): void { padding: 10px 0 10px 0; } - .textbox-container { - display: flex; - width: 100%; - justify-content: center; - gap: 4rem; - } + .textbox-container { + display: flex; + width: 100%; + justify-content: center; + gap: 4rem; + } - .textbox-item-wrapper { - width: 30%; - } + .textbox-item-wrapper { + width: 30%; + } - .button-wrapper { - display: flex; - justify-content: center; - gap: 1rem; - margin-top: 15px; - } - \ No newline at end of file + .button-wrapper { + display: flex; + justify-content: center; + gap: 1rem; + margin-top: 15px; + } + diff --git a/src/lib/components/ui/cards/pis/PisStartEndCard.svelte b/src/lib/components/ui/cards/pis/PisStartEndCard.svelte index 49dad3e..ddf347a 100644 --- a/src/lib/components/ui/cards/pis/PisStartEndCard.svelte +++ b/src/lib/components/ui/cards/pis/PisStartEndCard.svelte @@ -1,34 +1,33 @@
-
-
- -
-
- -
-
-
- - -
+
+
+ +
+
+ +
+
+
+ + +
@@ -40,21 +39,21 @@ function resetValues(): void { padding: 10px 0 10px 0; } - .textbox-container { - display: flex; - width: 100%; - justify-content: center; - gap: 4rem; - } + .textbox-container { + display: flex; + width: 100%; + justify-content: center; + gap: 4rem; + } - .textbox-item-wrapper { - width: 30%; - } + .textbox-item-wrapper { + width: 30%; + } - .button-wrapper { - display: flex; - justify-content: center; - gap: 1rem; - margin-top: 15px; - } - \ No newline at end of file + .button-wrapper { + display: flex; + justify-content: center; + gap: 1rem; + margin-top: 15px; + } + diff --git a/src/lib/geohash.svelte.ts b/src/lib/geohash.svelte.ts new file mode 100644 index 0000000..8c6f575 --- /dev/null +++ b/src/lib/geohash.svelte.ts @@ -0,0 +1,90 @@ +import { OwlClient, ValidationError, ApiError } from './owlClient'; +import type { ApiStationsNearestStations } from '@owlboard/owlboard-ts'; + +class NearestStationsState { + list = $state([]); + currentHash = $state(''); + loading = $state(true); + error = $state(null); + + private geoConfig: PositionOptions = { + enableHighAccuracy: false, + timeout: 30000, + maximumAge: 120000 + }; + + constructor() { + if (typeof window !== 'undefined' && 'geolocation' in navigator) { + this.jumpstart(); + this.initWatcher(); + } + } + + private jumpstart() { + navigator.geolocation.getCurrentPosition( + (pos) => this.handleUpdate(pos.coords.latitude, pos.coords.longitude), + (err) => this.handleError(err), + this.geoConfig + ); + } + + private initWatcher() { + navigator.geolocation.watchPosition( + (pos) => this.handleUpdate(pos.coords.latitude, pos.coords.longitude), + (err) => this.handleError(err), + this.geoConfig + ); + } + + private async handleUpdate(lat: number, lon: number) { + const newHash = OwlClient.stationData.generateGeohash(lat, lon); + if (newHash !== this.currentHash) { + this.loading = true; + + try { + const result = await OwlClient.stationData.getNearestStations(newHash); + this.list = result.data; + this.error = null; + this.currentHash = newHash; + } catch (e) { + this.handleApiError(e); + } finally { + this.loading = false; + } + } + } + + private handleError(err: GeolocationPositionError) { + if (err.code === 1) { + this.error = 'Location access denied by device'; + } else { + this.error = 'Waiting for GPS signal...'; + } + } + + private handleApiError(e: unknown) { + if (e instanceof ValidationError) { + this.error = `Request Error: ${e.reason} (Field: ${e.field})`; + } else if (e instanceof ApiError) { + switch (e.status) { + case 404: + this.error = 'No stations found nearby'; + break; + case 429: + this.error = 'Too many requests, will retry'; + break; + case 500: + this.error = 'Server Error, will retry'; + break; + default: + this.error = `Service error: ${e.code}`; + } + } else { + this.error = 'Connection lost, waiting for signal'; + } + + console.error('OwlBoard API Error:', e); + } +} + +export const nearestStationsState = new NearestStationsState(); diff --git a/src/lib/locations-object.svelte.ts b/src/lib/locations-object.svelte.ts index e804181..520229f 100644 --- a/src/lib/locations-object.svelte.ts +++ b/src/lib/locations-object.svelte.ts @@ -1,5 +1,5 @@ -import { OwlClient } from "./owlClient"; -import type { ApiLocationFilter } from '@owlboard/owlboard-ts' +import { OwlClient } from './owlClient'; +import type { ApiLocationFilter } from '@owlboard/owlboard-ts'; class LocationStore { data = $state([]); @@ -9,7 +9,7 @@ class LocationStore { if (this.loaded) return; try { - const fetch = await OwlClient.locationFilter.getLocationFilterData() + const fetch = await OwlClient.locationFilter.getLocationFilterData(); this.data = fetch.data; this.loaded = true; } catch (err) { diff --git a/src/lib/owlClient.ts b/src/lib/owlClient.ts index b6eab3f..bff817d 100644 --- a/src/lib/owlClient.ts +++ b/src/lib/owlClient.ts @@ -1,21 +1,21 @@ -import { OwlBoardClient, ValidationError, ApiError } from "@owlboard/owlboard-ts"; -import { browser, dev } from "$app/environment"; +import { OwlBoardClient, ValidationError, ApiError } from '@owlboard/owlboard-ts'; +import { browser, dev } from '$app/environment'; // Import the runes containing the API Key config Here... const baseUrl: string = browser ? window.location.origin : ''; const getBaseUrl = () => { - if (!browser) return ''; + if (!browser) return ''; - if (dev) return 'https://test.owlboard.info'; + if (dev) return 'https://test.owlboard.info'; - return window.location.origin; -} + return window.location.origin; +}; export const OwlClient = new OwlBoardClient( - getBaseUrl(), - // API Key Here when ready!!! -) + getBaseUrl() + // API Key Here when ready!!! +); -export { ValidationError, ApiError }; \ No newline at end of file +export { ValidationError, ApiError }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 79fc665..79d6f94 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,6 +4,7 @@ import { onMount } from 'svelte'; import { LOCATIONS } from '$lib/locations-object.svelte'; + import { nearestStationsState } from '$lib/geohash.svelte'; import '$lib/global.css'; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 6ec1a92..c18f801 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,9 +1,11 @@
+
\ No newline at end of file + .reset-button-container { + padding: 20px 0 3px 0; + } + diff --git a/src/routes/pis/+page.ts b/src/routes/pis/+page.ts index 2d33b8a..798c608 100644 --- a/src/routes/pis/+page.ts +++ b/src/routes/pis/+page.ts @@ -2,4 +2,4 @@ export const load = () => { return { title: 'PIS Codes' }; -}; \ No newline at end of file +}; diff --git a/static/api/tiplocs b/static/api/tiplocs deleted file mode 100644 index afa6f60..0000000 --- a/static/api/tiplocs +++ /dev/null @@ -1,106 +0,0 @@ -[ - {"n":"Manchester Piccadilly","t":"MANPICD","c":"MAN","s":"manchester piccadilly man manpicd"}, - {"n":"Manchester Victoria","t":"MCV","c":"MCV","s":"manchester victoria mcv"}, - {"n":"Manchester Oxford Road","t":"MCOR","c":"MCO","s":"manchester oxford road mco mcor"}, - {"n":"Manchester Airport","t":"MANAPTL","c":"MIA","s":"manchester airport mia manaptl"}, - {"n":"London Euston","t":"EUSTON","c":"EUS","s":"london euston eus euston"}, - {"n":"London Kings Cross","t":"KGX","c":"KGX","s":"london kings cross kgx kingscross"}, - {"n":"London St Pancras International","t":"STPANCR","c":"STP","s":"london st pancras international stp stpancr"}, - {"n":"London Paddington","t":"PADTON","c":"PAD","s":"london paddington pad padton"}, - {"n":"London Victoria","t":"VIC","c":"VIC","s":"london victoria vic"}, - {"n":"London Liverpool Street","t":"LIVST","c":"LST","s":"london liverpool street lst livst"}, - {"n":"London Bridge","t":"LONGBR","c":"LBG","s":"london bridge lbg longbr"}, - {"n":"Birmingham New Street","t":"BHMNEWST","c":"BHM","s":"birmingham new street bhm bhmnewst bham"}, - {"n":"Birmingham Moor Street","t":"BHMMRS","c":"BMO","s":"birmingham moor street bmo bhmmrs"}, - {"n":"Birmingham Snow Hill","t":"BHMSH","c":"BSW","s":"birmingham snow hill bsw bhmsh"}, - {"n":"Leeds","t":"LEEDS","c":"LDS","s":"leeds lds"}, - {"n":"York","t":"YORK","c":"YRK","s":"york yrk"}, - {"n":"Liverpool Lime Street","t":"LIVLST","c":"LIV","s":"liverpool lime street liv livlst"}, - {"n":"Liverpool Central","t":"LIVCEN","c":"LVC","s":"liverpool central lvc livcen"}, - {"n":"Sheffield","t":"SHEFFLD","c":"SHF","s":"sheffield shf sheffld"}, - {"n":"Nottingham","t":"NOTTM","c":"NOT","s":"nottingham not nottm"}, - {"n":"Derby","t":"DERBY","c":"DBY","s":"derby dby"}, - {"n":"Leicester","t":"LEICEST","c":"LEI","s":"leicester lei leicest"}, - {"n":"Bristol Temple Meads","t":"BRSTLTM","c":"BRI","s":"bristol temple meads bri brstltm"}, - {"n":"Cardiff Central","t":"CDFCEN","c":"CDF","s":"cardiff central cdf cdfcen"}, - {"n":"Newcastle","t":"NEWCAST","c":"NCL","s":"newcastle ncl newcast"}, - {"n":"Edinburgh Waverley","t":"EDINBUR","c":"EDB","s":"edinburgh waverley edb edinbur"}, - {"n":"Glasgow Central","t":"GLASCEN","c":"GLC","s":"glasgow central glc glascen"}, - {"n":"Glasgow Queen Street","t":"GLAQS","c":"GLQ","s":"glasgow queen street glq glaqs"}, - {"n":"Reading","t":"READING","c":"RDG","s":"reading rdg"}, - {"n":"Oxford","t":"OXFORD","c":"OXF","s":"oxford oxf"}, - {"n":"Cambridge","t":"CAMBRDG","c":"CBG","s":"cambridge cbg cambrdg"}, - {"n":"Peterborough","t":"PBOUGH","c":"PBO","s":"peterborough pbo pbough"}, - {"n":"Doncaster","t":"DONCAST","c":"DON","s":"doncaster don doncast"}, - {"n":"Crewe","t":"CREWE","c":"CRE","s":"crewe cre"}, - {"n":"Preston","t":"PRESTON","c":"PRE","s":"preston pre"}, - {"n":"Blackpool North","t":"BPLNOR","c":"BPN","s":"blackpool north bpn bplnor"}, - {"n":"Bolton","t":"BOLTON","c":"BON","s":"bolton bon"}, - {"n":"Huddersfield","t":"HUDDSFD","c":"HUD","s":"huddersfield hud huddsfd"}, - {"n":"Stockport","t":"STOCKPT","c":"SPT","s":"stockport spt stockpt"}, - {"n":"Wigan North Western","t":"WIGNW","c":"WGN","s":"wigan north western wgn wignw"}, - {"n":"Bath Spa","t":"BATHSPA","c":"BTH","s":"bath spa bth bathspa"}, - {"n":"Exeter St Davids","t":"EXD","c":"EXD","s":"exeter st davids exd"}, - {"n":"Plymouth","t":"PLYMTH","c":"PLY","s":"plymouth ply plymth"}, - {"n":"Truro","t":"TRURO","c":"TRU","s":"truro tru"}, - {"n":"Aberdeen","t":"ABERDN","c":"ABD","s":"aberdeen abd aberdn"}, - {"n":"Inverness","t":"INVNESS","c":"INV","s":"inverness inv invness"}, - {"n":"Perth","t":"PERTH","c":"PTH","s":"perth pth"}, - {"n":"Dundee","t":"DUNDEE","c":"DEE","s":"dundee dee"}, - {"n":"Stirling","t":"STIRLNG","c":"STG","s":"stirling stg stirlng"}, - {"n":"Falkirk Grahamston","t":"FLKGRA","c":"FKG","s":"falkirk grahamston fkg flkgra"}, - {"n":"Motherwell","t":"MOTHRWL","c":"MTH","s":"motherwell mth mothrwl"}, - {"n":"Paisley Gilmour Street","t":"PAISGL","c":"PYG","s":"paisley gilmour street pyg paisgl"}, - {"n":"Greenock Central","t":"GRNOCK","c":"GKC","s":"greenock central gkc grnock"}, - {"n":"Ayr","t":"AYR","c":"AYR","s":"ayr"}, - {"n":"Carlisle","t":"CARLISL","c":"CAR","s":"carlisle car carlisl"}, - {"n":"Penrith North Lakes","t":"PNRITH","c":"PNR","s":"penrith north lakes pnr pnrith"}, - {"n":"Kendal","t":"KENDAL","c":"KEN","s":"kendal ken"}, - {"n":"Windermere","t":"WNDRMRE","c":"WDM","s":"windermere wdm wndrme"}, - {"n":"Lancaster","t":"LANCAST","c":"LAN","s":"lancaster lan lancast"}, - {"n":"Chester","t":"CHESTER","c":"CTR","s":"chester ctr"}, - {"n":"Warrington Bank Quay","t":"WRRGBQ","c":"WBQ","s":"warrington bank quay wbq wrrgbq"}, - {"n":"Warrington Central","t":"WRRGCN","c":"WAC","s":"warrington central wac wrrgcn"}, - {"n":"Runcorn","t":"RUNCORN","c":"RUN","s":"runcorn run"}, - {"n":"Widnes","t":"WIDNES","c":"WID","s":"widnes wid"}, - {"n":"Southport","t":"STHPORT","c":"SOP","s":"southport sop sthport"}, - {"n":"Ormskirk","t":"ORMSKRK","c":"OMS","s":"ormskirk oms ormskrk"}, - {"n":"Blackburn","t":"BLKBRN","c":"BBN","s":"blackburn bbn blkbrn"}, - {"n":"Burnley Manchester Road","t":"BURNMR","c":"BYM","s":"burnley manchester road bym burnmr"}, - {"n":"Rochdale","t":"ROCHDAL","c":"RCD","s":"rochdale rcd rochdal"}, - {"n":"Oldham Mumps","t":"OLDMUM","c":"OMM","s":"oldham mumps omm oldmum"}, - {"n":"Ashton-under-Lyne","t":"ASHTON","c":"AHN","s":"ashton under lyne ahn ashton"}, - {"n":"Stalybridge","t":"STALYBG","c":"SYB","s":"stalybridge syb stalybg"}, - {"n":"Macclesfield","t":"MACCLFD","c":"MAC","s":"macclesfield mac macclfd"}, - {"n":"Congleton","t":"CONGLTN","c":"CNG","s":"congleton cng conglt"}, - {"n":"Stoke-on-Trent","t":"STOKETR","c":"SOT","s":"stoke on trent sot stoketr"}, - {"n":"Stafford","t":"STAFFRD","c":"STA","s":"stafford sta staffrd"}, - {"n":"Tamworth","t":"TAMWTH","c":"TAM","s":"tamworth tam tamwth"}, - {"n":"Nuneaton","t":"NUNEATN","c":"NUN","s":"nuneaton nun nuneatn"}, - {"n":"Coventry","t":"COVNTRY","c":"COV","s":"coventry cov covntry"}, - {"n":"Rugby","t":"RUGBY","c":"RUG","s":"rugby rug"}, - {"n":"Milton Keynes Central","t":"MKCEN","c":"MKC","s":"milton keynes central mkc mkcen"}, - {"n":"Birmingham Washwood Heath Junction","t":"BWHJCT","c":"","s":"birmingham washwood heath junction bwhjct"}, - {"n":"Manchester Trafford Park Yard","t":"MTRYD","c":"","s":"manchester trafford park yard mtryd"}, - {"n":"London Willesden Junction","t":"WLSDJCT","c":"","s":"london willesden junction wlsdjct"}, - {"n":"Leeds Neville Hill Depot","t":"NVHLDP","c":"","s":"leeds neville hill depot nvhldp"}, - {"n":"York Holgate Junction","t":"YHGJCT","c":"","s":"york holgate junction yhgjct"}, - {"n":"Crewe Basford Hall Junction","t":"CBHJCT","c":"","s":"crewe basford hall junction cbhjct"}, - {"n":"Doncaster Decoy Sidings","t":"DCDSID","c":"","s":"doncaster decoy sidings dcdsid"}, - {"n":"Liverpool Edge Hill Yard","t":"LEHYD","c":"","s":"liverpool edge hill yard lehyd"}, - {"n":"Bristol East Junction","t":"BREJCT","c":"","s":"bristol east junction brejct"}, - {"n":"Glasgow Polmadie Depot","t":"GLPDEP","c":"","s":"glasgow polmadie depot glpdep"}, - {"n":"Newcastle Manors Junction","t":"NCMJCT","c":"","s":"newcastle manors junction ncmjct"}, - {"n":"Edinburgh Haymarket Sidings","t":"EHSID","c":"","s":"edinburgh haymarket sidings ehsid"}, - {"n":"Reading South Junction","t":"RDSJCT","c":"","s":"reading south junction rdsjct"}, - {"n":"Oxford Rewley Road Depot","t":"OXRDEP","c":"","s":"oxford rewley road depot oxrdep"}, - {"n":"Cambridge Coldham Lane Junction","t":"CCLJCT","c":"","s":"cambridge coldham lane junction ccljct"}, - {"n":"Watford North Junction","t":"WFNJCT","c":"","s":"watford north junction wfnjct"}, - {"n":"Luton Airport Sidings","t":"LUTSID","c":"","s":"luton airport sidings lutsid"}, - {"n":"Stevenage Hitchin Junction","t":"STHJC","c":"","s":"stevenage hitchin junction sthjc"}, - {"n":"Chelmsford New Hall Junction","t":"CHNJCT","c":"","s":"chelmsford new hall junction chnjct"}, - {"n":"","t":"BPWY532","c":"","s":"bpwy532"}, - {"n":"Ipswich Derby Road Depot","t":"IPDRDP","c":"","s":"ipswich derby road depot ipdrdp"}, - {"n":"Rhoose Cardiff International Airport","c":"RIA","t":"RHOOSE","s":"rhoose cardiff international airport ria"}, - {"n":"Southampton Airport Parkway","c":"SOA","t":"SOTAPT","s":"southampton airport parkway soa sotapt"} -] \ No newline at end of file