Compare commits
8 Commits
v3.0.0-dev
...
v3.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 777519ff5d | |||
| 4a969e626c | |||
| 3e1b7ea5d5 | |||
| fd213d6340 | |||
| 3eceddf20a | |||
| 1d461780ab | |||
| ec4dd5dd3b | |||
| a7c244171c |
54
package-lock.json
generated
54
package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@owlboard/owlboard-ts": "^3.0.0-dev.20260319T2004",
|
||||
"@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.1-alpha3",
|
||||
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fapi-schema-types/-/3.0.1-alpha3/api-schema-types-3.0.1-alpha3.tgz",
|
||||
"integrity": "sha512-5CVm1k/C++/VrtAw4NkvclDunH+RmYLnDZZMSWTM1mm+WlEVnmD+MVnTgC/FhcsAmsNHV8swm66RCqkCuhbOnA==",
|
||||
"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.20260319T2004",
|
||||
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fowlboard-ts/-/3.0.0-dev.20260319T2004/owlboard-ts-3.0.0-dev.20260319t2004.tgz",
|
||||
"integrity": "sha512-pphq1/l/8eOH4C0O7ocwBOUzt0HkCWGUlhy1itzKnQbmog7oPUEdyaxzS4Evw8onLsxZwkyqsLAyK7okYi+4XA==",
|
||||
"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.1-alpha3"
|
||||
"@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,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@owlboard/owlboard-ts": "^3.0.0-dev.20260319T2004",
|
||||
"@owlboard/owlboard-ts": "^3.0.0-dev.202603302258",
|
||||
"@playwright/test": "^1.58.1",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { LOCATIONS } from '$lib/locations-object.svelte.ts';
|
||||
import { LOCATIONS } from '$lib/locations-object.svelte';
|
||||
import type { ApiLocationFilter } from '@owlboard/api-schema-types';
|
||||
|
||||
let { value = $bindable() } = $props();
|
||||
|
||||
@@ -26,13 +27,15 @@
|
||||
return LOCATIONS.data
|
||||
.filter((r) => tokens.every((t) => r.s.includes(t)))
|
||||
.sort((a, b) => {
|
||||
// Check if query matches CRS
|
||||
const aIsCrs = a.c?.toLowerCase() === lowerQuery;
|
||||
const bIsCrs = b.c?.toLowerCase() === lowerQuery;
|
||||
// Priority One - Exact CRS Match
|
||||
const aExactCrs = a.c?.toLowerCase() === lowerQuery;
|
||||
const bExactCrs = b.c?.toLowerCase() === lowerQuery;
|
||||
if (aExactCrs && !bExactCrs) return -1;
|
||||
if (!aExactCrs && bExactCrs) return 1;
|
||||
|
||||
// Sort matching CRS first
|
||||
if (aIsCrs && !bIsCrs) return -1;
|
||||
if (!aIsCrs && bIsCrs) return 1;
|
||||
// Priority Two - 'Stations' with CRS
|
||||
if (!!a.c && !b.c) return -1;
|
||||
if (!a.c & !!b.c) return 1;
|
||||
|
||||
// Alphabetical Sort
|
||||
return a.n.localeCompare(b.n);
|
||||
@@ -59,7 +62,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
function choose(loc: LocationRecord) {
|
||||
function choose(loc: ApiLocationFilter.LocationFilterObject) {
|
||||
showResults = false;
|
||||
selectedIndex = -1;
|
||||
value = '';
|
||||
@@ -195,6 +198,7 @@
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.tiploc {
|
||||
|
||||
56
src/lib/components/ui/TocStyle.svelte
Normal file
56
src/lib/components/ui/TocStyle.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
toc: string;
|
||||
}
|
||||
|
||||
let { toc }: Props = $props();
|
||||
|
||||
let code = $derived(toc.toUpperCase());
|
||||
</script>
|
||||
|
||||
<div class="toc-container {code}">
|
||||
{code}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toc-container {
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 8px;
|
||||
font-weight: 800;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.GW {
|
||||
/* Great Western Railway */
|
||||
background: #004225;
|
||||
color: #e2e2e2;
|
||||
}
|
||||
|
||||
.GR {
|
||||
/* LNER */
|
||||
background-color: #c00000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.VT {
|
||||
/* Avanti West Coast */
|
||||
background-color: #004354;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.SW {
|
||||
/* South Western Railway */
|
||||
background-color: #2a3389;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.XC {
|
||||
/* CrossCountry */
|
||||
background-color: #660000;
|
||||
color: #e4d5b1;
|
||||
}
|
||||
</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">
|
||||
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
||||
import Textbox from '$lib/components/ui/Textbox.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
||||
import Textbox from '$lib/components/ui/Textbox.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 {
|
||||
codeValue = '';
|
||||
}
|
||||
function resetValues(): void {
|
||||
codeValue = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<BaseCard header={'Find by Code'}>
|
||||
<div class="card-content">
|
||||
<div class="textbox-container">
|
||||
<div class="textbox-item-wrapper">
|
||||
<Textbox placeholder={"Code"} uppercase={true} type={'number'} max={9999} bind:value={codeValue} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-wrapper">
|
||||
<Button onclick={() => onsearch(codeValue.toString())}>Search</Button>
|
||||
<Button onclick={resetValues}>Reset</Button>
|
||||
</div>
|
||||
<div class="textbox-container">
|
||||
<div class="textbox-item-wrapper">
|
||||
<Textbox
|
||||
placeholder={'Code'}
|
||||
uppercase={true}
|
||||
type={'number'}
|
||||
max={9999}
|
||||
bind:value={codeValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-wrapper">
|
||||
<Button onclick={() => onsearch(codeValue.toString())}>Search</Button>
|
||||
<Button onclick={resetValues}>Reset</Button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
<script lang="ts">
|
||||
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
||||
import Textbox from '$lib/components/ui/Textbox.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
||||
import Textbox from '$lib/components/ui/Textbox.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('');
|
||||
let endValue = $state('');
|
||||
|
||||
function resetValues(): void {
|
||||
startValue = '';
|
||||
endValue = '';
|
||||
}
|
||||
function resetValues(): void {
|
||||
startValue = '';
|
||||
endValue = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<BaseCard header={'Find by Start/End CRS'}>
|
||||
<div class="card-content">
|
||||
<div class="textbox-container">
|
||||
<div class="textbox-item-wrapper">
|
||||
<Textbox placeholder={"Start"} uppercase={true} maxLength={3} bind:value={startValue} />
|
||||
</div>
|
||||
<div class="textbox-item-wrapper">
|
||||
<Textbox placeholder={"End"} uppercase={true} maxLength={3} bind:value={endValue} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-wrapper">
|
||||
<Button onclick={() => onsearch(startValue, endValue)}>Search</Button>
|
||||
<Button onclick={resetValues}>Reset</Button>
|
||||
</div>
|
||||
<div class="textbox-container">
|
||||
<div class="textbox-item-wrapper">
|
||||
<Textbox placeholder={'Start'} uppercase={true} maxLength={3} bind:value={startValue} />
|
||||
</div>
|
||||
<div class="textbox-item-wrapper">
|
||||
<Textbox placeholder={'End'} uppercase={true} maxLength={3} bind:value={endValue} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-wrapper">
|
||||
<Button onclick={() => onsearch(startValue, endValue)}>Search</Button>
|
||||
<Button onclick={resetValues}>Reset</Button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 15px;
|
||||
}
|
||||
</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,27 +1,23 @@
|
||||
interface LocationRecord {
|
||||
n: string; // name
|
||||
t: string; // tiploc
|
||||
c?: string; // crs
|
||||
s: string; // search string
|
||||
}
|
||||
import { OwlClient } from './owlClient';
|
||||
import type { ApiLocationFilter } from '@owlboard/owlboard-ts';
|
||||
|
||||
class LocationStore {
|
||||
data = $state<LocationRecord[]>([]);
|
||||
data = $state<ApiLocationFilter.LocationFilterObject[]>([]);
|
||||
loaded = $state(false);
|
||||
|
||||
async init(fetcher = fetch) {
|
||||
async init() {
|
||||
if (this.loaded) return;
|
||||
|
||||
try {
|
||||
const res = await fetcher('/api/tiplocs');
|
||||
this.data = await res.json();
|
||||
const fetch = await OwlClient.locationFilter.getLocationFilterData();
|
||||
this.data = fetch.data;
|
||||
this.loaded = true;
|
||||
} catch (err) {
|
||||
console.error('Failed to load locations', err);
|
||||
}
|
||||
}
|
||||
|
||||
find(id: string | null): LocationRecord | undefined {
|
||||
find(id: string | null): ApiLocationFilter.LocationFilterObject | undefined {
|
||||
if (!id) return undefined;
|
||||
|
||||
const query = id.toUpperCase().trim();
|
||||
|
||||
@@ -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 };
|
||||
export { ValidationError, ApiError };
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { slide, fade } from 'svelte/transition';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { LOCATIONS } from '$lib/locations-object.svelte.ts';
|
||||
import { LOCATIONS } from '$lib/locations-object.svelte';
|
||||
import { nearestStationsState } from '$lib/geohash.svelte';
|
||||
|
||||
import '$lib/global.css';
|
||||
|
||||
@@ -13,7 +14,7 @@
|
||||
|
||||
import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte';
|
||||
|
||||
onMount(() => LOCATIONS.init(fetch));
|
||||
onMount(() => LOCATIONS.init());
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
@@ -167,6 +168,7 @@
|
||||
margin-left: 5px;
|
||||
padding-bottom: 2px;
|
||||
color: var(--color-title);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
header,
|
||||
nav {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import LocationBoardCard from '$lib/components/ui/cards/LocationBoardCard.svelte';
|
||||
import NearbyStationsCard from '$lib/components/ui/cards/NearbyStationsCard.svelte';
|
||||
</script>
|
||||
|
||||
<div class="card-container">
|
||||
<LocationBoardCard />
|
||||
<NearbyStationsCard />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
</p>
|
||||
<p class="amble">
|
||||
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
|
||||
app has been built and is run, relating to the 'Minerva Owl' sculpture trail in the city, with
|
||||
many of the sculptures still in the area.
|
||||
associated with the equivalent Roman Goddess - Minerva - as well as with wisdom. This also links
|
||||
to Bath, where the app has been built and is run, relating to the 'Minerva Owl' sculpture trail
|
||||
in the city, with many of the sculptures still in the area.
|
||||
</p>
|
||||
<p class="opensource">
|
||||
Some components that combine to form OwlBoard are open-source, see the <a
|
||||
|
||||
@@ -6,7 +6,7 @@ export const load: PageLoad = async ({ url }) => {
|
||||
const locId = url.searchParams.get('loc');
|
||||
|
||||
if (!LOCATIONS.loaded) {
|
||||
await LOCATIONS.init(fetch);
|
||||
await LOCATIONS.init();
|
||||
}
|
||||
|
||||
let title: string = '';
|
||||
|
||||
@@ -1,86 +1,102 @@
|
||||
<script lang="ts">
|
||||
import PisStartEndCard from '$lib/components/ui/cards/pis/PisStartEndCard.svelte';
|
||||
import PisCode from '$lib/components/ui/cards/pis/PisCode.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import type { PisObjects } from '@owlboard/api-schema-types';
|
||||
import { OwlClient, ApiError, ValidationError } from '$lib/owlClient';
|
||||
import PisStartEndCard from '$lib/components/ui/cards/pis/PisStartEndCard.svelte';
|
||||
import PisCode from '$lib/components/ui/cards/pis/PisCode.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import type { ApiPisObject } from '@owlboard/owlboard-ts';
|
||||
import { OwlClient, ApiError, ValidationError } from '$lib/owlClient';
|
||||
import TocStyle from '$lib/components/ui/TocStyle.svelte';
|
||||
|
||||
let results = $state<ApiPisObject.PisObjects[]>([]);
|
||||
let resultsLoaded = $state<boolean>(false);
|
||||
let errorState = $state<{ status: number; message: string } | null>(null);
|
||||
|
||||
let results = $state<PisObjects[]>([]);
|
||||
let resultsLoaded = $state<boolean>(false);
|
||||
let errorState = $state<{status: number, message: string} | null>(null);
|
||||
async function handleStartEndSearch(start: string, end: string): Promise<void> {
|
||||
console.log(`PIS Search: ${start}-${end}`);
|
||||
errorState = null;
|
||||
|
||||
async function handleStartEndSearch(start: string, end: string): Promise<void> {
|
||||
console.log(`PIS Search: ${start}-${end}`);
|
||||
errorState = null;
|
||||
try {
|
||||
const response = await OwlClient.pis.getByStartEndCrs(start, end);
|
||||
results = response.data || [];
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
errorState = { status: 400, message: e.message };
|
||||
} else if (e instanceof ApiError) {
|
||||
console.log(e);
|
||||
errorState = { status: 20, message: e.message };
|
||||
} else {
|
||||
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
|
||||
}
|
||||
} finally {
|
||||
resultsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await OwlClient.pis.getByStartEndCrs(start, end);
|
||||
results = await response.data || [];
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
errorState = { status: 400, message: e.message };
|
||||
} else if (e instanceof ApiError) {
|
||||
console.log(e)
|
||||
errorState = { status: 20, message: e.message };
|
||||
} else {
|
||||
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
|
||||
}
|
||||
} finally {
|
||||
resultsLoaded = true;
|
||||
}
|
||||
}
|
||||
async function handleCodeSearch(code: string) {
|
||||
console.log(`PIS Search: ${code}`);
|
||||
errorState = null;
|
||||
try {
|
||||
const response = await OwlClient.pis.getByCode(code);
|
||||
results = response.data || [];
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
errorState = { status: 400, message: e.message };
|
||||
} else if (e instanceof ApiError) {
|
||||
console.log(e);
|
||||
errorState = { status: 20, message: e.message };
|
||||
} else {
|
||||
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
|
||||
}
|
||||
} finally {
|
||||
resultsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCodeSearch(code: string) {
|
||||
console.log(`PIS Search: ${code}`);
|
||||
errorState = null;
|
||||
try {
|
||||
const response = await OwlClient.pis.getByCode(code);
|
||||
results = response.data || []
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
errorState = { status: 400, message: e.message };
|
||||
} else if (e instanceof ApiError) {
|
||||
console.log(e)
|
||||
errorState = { status: 20, message: e.message };
|
||||
} else {
|
||||
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
|
||||
}
|
||||
} finally {
|
||||
resultsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
console.log('Clearing Results');
|
||||
resultsLoaded = false;
|
||||
results = [];
|
||||
}
|
||||
function clearResults() {
|
||||
console.log('Clearing Results');
|
||||
resultsLoaded = false;
|
||||
results = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !resultsLoaded}
|
||||
<div class="card-container">
|
||||
<PisStartEndCard onsearch={handleStartEndSearch} />
|
||||
<PisCode onsearch={handleCodeSearch} />
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<PisStartEndCard onsearch={handleStartEndSearch} />
|
||||
<PisCode onsearch={handleCodeSearch} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="result-container">
|
||||
{#if errorState}
|
||||
<span class="errCode">Error: {errorState.status}</span>
|
||||
<span class="errMsg">{errorState.message}</span>
|
||||
{:else}
|
||||
{#if results.length}
|
||||
<h2 class="result-title">{results.length} Result{#if results.length > 1}s{/if} found:</h2>
|
||||
{#each results as result}
|
||||
<p>{JSON.stringify(result)}</p>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="no-results">No matching results</p>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="reset-button-container">
|
||||
<Button onclick={clearResults}>Reset</Button>
|
||||
</div> </div>
|
||||
<div class="result-container">
|
||||
{#if errorState}
|
||||
<span class="errCode">Error: {errorState.status}</span>
|
||||
<span class="errMsg">{errorState.message}</span>
|
||||
{:else if results.length}
|
||||
<h2 class="result-title">
|
||||
{results.length} Result{#if results.length > 1}s{/if} found
|
||||
</h2>
|
||||
<table class="result-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:16%">TOC</th>
|
||||
<th style="width:14%">Code</th>
|
||||
<th style="width:70%">Locations</th>
|
||||
</tr></thead
|
||||
>
|
||||
{#each results as result}
|
||||
<tbody
|
||||
><tr>
|
||||
<td><TocStyle toc={result.toc || ''} /></td>
|
||||
<td>{result.code}</td>
|
||||
<td class="locations-row">{result.crsStops?.join(' ') || ''}</td>
|
||||
</tr></tbody
|
||||
>
|
||||
{/each}
|
||||
</table>
|
||||
{:else}
|
||||
<p class="no-results">No matching results</p>
|
||||
{/if}
|
||||
<div class="reset-button-container">
|
||||
<Button onclick={clearResults}>Reset</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@@ -93,32 +109,54 @@
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.result-container {
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
background: var(--color-accent);
|
||||
border-radius: 15px;
|
||||
padding: 20px 0 20px 0;
|
||||
margin: auto;
|
||||
margin-top: 25px;
|
||||
width: 90%;
|
||||
max-width: 1000px;
|
||||
.result-container {
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
background: var(--color-accent);
|
||||
border-radius: 15px;
|
||||
padding: 20px 0 20px 0;
|
||||
margin: auto;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: var(--shadow-std);
|
||||
}
|
||||
}
|
||||
|
||||
.errCode {
|
||||
color: rgb(255, 54, 54);
|
||||
font-weight: 600;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.result-title {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.reset-button-container {
|
||||
padding: 20px 0 3px 0;
|
||||
}
|
||||
</style>
|
||||
.result-table {
|
||||
width: 90%;
|
||||
max-width: 350px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
table-layout: fixed;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.locations-row {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
text-align: left;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.errCode {
|
||||
color: rgb(255, 54, 54);
|
||||
font-weight: 600;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.reset-button-container {
|
||||
padding: 20px 0 3px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,4 +2,4 @@ export const load = () => {
|
||||
return {
|
||||
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"}
|
||||
]
|
||||
@@ -8,7 +8,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"sourceMap": false,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user