Add NearestStations Card & Location monitor
This commit is contained in:
54
package-lock.json
generated
54
package-lock.json
generated
@@ -13,7 +13,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^2.0.2",
|
"@eslint/compat": "^2.0.2",
|
||||||
"@eslint/js": "^9.39.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",
|
"@playwright/test": "^1.58.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.50.2",
|
"@sveltejs/kit": "^2.50.2",
|
||||||
@@ -780,20 +780,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@owlboard/api-schema-types": {
|
"node_modules/@owlboard/api-schema-types": {
|
||||||
"version": "3.0.2-alpha2",
|
"version": "3.0.2-alpha3",
|
||||||
"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",
|
"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-KyX4QcOCzVqYpiXY+WfhM1soXduMt2ldG6JSBK2WBxXWokS+keZshOHWHGTZvPLoZEWsuPznMAdzytI03/D3Ag==",
|
"integrity": "sha512-3NFP21QdSfjziwlGQixlNnUWC55HlKZGyWANIOimWu0FZejWQWExJiaAVfb6m3Sbv+zvQMu3B8mzcMCcGadZCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@owlboard/owlboard-ts": {
|
"node_modules/@owlboard/owlboard-ts": {
|
||||||
"version": "3.0.0-dev.20260325T1023",
|
"version": "3.0.0-dev.202603302258",
|
||||||
"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",
|
"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-h5jAO9MYmYUToXvw3gCohUbG3oVl7h+PKrZ94I1NahXOLEd+CaQzXbXk5+KCsnojgkqf0I1FavaBbADgb2ZKkQ==",
|
"integrity": "sha512-uJRoahtqnkmkPg8QsIWhtxhAibWY8xG25fhjzzdDFNE/2+uCj1ev5iD+t0O1dkE7ic3yTcMZqDitEspW216bjw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"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": {
|
"node_modules/@playwright/test": {
|
||||||
@@ -1558,9 +1559,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1881,9 +1882,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2664,6 +2665,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -2901,9 +2909,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3028,9 +3036,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-load-config/node_modules/yaml": {
|
"node_modules/postcss-load-config/node_modules/yaml": {
|
||||||
"version": "1.10.2",
|
"version": "1.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
|
||||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3843,9 +3851,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.8.2",
|
"version": "2.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^2.0.2",
|
"@eslint/compat": "^2.0.2",
|
||||||
"@eslint/js": "^9.39.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",
|
"@playwright/test": "^1.58.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.50.2",
|
"@sveltejs/kit": "^2.50.2",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
return LOCATIONS.data
|
return LOCATIONS.data
|
||||||
.filter((r) => tokens.every((t) => r.s.includes(t)))
|
.filter((r) => tokens.every((t) => r.s.includes(t)))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
|
||||||
// Priority One - Exact CRS Match
|
// Priority One - Exact CRS Match
|
||||||
const aExactCrs = a.c?.toLowerCase() === lowerQuery;
|
const aExactCrs = a.c?.toLowerCase() === lowerQuery;
|
||||||
const bExactCrs = b.c?.toLowerCase() === lowerQuery;
|
const bExactCrs = b.c?.toLowerCase() === lowerQuery;
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
|
|
||||||
// Priority Two - 'Stations' with CRS
|
// 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;
|
if (!a.c & !!b.c) return 1;
|
||||||
|
|
||||||
// Alphabetical Sort
|
// Alphabetical Sort
|
||||||
return a.n.localeCompare(b.n);
|
return a.n.localeCompare(b.n);
|
||||||
|
|||||||
@@ -1,53 +1,56 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
interface Props {
|
interface Props {
|
||||||
toc: string;
|
toc: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { toc }: Props = $props();
|
||||||
toc
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let code = $derived(toc.toUpperCase());
|
let code = $derived(toc.toUpperCase());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="toc-container {code}">
|
<div class="toc-container {code}">
|
||||||
{code}
|
{code}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.toc-container {
|
.toc-container {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.GW { /* Great Western Railway */
|
.GW {
|
||||||
background: #004225;
|
/* Great Western Railway */
|
||||||
color: #E2E2E2;
|
background: #004225;
|
||||||
}
|
color: #e2e2e2;
|
||||||
|
}
|
||||||
|
|
||||||
.GR { /* LNER */
|
.GR {
|
||||||
background-color: #C00000;
|
/* LNER */
|
||||||
color: #FFFFFF;
|
background-color: #c00000;
|
||||||
}
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.VT { /* Avanti West Coast */
|
.VT {
|
||||||
background-color: #004354;
|
/* Avanti West Coast */
|
||||||
color: #FFFFFF;
|
background-color: #004354;
|
||||||
}
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.SW { /* South Western Railway */
|
.SW {
|
||||||
background-color: #2A3389;
|
/* South Western Railway */
|
||||||
color: #FFFFFF;
|
background-color: #2a3389;
|
||||||
}
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.XC { /* CrossCountry */
|
.XC {
|
||||||
background-color: #660000;
|
/* CrossCountry */
|
||||||
color: #E4D5B1;
|
background-color: #660000;
|
||||||
}
|
color: #e4d5b1;
|
||||||
</style>
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
69
src/lib/components/ui/cards/NearbyStationsCard.svelte
Normal file
69
src/lib/components/ui/cards/NearbyStationsCard.svelte
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
||||||
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
|
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
|
||||||
|
import { nearestStationsState } from '$lib/geohash.svelte';
|
||||||
|
|
||||||
|
const flipDuration = 300;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseCard header={'Nearby Stations'}>
|
||||||
|
<div class="card-content">
|
||||||
|
{#if nearestStationsState.error && nearestStationsState.list.length === 0}
|
||||||
|
<p class="msg">{nearestStationsState.error}</p>
|
||||||
|
{:else if nearestStationsState.loading && nearestStationsState.list.length === 0}
|
||||||
|
<p class="msg">Locating stations...</p>
|
||||||
|
{:else}
|
||||||
|
<div class="stations-flex">
|
||||||
|
{#each nearestStationsState.list as station (station.c)}
|
||||||
|
<div
|
||||||
|
class="btn-container"
|
||||||
|
animate:flip={{ duration: flipDuration }}
|
||||||
|
in:fade={{ duration: 200, delay: 100 }}
|
||||||
|
out:fade={{ duration: 150 }}
|
||||||
|
>
|
||||||
|
<Button href={`/board?loc=${station.c}`}
|
||||||
|
><span class="stn-name">{station.n}</span></Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</BaseCard>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card-content {
|
||||||
|
text-align: center;
|
||||||
|
width: 90%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stations-flex {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-container {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stn-name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,28 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
||||||
import Textbox from '$lib/components/ui/Textbox.svelte';
|
import Textbox from '$lib/components/ui/Textbox.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
|
|
||||||
let { onsearch }: { onsearch: (c: string) => void } = $props();
|
let { onsearch }: { onsearch: (c: string) => void } = $props();
|
||||||
|
|
||||||
let codeValue = $state('');
|
let codeValue = $state('');
|
||||||
|
|
||||||
function resetValues(): void {
|
function resetValues(): void {
|
||||||
codeValue = '';
|
codeValue = '';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseCard header={'Find by Code'}>
|
<BaseCard header={'Find by Code'}>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="textbox-container">
|
<div class="textbox-container">
|
||||||
<div class="textbox-item-wrapper">
|
<div class="textbox-item-wrapper">
|
||||||
<Textbox placeholder={"Code"} uppercase={true} type={'number'} max={9999} bind:value={codeValue} />
|
<Textbox
|
||||||
</div>
|
placeholder={'Code'}
|
||||||
</div>
|
uppercase={true}
|
||||||
<div class="button-wrapper">
|
type={'number'}
|
||||||
<Button onclick={() => onsearch(codeValue.toString())}>Search</Button>
|
max={9999}
|
||||||
<Button onclick={resetValues}>Reset</Button>
|
bind:value={codeValue}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-wrapper">
|
||||||
|
<Button onclick={() => onsearch(codeValue.toString())}>Search</Button>
|
||||||
|
<Button onclick={resetValues}>Reset</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
|
||||||
@@ -34,21 +40,21 @@ function resetValues(): void {
|
|||||||
padding: 10px 0 10px 0;
|
padding: 10px 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textbox-container {
|
.textbox-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4rem;
|
gap: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textbox-item-wrapper {
|
.textbox-item-wrapper {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-wrapper {
|
.button-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
||||||
import Textbox from '$lib/components/ui/Textbox.svelte';
|
import Textbox from '$lib/components/ui/Textbox.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
|
|
||||||
let { onsearch }: { onsearch: (s: string, e: string) => void } = $props();
|
let { onsearch }: { onsearch: (s: string, e: string) => void } = $props();
|
||||||
|
|
||||||
|
let startValue = $state('');
|
||||||
|
let endValue = $state('');
|
||||||
|
|
||||||
let startValue = $state('');
|
function resetValues(): void {
|
||||||
let endValue = $state('');
|
startValue = '';
|
||||||
|
endValue = '';
|
||||||
function resetValues(): void {
|
}
|
||||||
startValue = '';
|
|
||||||
endValue = '';
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseCard header={'Find by Start/End CRS'}>
|
<BaseCard header={'Find by Start/End CRS'}>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="textbox-container">
|
<div class="textbox-container">
|
||||||
<div class="textbox-item-wrapper">
|
<div class="textbox-item-wrapper">
|
||||||
<Textbox placeholder={"Start"} uppercase={true} maxLength={3} bind:value={startValue} />
|
<Textbox placeholder={'Start'} uppercase={true} maxLength={3} bind:value={startValue} />
|
||||||
</div>
|
</div>
|
||||||
<div class="textbox-item-wrapper">
|
<div class="textbox-item-wrapper">
|
||||||
<Textbox placeholder={"End"} uppercase={true} maxLength={3} bind:value={endValue} />
|
<Textbox placeholder={'End'} uppercase={true} maxLength={3} bind:value={endValue} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-wrapper">
|
<div class="button-wrapper">
|
||||||
<Button onclick={() => onsearch(startValue, endValue)}>Search</Button>
|
<Button onclick={() => onsearch(startValue, endValue)}>Search</Button>
|
||||||
<Button onclick={resetValues}>Reset</Button>
|
<Button onclick={resetValues}>Reset</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
|
||||||
@@ -40,21 +39,21 @@ function resetValues(): void {
|
|||||||
padding: 10px 0 10px 0;
|
padding: 10px 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textbox-container {
|
.textbox-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4rem;
|
gap: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textbox-item-wrapper {
|
.textbox-item-wrapper {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-wrapper {
|
.button-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
90
src/lib/geohash.svelte.ts
Normal file
90
src/lib/geohash.svelte.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { OwlClient, ValidationError, ApiError } from './owlClient';
|
||||||
|
import type { ApiStationsNearestStations } from '@owlboard/owlboard-ts';
|
||||||
|
|
||||||
|
class NearestStationsState {
|
||||||
|
list = $state<ApiStationsNearestStations.StationsNearestStations[]>([]);
|
||||||
|
currentHash = $state('');
|
||||||
|
loading = $state(true);
|
||||||
|
error = $state<string | null>(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();
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OwlClient } from "./owlClient";
|
import { OwlClient } from './owlClient';
|
||||||
import type { ApiLocationFilter } from '@owlboard/owlboard-ts'
|
import type { ApiLocationFilter } from '@owlboard/owlboard-ts';
|
||||||
|
|
||||||
class LocationStore {
|
class LocationStore {
|
||||||
data = $state<ApiLocationFilter.LocationFilterObject[]>([]);
|
data = $state<ApiLocationFilter.LocationFilterObject[]>([]);
|
||||||
@@ -9,7 +9,7 @@ class LocationStore {
|
|||||||
if (this.loaded) return;
|
if (this.loaded) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fetch = await OwlClient.locationFilter.getLocationFilterData()
|
const fetch = await OwlClient.locationFilter.getLocationFilterData();
|
||||||
this.data = fetch.data;
|
this.data = fetch.data;
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { OwlBoardClient, ValidationError, ApiError } from "@owlboard/owlboard-ts";
|
import { OwlBoardClient, ValidationError, ApiError } from '@owlboard/owlboard-ts';
|
||||||
import { browser, dev } from "$app/environment";
|
import { browser, dev } from '$app/environment';
|
||||||
|
|
||||||
// Import the runes containing the API Key config Here...
|
// Import the runes containing the API Key config Here...
|
||||||
|
|
||||||
const baseUrl: string = browser ? window.location.origin : '';
|
const baseUrl: string = browser ? window.location.origin : '';
|
||||||
|
|
||||||
const getBaseUrl = () => {
|
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(
|
export const OwlClient = new OwlBoardClient(
|
||||||
getBaseUrl(),
|
getBaseUrl()
|
||||||
// API Key Here when ready!!!
|
// API Key Here when ready!!!
|
||||||
)
|
);
|
||||||
|
|
||||||
export { ValidationError, ApiError };
|
export { ValidationError, ApiError };
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { LOCATIONS } from '$lib/locations-object.svelte';
|
import { LOCATIONS } from '$lib/locations-object.svelte';
|
||||||
|
import { nearestStationsState } from '$lib/geohash.svelte';
|
||||||
|
|
||||||
import '$lib/global.css';
|
import '$lib/global.css';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LocationBoardCard from '$lib/components/ui/cards/LocationBoardCard.svelte';
|
import LocationBoardCard from '$lib/components/ui/cards/LocationBoardCard.svelte';
|
||||||
|
import NearbyStationsCard from '$lib/components/ui/cards/NearbyStationsCard.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card-container">
|
<div class="card-container">
|
||||||
<LocationBoardCard />
|
<LocationBoardCard />
|
||||||
|
<NearbyStationsCard />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<p class="amble">
|
<p class="amble">
|
||||||
Why OwlBoard? The name was chosen as an evolution of its predecessor, 'Athena'; owls are
|
Why OwlBoard? The name was chosen as an evolution of its predecessor, 'Athena'; owls are
|
||||||
associated with the equivalent Roman Goddess - Minerva - as well as with wisdom. This also links to Bath, where the
|
associated with the equivalent Roman Goddess - Minerva - as well as with wisdom. This also links
|
||||||
app has been built and is run, relating to the 'Minerva Owl' sculpture trail in the city, with
|
to Bath, where the app has been built and is run, relating to the 'Minerva Owl' sculpture trail
|
||||||
many of the sculptures still in the area.
|
in the city, with many of the sculptures still in the area.
|
||||||
</p>
|
</p>
|
||||||
<p class="opensource">
|
<p class="opensource">
|
||||||
Some components that combine to form OwlBoard are open-source, see the <a
|
Some components that combine to form OwlBoard are open-source, see the <a
|
||||||
|
|||||||
@@ -1,98 +1,102 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PisStartEndCard from '$lib/components/ui/cards/pis/PisStartEndCard.svelte';
|
import PisStartEndCard from '$lib/components/ui/cards/pis/PisStartEndCard.svelte';
|
||||||
import PisCode from '$lib/components/ui/cards/pis/PisCode.svelte';
|
import PisCode from '$lib/components/ui/cards/pis/PisCode.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import type { ApiPisObject } from '@owlboard/owlboard-ts';
|
import type { ApiPisObject } from '@owlboard/owlboard-ts';
|
||||||
import { OwlClient, ApiError, ValidationError } from '$lib/owlClient';
|
import { OwlClient, ApiError, ValidationError } from '$lib/owlClient';
|
||||||
import TocStyle from '$lib/components/ui/TocStyle.svelte';
|
import TocStyle from '$lib/components/ui/TocStyle.svelte';
|
||||||
|
|
||||||
let results = $state<ApiPisObject.PisObjects[]>([]);
|
let results = $state<ApiPisObject.PisObjects[]>([]);
|
||||||
let resultsLoaded = $state<boolean>(false);
|
let resultsLoaded = $state<boolean>(false);
|
||||||
let errorState = $state<{status: number, message: string} | null>(null);
|
let errorState = $state<{ status: number; message: string } | null>(null);
|
||||||
|
|
||||||
async function handleStartEndSearch(start: string, end: string): Promise<void> {
|
async function handleStartEndSearch(start: string, end: string): Promise<void> {
|
||||||
console.log(`PIS Search: ${start}-${end}`);
|
console.log(`PIS Search: ${start}-${end}`);
|
||||||
errorState = null;
|
errorState = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await OwlClient.pis.getByStartEndCrs(start, end);
|
const response = await OwlClient.pis.getByStartEndCrs(start, end);
|
||||||
results = response.data || [];
|
results = response.data || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ValidationError) {
|
if (e instanceof ValidationError) {
|
||||||
errorState = { status: 400, message: e.message };
|
errorState = { status: 400, message: e.message };
|
||||||
} else if (e instanceof ApiError) {
|
} else if (e instanceof ApiError) {
|
||||||
console.log(e)
|
console.log(e);
|
||||||
errorState = { status: 20, message: e.message };
|
errorState = { status: 20, message: e.message };
|
||||||
} else {
|
} else {
|
||||||
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
|
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
resultsLoaded = true;
|
resultsLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCodeSearch(code: string) {
|
async function handleCodeSearch(code: string) {
|
||||||
console.log(`PIS Search: ${code}`);
|
console.log(`PIS Search: ${code}`);
|
||||||
errorState = null;
|
errorState = null;
|
||||||
try {
|
try {
|
||||||
const response = await OwlClient.pis.getByCode(code);
|
const response = await OwlClient.pis.getByCode(code);
|
||||||
results = response.data || []
|
results = response.data || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ValidationError) {
|
if (e instanceof ValidationError) {
|
||||||
errorState = { status: 400, message: e.message };
|
errorState = { status: 400, message: e.message };
|
||||||
} else if (e instanceof ApiError) {
|
} else if (e instanceof ApiError) {
|
||||||
console.log(e)
|
console.log(e);
|
||||||
errorState = { status: 20, message: e.message };
|
errorState = { status: 20, message: e.message };
|
||||||
} else {
|
} else {
|
||||||
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
|
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
resultsLoaded = true;
|
resultsLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearResults() {
|
function clearResults() {
|
||||||
console.log('Clearing Results');
|
console.log('Clearing Results');
|
||||||
resultsLoaded = false;
|
resultsLoaded = false;
|
||||||
results = [];
|
results = [];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !resultsLoaded}
|
{#if !resultsLoaded}
|
||||||
<div class="card-container">
|
<div class="card-container">
|
||||||
<PisStartEndCard onsearch={handleStartEndSearch} />
|
<PisStartEndCard onsearch={handleStartEndSearch} />
|
||||||
<PisCode onsearch={handleCodeSearch} />
|
<PisCode onsearch={handleCodeSearch} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="result-container">
|
<div class="result-container">
|
||||||
{#if errorState}
|
{#if errorState}
|
||||||
<span class="errCode">Error: {errorState.status}</span>
|
<span class="errCode">Error: {errorState.status}</span>
|
||||||
<span class="errMsg">{errorState.message}</span>
|
<span class="errMsg">{errorState.message}</span>
|
||||||
{:else}
|
{:else if results.length}
|
||||||
{#if results.length}
|
<h2 class="result-title">
|
||||||
<h2 class="result-title">{results.length} Result{#if results.length > 1}s{/if} found</h2>
|
{results.length} Result{#if results.length > 1}s{/if} found
|
||||||
<table class="result-table">
|
</h2>
|
||||||
<thead>
|
<table class="result-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th style="width:16%">TOC</th>
|
<tr>
|
||||||
<th style="width:14%">Code</th>
|
<th style="width:16%">TOC</th>
|
||||||
<th style="width:70%">Locations</th>
|
<th style="width:14%">Code</th>
|
||||||
</tr></thead>
|
<th style="width:70%">Locations</th>
|
||||||
{#each results as result}
|
</tr></thead
|
||||||
<tbody><tr>
|
>
|
||||||
<td><TocStyle toc={result.toc || ""} /></td>
|
{#each results as result}
|
||||||
<td>{result.code}</td>
|
<tbody
|
||||||
<td class="locations-row">{result.crsStops?.join(' ') || ''}</td>
|
><tr>
|
||||||
</tr></tbody>
|
<td><TocStyle toc={result.toc || ''} /></td>
|
||||||
{/each}
|
<td>{result.code}</td>
|
||||||
</table>
|
<td class="locations-row">{result.crsStops?.join(' ') || ''}</td>
|
||||||
{:else}
|
</tr></tbody
|
||||||
<p class="no-results">No matching results</p>
|
>
|
||||||
{/if}
|
{/each}
|
||||||
{/if}
|
</table>
|
||||||
<div class="reset-button-container">
|
{:else}
|
||||||
<Button onclick={clearResults}>Reset</Button>
|
<p class="no-results">No matching results</p>
|
||||||
</div> </div>
|
{/if}
|
||||||
|
<div class="reset-button-container">
|
||||||
|
<Button onclick={clearResults}>Reset</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -105,54 +109,54 @@
|
|||||||
padding: 20px 10px;
|
padding: 20px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-container {
|
.result-container {
|
||||||
font-family: 'URW Gothic', sans-serif;
|
font-family: 'URW Gothic', sans-serif;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--color-accent);
|
background: var(--color-accent);
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
padding: 20px 0 20px 0;
|
padding: 20px 0 20px 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
box-shadow: var(--shadow-std);
|
box-shadow: var(--shadow-std);
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-title {
|
.result-title {
|
||||||
color: var(--color-brand);
|
color: var(--color-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-table {
|
.result-table {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0 20px;
|
border-spacing: 0 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.locations-row {
|
.locations-row {
|
||||||
font-family:'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errCode {
|
.errCode {
|
||||||
color: rgb(255, 54, 54);
|
color: rgb(255, 54, 54);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-button-container {
|
.reset-button-container {
|
||||||
padding: 20px 0 3px 0;
|
padding: 20px 0 3px 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ export const load = () => {
|
|||||||
return {
|
return {
|
||||||
title: 'PIS Codes'
|
title: 'PIS Codes'
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"}
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user