first commit
This commit is contained in:
commit
3f68f9ae72
25 changed files with 8803 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.make.*
|
||||
app.js
|
||||
elm-stuff/
|
||||
error.txt
|
||||
platform_data/
|
2
.watchmakerc
Normal file
2
.watchmakerc
Normal file
|
@ -0,0 +1,2 @@
|
|||
extensions:
|
||||
- .elm
|
11
Makefile
Normal file
11
Makefile
Normal file
|
@ -0,0 +1,11 @@
|
|||
DIRNAME=$(notdir $(CURDIR))
|
||||
|
||||
ELMS=$(wildcard src/*.elm)
|
||||
|
||||
app.js: src/App.elm $(ELMS)
|
||||
-elm make $< --output=$@ 2> error.txt
|
||||
@cat error.txt
|
||||
|
||||
upload: app.js index.html style.css
|
||||
rsync -avz . clpland:~/domains/somethingorotherwhatever.com/html/$(DIRNAME)
|
||||
@echo "Uploaded to https://somethingorotherwhatever.com/$(DIRNAME)"
|
22
README.txt
Normal file
22
README.txt
Normal file
|
@ -0,0 +1,22 @@
|
|||
JSON files:
|
||||
|
||||
lines.json
|
||||
Maps line names ("GREEN" or "YELLOW") to a list of stations along that line, with properties "key" (string) and "position" (list of two floats).
|
||||
|
||||
platforms.json
|
||||
From https://github.com/danielgjackson/metro-rti/blob/main/data/platforms.json
|
||||
Maps station keys to objects:
|
||||
|
||||
"platformNumber": int
|
||||
"direction": string, "IN" or "OUT"
|
||||
"helperText": string, readable description of the direction of travel
|
||||
|
||||
station-directions.json
|
||||
Maps station keys to a dictionary mapping platform numbers to the angle of that platform on the map.
|
||||
|
||||
station-positions.json
|
||||
Maps line names ("green" or "yellow") to a list of pairs of floats, giving the position on the map of each station on the line.
|
||||
|
||||
stations.json
|
||||
From https://github.com/danielgjackson/metro-rti/blob/main/data/stations.json
|
||||
Maps station keys to readable station names.
|
BIN
Sanchez-Regular.otf
Normal file
BIN
Sanchez-Regular.otf
Normal file
Binary file not shown.
BIN
Sanchez-Regular.ttf
Normal file
BIN
Sanchez-Regular.ttf
Normal file
Binary file not shown.
31
cgi-bin/platform_data.py
Executable file
31
cgi-bin/platform_data.py
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/srv/think.somethingorotherwhatever.com/venv/bin/python
|
||||
|
||||
import cgi
|
||||
import cgitb
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
import getpass
|
||||
|
||||
cgitb.enable()
|
||||
|
||||
form = cgi.FieldStorage()
|
||||
|
||||
print('Content-Type: application/json\n')
|
||||
|
||||
station = form['station'].value
|
||||
platform = form['platform'].value
|
||||
|
||||
data_root = Path('..') / 'platform_data'
|
||||
|
||||
data_root.mkdir(exist_ok=True)
|
||||
|
||||
fname = data_root / f'{station}-{platform}.json'
|
||||
|
||||
if (not fname.exists()) or (datetime.now() - datetime.fromtimestamp(fname.stat().st_mtime) > timedelta(minutes=1)):
|
||||
req = requests.get(f'https://metro-rti.nexus.org.uk/api/times/{station}/{platform}')
|
||||
with open(fname, 'w') as f:
|
||||
f.write(req.text)
|
||||
|
||||
with open(fname) as f:
|
||||
print(f.read())
|
25
elm.json
Normal file
25
elm.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/svg": "1.0.1",
|
||||
"elm/time": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/url": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.3"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
"direct": {},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
25
index.html
Normal file
25
index.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Metro info</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" href="metro_logo.svg" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>Elm app by clp</h1>
|
||||
</header>
|
||||
<main>
|
||||
<p>This is an app which will either load succesfully, and you'll wonder whether you saw this text at all, or fail ignominiously, showing you only this text.</p>
|
||||
<p>On balance of probabilities: I'm sorry I couldn't be bothered to make this work for you.</p>
|
||||
</main>
|
||||
<footer>Made by <a href="https://somethingorotherwhatever.com">clp</a></footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<script src="load-app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
1
lines.json
Normal file
1
lines.json
Normal file
File diff suppressed because one or more lines are too long
61
load-app.js
Normal file
61
load-app.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import show_error from './show-error.mjs';
|
||||
async function init_app() {
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const station = params.get('station');
|
||||
|
||||
|
||||
const station_names = await (await fetch('stations.json')).json();
|
||||
const station_directions = await (await fetch('station-directions.json')).json();
|
||||
const station_latlons = await (await fetch('station-latlons.json')).json();
|
||||
|
||||
const platforms = await (await fetch('platforms.json')).json();
|
||||
|
||||
station_names['MON'] = 'Monument';
|
||||
station_latlons['MON'] = station_latlons['MTW'];
|
||||
delete station_names['MTW'];
|
||||
delete station_names['MTS'];
|
||||
delete station_latlons['MTW'];
|
||||
delete station_latlons['MTS'];
|
||||
Object.entries(platforms).forEach(([station_key,platforms]) => {
|
||||
platforms.forEach(p => {
|
||||
p.station_key = station_key;
|
||||
});
|
||||
})
|
||||
platforms['MON'] = platforms['MTS'].concat(platforms['MTW']);
|
||||
station_directions['MON'] = Object.assign({}, station_directions['MTS'], station_directions['MTW']);
|
||||
|
||||
const stations = Object.keys(station_names).map(key => {
|
||||
return {key, name: station_names[key], platforms: platforms[key], directions: station_directions[key], latlon: station_latlons[key]};
|
||||
});
|
||||
|
||||
const lines = await (await fetch('lines.json')).json();
|
||||
Object.values(lines).map(stops => stops.forEach(stop => {
|
||||
stop.key = (stop.key == 'MTS' || stop.key == 'MTW') ? 'MON' : stop.key;
|
||||
}))
|
||||
const compilation_error = await show_error;
|
||||
if(compilation_error) {
|
||||
return;
|
||||
}
|
||||
const app = Elm.App.init({node: document.body, flags: {stations, lines, station}});
|
||||
|
||||
app.ports.request_data.subscribe(async ([station,platformNumber]) => {
|
||||
const url = `cgi-bin/platform_data.py?station=${station}&platform=${platformNumber}`;
|
||||
const data = await (await fetch(url)).json();
|
||||
station = (station == 'MTS' || station == 'MTW') ? 'MON' : station;
|
||||
const out = {time: (new Date()).toISOString(), station, platformNumber, data}
|
||||
app.ports.receive_platform_data.send(out);
|
||||
});
|
||||
|
||||
app.ports.request_location.subscribe(async () => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(r) => {
|
||||
console.log(r);
|
||||
app.ports.receive_location.send(r);
|
||||
},
|
||||
(e) => console.error(e)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
init_app();
|
25
manifest.json
Normal file
25
manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "Metro info",
|
||||
"description": "See the next trains from Metro stations",
|
||||
"start_url": "https://metro-info.think.somethingorotherwhatever.com/",
|
||||
"display": "standalone",
|
||||
"background_color": "#feb300",
|
||||
"theme_color": "#feb300",
|
||||
"icons": [
|
||||
{
|
||||
"src": "metro_logo.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "any"
|
||||
},
|
||||
{
|
||||
"src": "metro_logo.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "metro_logo_192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
}
|
BIN
metro_logo.png
Normal file
BIN
metro_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
55
metro_logo.svg
Normal file
55
metro_logo.svg
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
version="1.1"
|
||||
id="svg138"
|
||||
sodipodi:docname="Tyne_Wear_Metro_logo.svg"
|
||||
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
|
||||
inkscape:export-filename="Tyne_Wear_Metro_logo.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs142" />
|
||||
<sodipodi:namedview
|
||||
id="namedview140"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.46365422"
|
||||
inkscape:cx="129.40678"
|
||||
inkscape:cy="255.57839"
|
||||
inkscape:window-width="1379"
|
||||
inkscape:window-height="847"
|
||||
inkscape:window-x="61"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg138" />
|
||||
<path
|
||||
d="M 0,383.75406 H 512 V 0 H 0 Z"
|
||||
fill="#fdb826"
|
||||
id="path132"
|
||||
style="stroke-width:1.0906" />
|
||||
<path
|
||||
style="stroke-width:1.0906"
|
||||
d="M 0,512 H 512 V 383.75406 H 0 Z"
|
||||
id="path421" />
|
||||
<path
|
||||
style="stroke-width:1.00294"
|
||||
d="m 343.682,111.49184 -63.92,216.73307 h -47.754 l -64.38,-216.87792 h -0.03 v 216.87792 h -75.34 v -45.11115 h 25.861 V 96.879274 H 92.258 V 51.795286 H 199.99 l 28.738,100.380774 27.559,94.50638 26.725,-96.0977 27.53,-98.788448 H 418.274 V 96.879274 H 392.009 V 283.11376 h 26.265 v 45.11115 H 343.768 V 111.49284"
|
||||
id="path134" />
|
||||
<path
|
||||
d="m 390.543,427.39057 c -8.889,0 -15.592,5.67121 -15.592,19.41569 0,8.94236 3.596,19.41669 15.592,19.41669 11.305,0 15.592,-9.98345 15.592,-19.41669 0,-9.63542 -4.287,-19.41569 -15.592,-19.41569 m 0,51.73795 c -19.446,0 -30.867,-15.13361 -30.867,-32.32226 0,-17.18764 11.42,-32.32126 30.867,-32.32126 19.475,0 30.867,15.13362 30.867,32.32126 0,17.18865 -11.392,32.32226 -30.867,32.32226 m -80.145,-35.07035 h 13.981 c 4.89,-0.0875 8.141,-2.57508 8.141,-8.2483 0,-5.6702 -3.25,-8.15877 -8.141,-8.27445 h -13.98 z M 295.152,416.192 h 30.493 c 13.722,0 22.15,6.36628 22.15,20.13993 0,7.89925 -4.976,14.23636 -12.628,16.49358 l 13.98,24.59602 H 332.09 l -11.996,-22.02095 h -9.695 v 22.02095 H 295.153 M 233.994,416.192 h 47.178 v 11.86347 h -15.966 v 49.36606 H 249.96 v -49.36606 h -15.966 m -58.195,49.36606 V 416.192 h 44.934 v 11.86347 h -29.659 v 11.1694 h 27.674 v 11.89364 h -27.674 v 14.41038 h 29.515 v 11.89264 M 89.469,416.192 h 22.38 l 10.788,45.48835 h 0.173 L 133.971,416.192 h 22.122 v 61.22852 H 141.68 v -47.57055 h -0.172 l -12.082,47.57156 h -13.463 l -12.428,-47.57156 h -0.172 v 47.57156 H 89.47"
|
||||
fill="#ffffff"
|
||||
id="path136"
|
||||
style="stroke-width:1.00294" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
BIN
metro_logo_192.png
Normal file
BIN
metro_logo_192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
7504
metro_map.svg
Normal file
7504
metro_map.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 316 KiB |
1
platforms.json
Normal file
1
platforms.json
Normal file
File diff suppressed because one or more lines are too long
21
show-error.mjs
Normal file
21
show-error.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
export default fetch('/error.txt').then(r=>{
|
||||
if(r.ok) {
|
||||
return r.text();
|
||||
} else {
|
||||
throw('');
|
||||
}
|
||||
}).then(text => {
|
||||
if(!text) {
|
||||
return false;
|
||||
}
|
||||
document.body.innerHTML = '';
|
||||
const error_show = document.createElement('pre');
|
||||
error_show.setAttribute('id','build-error');
|
||||
error_show.style.background = 'black';
|
||||
error_show.style.color = 'white';
|
||||
error_show.style.padding = '1em';
|
||||
error_show.style['font-size'] = '16px';
|
||||
error_show.textContent = text;
|
||||
document.body.appendChild(error_show);
|
||||
return true;
|
||||
}).catch(e => false);
|
610
src/App.elm
Normal file
610
src/App.elm
Normal file
|
@ -0,0 +1,610 @@
|
|||
port module App exposing (..)
|
||||
|
||||
import Browser
|
||||
import Browser.Events exposing (Visibility(..))
|
||||
import Dict exposing (Dict)
|
||||
import Html as H exposing (Html)
|
||||
import Html.Attributes as HA
|
||||
import Html.Events as HE
|
||||
import Json.Decode as JD
|
||||
import LatLonDistance exposing (lat_lon_distance, LatLon)
|
||||
import Svg as Svg
|
||||
import Svg.Attributes as SA
|
||||
import Svg.Events as SE
|
||||
import Task
|
||||
import Time
|
||||
import Tuple exposing (pair, first, second)
|
||||
|
||||
|
||||
port request_data : (String, Int) -> Cmd msg
|
||||
port request_location : () -> Cmd msg
|
||||
port receive_platform_data : (JD.Value -> msg) -> Sub msg
|
||||
port receive_location : (JD.Value -> msg) -> Sub msg
|
||||
|
||||
main = Browser.document
|
||||
{ init = init
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
, view = view
|
||||
}
|
||||
|
||||
type alias Model =
|
||||
{ stations : (List Station)
|
||||
, lines : Dict String (List Stop)
|
||||
, error : Maybe JD.Error
|
||||
, current_station : Maybe String
|
||||
, time : Time.Posix
|
||||
, time_zone : Time.Zone
|
||||
, window_visible : Visibility
|
||||
, current_position : Maybe LatLon
|
||||
}
|
||||
|
||||
type alias Stop =
|
||||
{ station : String
|
||||
, position: (Float, Float)
|
||||
, latlon : (Float, Float)
|
||||
}
|
||||
|
||||
type alias Station =
|
||||
{ key : String
|
||||
, name : String
|
||||
, platforms : List Platform
|
||||
, latlon : LatLon
|
||||
}
|
||||
|
||||
blank_station =
|
||||
{ key = "???"
|
||||
, name = "Unknown station"
|
||||
, platforms = []
|
||||
, latlon = {lat = 0, lon = 0}
|
||||
}
|
||||
|
||||
type alias Platform =
|
||||
{ last_checked : Time.Posix
|
||||
, platformNumber : Int
|
||||
, direction : PlatformDirection
|
||||
, angle : Float
|
||||
, helperText : String
|
||||
, trains : List TrainInfo
|
||||
, station_key : String
|
||||
}
|
||||
|
||||
type alias TrainInfo =
|
||||
{ train : String
|
||||
, last_event : TrainEvent
|
||||
, last_event_location : (String, Int)
|
||||
, last_event_time : String
|
||||
, destination : String
|
||||
, due_in : Int
|
||||
, line : String
|
||||
}
|
||||
|
||||
type alias PlatformDataResponse =
|
||||
{ time : String
|
||||
, station : String
|
||||
, platformNumber : Int
|
||||
, data : List TrainInfo
|
||||
}
|
||||
|
||||
type TrainEvent
|
||||
= Departed
|
||||
| Arrived
|
||||
| ReadyToStart
|
||||
| Approaching
|
||||
|
||||
event_description : TrainEvent -> String
|
||||
event_description event = case event of
|
||||
Departed -> "Departed"
|
||||
Arrived -> "Arrived"
|
||||
ReadyToStart -> "Starting"
|
||||
Approaching -> "Approaching"
|
||||
|
||||
type PlatformDirection
|
||||
= In
|
||||
| Out
|
||||
|
||||
dir_arrow : PlatformDirection -> String
|
||||
dir_arrow d = case d of
|
||||
In -> "←"
|
||||
Out -> "→"
|
||||
|
||||
decode_station : JD.Decoder Station
|
||||
decode_station =
|
||||
JD.map4 Station
|
||||
(JD.field "key" JD.string)
|
||||
(JD.field "name" JD.string)
|
||||
(JD.field "directions" (JD.dict JD.float) |> JD.andThen
|
||||
(\directions -> (JD.field "platforms" (JD.list (decode_platform directions))))
|
||||
)
|
||||
(JD.field "latlon" decode_latlon)
|
||||
|
||||
decode_platform : Dict String Float -> JD.Decoder Platform
|
||||
decode_platform directions =
|
||||
JD.map4
|
||||
(\platformNumber direction helperText station_key ->
|
||||
let
|
||||
angle = Dict.get (String.fromInt platformNumber) directions |> Maybe.withDefault 0
|
||||
in
|
||||
{ last_checked = (Time.millisToPosix 0)
|
||||
, platformNumber = platformNumber
|
||||
, direction = direction
|
||||
, helperText = helperText
|
||||
, station_key = station_key
|
||||
, trains = []
|
||||
, angle = angle
|
||||
}
|
||||
)
|
||||
(JD.field "platformNumber" JD.int)
|
||||
(JD.field "direction" decode_direction)
|
||||
(JD.field "helperText" JD.string)
|
||||
(JD.field "station_key" JD.string)
|
||||
|
||||
decode_direction : JD.Decoder PlatformDirection
|
||||
decode_direction =
|
||||
JD.string |> JD.andThen
|
||||
(\s -> case s of
|
||||
"IN" -> JD.succeed In
|
||||
"OUT" -> JD.succeed Out
|
||||
_ -> JD.fail <| "Unrecognised direction " ++ s
|
||||
)
|
||||
|
||||
decode_stop : JD.Decoder Stop
|
||||
decode_stop =
|
||||
JD.map3 Stop
|
||||
(JD.field "key" JD.string)
|
||||
(JD.field "map_position" (JD.map2 pair (JD.field "0" JD.float) (JD.field "1" JD.float)))
|
||||
(JD.field "latlon" (JD.map2 pair (JD.field "0" JD.float) (JD.field "1" JD.float)))
|
||||
|
||||
decode_train_info : Model -> JD.Decoder TrainInfo
|
||||
decode_train_info model =
|
||||
JD.map7 TrainInfo
|
||||
(JD.field "trn" JD.string)
|
||||
(JD.field "lastEvent" decode_train_event)
|
||||
(JD.field "lastEventLocation" (decode_event_location model))
|
||||
(JD.field "lastEventTime" JD.string)
|
||||
(JD.field "destination" JD.string)
|
||||
(JD.field "dueIn" JD.int)
|
||||
(JD.field "line" JD.string)
|
||||
|
||||
decode_event_location : Model -> JD.Decoder (String, Int)
|
||||
decode_event_location model =
|
||||
JD.string
|
||||
|> JD.map (\s ->
|
||||
let
|
||||
i = String.indices "Platform" s |> List.head |> Maybe.withDefault 0
|
||||
name = String.slice 0 (i-1) s |> station_name_to_key model |> Maybe.withDefault ("???" ++ s)
|
||||
platform_number = String.dropLeft (i+9) s |> String.toInt |> Maybe.withDefault 0
|
||||
in
|
||||
(name, platform_number)
|
||||
)
|
||||
|
||||
decode_train_event : JD.Decoder TrainEvent
|
||||
decode_train_event =
|
||||
JD.string |> JD.andThen
|
||||
(\s -> case s of
|
||||
"ARRIVED" -> JD.succeed Arrived
|
||||
"DEPARTED" -> JD.succeed Departed
|
||||
"READY_TO_START" -> JD.succeed ReadyToStart
|
||||
"APPROACHING" -> JD.succeed Approaching
|
||||
_ -> JD.fail <| "Unrecognised train event " ++ s
|
||||
)
|
||||
|
||||
decode_platform_data_response model =
|
||||
JD.map4 PlatformDataResponse
|
||||
(JD.field "time" JD.string)
|
||||
(JD.field "station" JD.string)
|
||||
(JD.field "platformNumber" JD.int)
|
||||
(JD.field "data" (JD.list (decode_train_info model)))
|
||||
|
||||
decode_geolocation : JD.Decoder LatLon
|
||||
decode_geolocation =
|
||||
JD.map2 LatLon
|
||||
(JD.at ["coords", "latitude"] JD.float)
|
||||
(JD.at ["coords", "longitude"] JD.float)
|
||||
|
||||
decode_latlon : JD.Decoder LatLon
|
||||
decode_latlon =
|
||||
JD.map2 LatLon
|
||||
(JD.field "0" JD.float)
|
||||
(JD.field "1" JD.float)
|
||||
|
||||
type Msg
|
||||
= ReceivePlatformData JD.Value
|
||||
| ReceiveLocation JD.Value
|
||||
| TriggerTrainInfoRequest Station
|
||||
| SetCurrentStation Station
|
||||
| ClearCurrentStation
|
||||
| UpdatePlatformData
|
||||
| Tick Time.Posix
|
||||
| SetTimeZone Time.Zone
|
||||
| VisibilityChange Visibility
|
||||
| RequestLocation
|
||||
|
||||
init : (JD.Value) -> (Model, Cmd Msg)
|
||||
init flags =
|
||||
let
|
||||
model = init_model flags
|
||||
in
|
||||
( model
|
||||
, Cmd.batch
|
||||
[ request_all_platform_data (station_with_key model (model.current_station |> Maybe.withDefault "MSN"))
|
||||
, Task.perform SetTimeZone Time.here
|
||||
]
|
||||
)
|
||||
|
||||
blank_model =
|
||||
{ stations = []
|
||||
, lines = Dict.empty
|
||||
, error = Nothing
|
||||
, current_station = Just "MSN"
|
||||
, time = Time.millisToPosix 0
|
||||
, time_zone = Time.utc
|
||||
, window_visible = Visible
|
||||
, current_position = Nothing
|
||||
}
|
||||
|
||||
type alias Flags =
|
||||
{ stations : List Station
|
||||
, lines : Dict String (List Stop)
|
||||
, station : String
|
||||
}
|
||||
|
||||
|
||||
decode_flags =
|
||||
JD.map3 Flags
|
||||
(JD.field "stations" (JD.list decode_station))
|
||||
(JD.field "lines" (JD.dict (JD.list decode_stop)))
|
||||
(JD.oneOf
|
||||
[ JD.field "station" JD.string
|
||||
, JD.succeed "MSN"
|
||||
]
|
||||
)
|
||||
|
||||
init_model : JD.Value -> Model
|
||||
init_model flags =
|
||||
flags
|
||||
|> JD.decodeValue decode_flags
|
||||
|> (\r -> case r of
|
||||
Ok flagdata -> { blank_model | stations = flagdata.stations, lines = flagdata.lines, current_station = Just flagdata.station }
|
||||
Err err -> { blank_model | error = Just (Debug.log "error" err) }
|
||||
)
|
||||
|
||||
nocmd model = (model, Cmd.none)
|
||||
|
||||
request_all_platform_data station =
|
||||
Cmd.batch (List.map (\p -> request_data (p.station_key, p.platformNumber)) station.platforms)
|
||||
|
||||
update msg model = case msg of
|
||||
TriggerTrainInfoRequest station ->
|
||||
( model
|
||||
, request_all_platform_data station
|
||||
)
|
||||
|
||||
ReceivePlatformData data ->
|
||||
case JD.decodeValue (decode_platform_data_response model) data of
|
||||
Err err -> {model | error = Just err } |> nocmd
|
||||
Ok d -> add_platform_data d model |> nocmd
|
||||
|
||||
RequestLocation -> (model, request_location ())
|
||||
|
||||
ReceiveLocation data ->
|
||||
case JD.decodeValue (decode_geolocation) data of
|
||||
Err err -> {model | error = Just (Debug.log "err" err) } |> nocmd
|
||||
Ok latlon ->
|
||||
let
|
||||
closest_stations = geo_closest_stations model latlon
|
||||
nstation = case List.head closest_stations of
|
||||
Nothing -> model.current_station
|
||||
Just (s,_) -> Just s.key
|
||||
in
|
||||
{ model | current_position = Just latlon, current_station = nstation } |> nocmd
|
||||
|
||||
SetCurrentStation station -> { model | current_station = Just station.key } |> update (TriggerTrainInfoRequest station)
|
||||
|
||||
ClearCurrentStation ->
|
||||
{ model | current_station = Nothing } |> nocmd
|
||||
|
||||
UpdatePlatformData ->
|
||||
( model
|
||||
, case (model.window_visible, model.current_station) of
|
||||
(Visible, Just s) -> station_with_key model s |> request_all_platform_data
|
||||
_ -> Cmd.none
|
||||
)
|
||||
|
||||
Tick time -> { model | time = time } |> nocmd
|
||||
|
||||
SetTimeZone zone -> { model | time_zone = zone } |> nocmd
|
||||
|
||||
VisibilityChange visible -> { model | window_visible = visible } |> nocmd
|
||||
|
||||
add_platform_data d model =
|
||||
let
|
||||
nstations =
|
||||
model.stations
|
||||
|> List.map (\s ->
|
||||
if s.key == d.station then
|
||||
{ s | platforms = List.map (\p -> if p.platformNumber == d.platformNumber then { p | trains = d.data, last_checked = model.time } else p) s.platforms}
|
||||
else s
|
||||
)
|
||||
in
|
||||
{ model | stations = nstations }
|
||||
|
||||
subscriptions model =
|
||||
Sub.batch
|
||||
[ receive_platform_data ReceivePlatformData
|
||||
, receive_location ReceiveLocation
|
||||
, Time.every (1000 * 60) (\_ -> UpdatePlatformData)
|
||||
, Time.every 100 Tick
|
||||
, Browser.Events.onVisibilityChange VisibilityChange
|
||||
]
|
||||
|
||||
view : Model -> Browser.Document Msg
|
||||
view model =
|
||||
let
|
||||
current_station = model.current_station |> Maybe.map (station_with_key model)
|
||||
|
||||
next_trains =
|
||||
current_station
|
||||
|> Maybe.map (\s ->
|
||||
s.platforms
|
||||
|> List.filterMap (\p -> p.trains |> List.head |> Maybe.map (\t -> "P" ++ (String.fromInt p.platformNumber) ++ ": " ++ (String.fromInt t.due_in)))
|
||||
|> String.join ", "
|
||||
|> \times -> times ++ " @ " ++ s.name
|
||||
)
|
||||
|> Maybe.withDefault ""
|
||||
|
||||
view_station open station =
|
||||
H.article
|
||||
[ HE.on "toggle" (
|
||||
JD.succeed (TriggerTrainInfoRequest station)
|
||||
)
|
||||
, HA.class "station"
|
||||
]
|
||||
[ H.ol
|
||||
[ HA.class "platforms"]
|
||||
(List.map (view_platform station) station.platforms)
|
||||
]
|
||||
|
||||
format_time t =
|
||||
let
|
||||
zone = model.time_zone
|
||||
hour = Time.toHour zone t
|
||||
minute = Time.toMinute zone t
|
||||
pad = String.fromInt >> String.padLeft 2 '0'
|
||||
in
|
||||
(pad hour) ++ ":" ++ (pad minute)
|
||||
|
||||
closest_stations = case model.current_position of
|
||||
Nothing -> []
|
||||
Just pos -> geo_closest_stations model pos
|
||||
|
||||
view_platform : Station -> Platform -> Html Msg
|
||||
view_platform station platform =
|
||||
H.li
|
||||
[ HA.value <| String.fromInt platform.platformNumber
|
||||
]
|
||||
[ table
|
||||
[ H.a
|
||||
[ HA.href <| "https://metro-rti.nexus.org.uk/api/times/" ++ station.key ++ "/" ++ (String.fromInt platform.platformNumber)
|
||||
, HA.target "_blank"
|
||||
]
|
||||
[H.text <|
|
||||
"Platform "
|
||||
++ (String.fromInt platform.platformNumber)
|
||||
++ ": "
|
||||
++ platform.helperText
|
||||
]
|
||||
, H.small [ HA.class "last-checked" ] [ H.text <| format_time platform.last_checked]
|
||||
]
|
||||
["Due", "Destination", "Last seen", "Event", "Train"]
|
||||
(List.map (\t ->
|
||||
let
|
||||
last_station = station_with_key model (first t.last_event_location)
|
||||
time_expected =
|
||||
(Time.posixToMillis model.time) + (1000 * 60 * t.due_in)
|
||||
|> Time.millisToPosix
|
||||
|
||||
due_string =
|
||||
if t.due_in > 1 then
|
||||
(String.fromInt t.due_in) ++ " (" ++ format_time time_expected ++ ")"
|
||||
else
|
||||
"here"
|
||||
in
|
||||
[ due_string
|
||||
, t.destination
|
||||
, last_station.name
|
||||
, event_description t.last_event
|
||||
, t.train
|
||||
]
|
||||
) platform.trains
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
title = "Metro info (" ++ next_trains ++ ")",
|
||||
body =
|
||||
[ H.div
|
||||
[ HA.id "controls" ]
|
||||
[ H.label
|
||||
[ HA.for "station" ]
|
||||
[ H.text "Trains from"]
|
||||
, H.select
|
||||
[ HA.id "station"
|
||||
, HA.value <| Maybe.withDefault "" <| model.current_station
|
||||
, HE.on "input"
|
||||
( JD.field "target" (JD.field "value" JD.string)
|
||||
|> JD.andThen
|
||||
( station_with_key model
|
||||
>> SetCurrentStation >> JD.succeed
|
||||
)
|
||||
)
|
||||
]
|
||||
(List.map
|
||||
(\s ->
|
||||
H.option
|
||||
[ HA.value s.key]
|
||||
[H.text s.name]
|
||||
)
|
||||
(List.sortBy .name model.stations)
|
||||
)
|
||||
, H.button
|
||||
[ HA.type_ "button"
|
||||
, HE.onClick UpdatePlatformData
|
||||
]
|
||||
[ H.text "⟳" ]
|
||||
|
||||
, H.button
|
||||
[ HA.type_ "button"
|
||||
, HE.onClick RequestLocation
|
||||
]
|
||||
[ H.text "🧭" ]
|
||||
|
||||
, H.a
|
||||
[ HA.href "https://www.nexus.org.uk/metro/updates"
|
||||
, HA.target "_blank"
|
||||
]
|
||||
[ H.text "Disruptions" ]
|
||||
]
|
||||
, case current_station of
|
||||
Nothing -> H.text ""
|
||||
Just station -> view_station True station
|
||||
, view_map model
|
||||
|
||||
, H.footer
|
||||
[]
|
||||
[ H.p
|
||||
[]
|
||||
[ H.text "Made unofficially by "
|
||||
, H.a [ HA.href "https://somethingorotherwhatever.com" ] [ H.text "clp" ]
|
||||
, H.text ". Uses data from the "
|
||||
, H.a [ HA.href "https://github.com/danielgjackson/metro-rti" ] [ H.text "Tyne and Wear Metro Real-Time Information API" ]
|
||||
, H.text "."
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
station_position : Model -> String -> String -> Maybe (Float, Float)
|
||||
station_position model line station_key =
|
||||
Dict.get line model.lines
|
||||
|> Maybe.andThen (List.filter (\s -> s.station == station_key) >> List.head >> Maybe.map .position)
|
||||
|
||||
platform_direction : Model -> String -> Int -> Maybe Float
|
||||
platform_direction model station_key platform_number =
|
||||
model.stations |> List.filter (.key >> (==) station_key) |> List.head |> Maybe.andThen (.platforms >> List.filter (.platformNumber >> (==) platform_number) >> List.head) |> Maybe.map (.angle)
|
||||
|
||||
station_name_to_key : Model -> String -> Maybe String
|
||||
station_name_to_key model name =
|
||||
model.stations
|
||||
|> List.map (\s -> (s.name, s.key))
|
||||
|> Dict.fromList
|
||||
|> Dict.get name
|
||||
|
||||
station_with_key : Model -> String -> Station
|
||||
station_with_key model key = model.stations |> List.filter (.key >> (==) key) |> List.head |> Maybe.withDefault { blank_station | key = key }
|
||||
|
||||
geo_closest_stations model pos = model.stations |> List.map (\s -> (s, lat_lon_distance pos s.latlon)) |> List.sortBy second
|
||||
|
||||
all_trains : Model -> List TrainInfo
|
||||
all_trains model =
|
||||
List.concatMap (.platforms >> List.concatMap (.trains)) (List.filter (\s -> model.current_station == Just s.key) model.stations)
|
||||
|> List.sortBy (\t -> (t.train, t.last_event_time))
|
||||
|
||||
view_map model =
|
||||
let
|
||||
trains =
|
||||
all_trains model |> List.filterMap (\t ->
|
||||
let
|
||||
(key, platformNumber) = t.last_event_location
|
||||
position = station_position model t.line key
|
||||
direction = platform_direction model key platformNumber |> Maybe.withDefault 0
|
||||
in
|
||||
position
|
||||
|> Maybe.map (\(x,y) ->
|
||||
Svg.g
|
||||
[ SA.class "train"
|
||||
, SA.transform <| "translate(" ++ (String.fromFloat x)++" "++(String.fromFloat y)++")"
|
||||
]
|
||||
[ Svg.circle
|
||||
[ SA.r "5"
|
||||
]
|
||||
[]
|
||||
, Svg.path
|
||||
[ SA.d "M 0 0 L 8 0 M 5 2 L 8 0 L 5 -2"
|
||||
, SA.fill "none"
|
||||
, SA.stroke "black"
|
||||
, SA.strokeWidth "5"
|
||||
, SA.transform <| "rotate(" ++ (String.fromFloat direction) ++ ")"
|
||||
]
|
||||
[]
|
||||
, Svg.text_
|
||||
[ SA.fontSize "4"
|
||||
, SA.fill "white"
|
||||
, SA.textAnchor "middle"
|
||||
, SA.dominantBaseline "middle"
|
||||
]
|
||||
[ Svg.text t.train ]
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
lines : List (String, List Stop)
|
||||
lines =
|
||||
model.lines
|
||||
|> Dict.toList
|
||||
|
||||
stations =
|
||||
lines
|
||||
|> List.concatMap (\(line,stops) ->
|
||||
stops
|
||||
|> List.map (\stop -> station_with_key model stop.station |> (\s ->
|
||||
Svg.circle
|
||||
[ SA.r "6"
|
||||
, SA.cx <| String.fromFloat (first stop.position)
|
||||
, SA.cy <| String.fromFloat (second stop.position)
|
||||
, SA.fillOpacity <| if model.current_station == Just stop.station then "0.5" else "0.01"
|
||||
, SA.fill "blue"
|
||||
, SE.onClick (SetCurrentStation s)
|
||||
]
|
||||
[]
|
||||
))
|
||||
)
|
||||
in
|
||||
H.div
|
||||
[ HA.id "map-container" ]
|
||||
[ Svg.svg
|
||||
[ HA.attribute "viewBox" "0 0 595.275 280.63"
|
||||
, HA.id "map"
|
||||
]
|
||||
[ Svg.use
|
||||
[ HA.attribute "href" "metro_map.svg#map"
|
||||
]
|
||||
[]
|
||||
, Svg.g [] (trains)
|
||||
, Svg.g [] (stations)
|
||||
]
|
||||
]
|
||||
|
||||
table : List (Html Msg) -> List String -> List (List String) -> Html Msg
|
||||
table caption headers rows =
|
||||
H.table
|
||||
[]
|
||||
[ H.caption
|
||||
[]
|
||||
caption
|
||||
, H.thead
|
||||
[]
|
||||
[H.tr [] (List.map (\h -> H.th [] [H.text h]) headers)]
|
||||
, H.tbody
|
||||
[]
|
||||
( (List.map (\row -> H.tr [] (List.map (\c -> H.td [] [H.text c]) row)) rows)
|
||||
|> pad_list 4 (H.tr [HA.class "empty"] (List.map (\_ -> H.td [] [ H.text "-"]) headers))
|
||||
)
|
||||
]
|
||||
|
||||
pad_list : Int -> a -> List a -> List a
|
||||
pad_list n blank list =
|
||||
List.append list (List.repeat (n - (List.length list)) blank)
|
50
src/LatLonDistance.elm
Normal file
50
src/LatLonDistance.elm
Normal file
|
@ -0,0 +1,50 @@
|
|||
module LatLonDistance exposing (lat_lon_distance, LatLon)
|
||||
|
||||
type alias LatLon = { lat : Float, lon : Float }
|
||||
|
||||
lat_lon_distance : LatLon -> LatLon -> Float
|
||||
lat_lon_distance p1 p2 =
|
||||
let
|
||||
a = 6378137.0
|
||||
b = 6356752.314245
|
||||
f = 1 / 298.257223563
|
||||
lat1 = degrees p1.lat
|
||||
lat2 = degrees p2.lat
|
||||
lon1 = degrees p1.lon
|
||||
lon2 = degrees p2.lon
|
||||
|
||||
dlon = (lon2 - lon1)
|
||||
|
||||
tanU1 = (1-f) * (tan lat1)
|
||||
cosU1 = 1 / (sqrt (1 + tanU1 * tanU1))
|
||||
sinU1 = tanU1 * cosU1
|
||||
|
||||
tanU2 = (1-f) * (tan lat2)
|
||||
cosU2 = 1 / (sqrt (1 + tanU2*tanU2))
|
||||
sinU2 = tanU2 * cosU2
|
||||
|
||||
approx lon =
|
||||
let
|
||||
sinlon = sin lon
|
||||
coslon = cos lon
|
||||
sinSqsigma = (cosU2*sinlon) * (cosU2*sinlon) + (cosU1*sinU2-sinU1*cosU2*coslon) ^ 2
|
||||
sinsigma = sqrt sinSqsigma
|
||||
cossigma = sinU1*sinU2 + cosU1*cosU2*coslon
|
||||
sigma = atan2 sinsigma cossigma
|
||||
sinalpha = cosU1 * cosU2 * sinlon / sinsigma
|
||||
cosSqalpha = 1 - sinalpha*sinalpha
|
||||
cos2sigma_m = cossigma - 2*sinU1*sinU2/cosSqalpha
|
||||
c = f/16*cosSqalpha*(4+f*(4-3*cosSqalpha))
|
||||
lon_ = dlon + (1-c) * f * sinalpha * (sigma + c*sinsigma*(cos2sigma_m+c*cossigma*(-1+2*cos2sigma_m*cos2sigma_m)))
|
||||
|
||||
uSq = cosSqalpha * (a*a - b*b) / (b*b)
|
||||
biga = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
|
||||
bigb = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
|
||||
deltasigma = bigb*sinsigma*(cos2sigma_m+bigb/4*(cossigma*(-1+2*cos2sigma_m*cos2sigma_m)-bigb/6*cos2sigma_m*(-3+4*sinsigma*sinsigma)*(-3+4*cos2sigma_m*cos2sigma_m)))
|
||||
|
||||
s = b*biga*(sigma-deltasigma)
|
||||
|
||||
in
|
||||
if abs (lon - lon_) > 1e-12 then (approx lon_) else s
|
||||
in
|
||||
approx dlon
|
63
station-directions.json
Normal file
63
station-directions.json
Normal file
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"APT": {"1": 0, "2": 180},
|
||||
"CAL": {"1": 0, "2": 180},
|
||||
"BFT": {"1": 0, "2": 180},
|
||||
"KSP": {"1": 0, "2": 180},
|
||||
"FAW": {"1": 0, "2": 180},
|
||||
"WBR": {"1": 0, "2": 180},
|
||||
"RGC": {"1": 0, "2": 180},
|
||||
"SGF": {"1": 60, "2": 240},
|
||||
"ILF": {"1": 60, "2": 240},
|
||||
"WJS": {"1": 60, "2": 240},
|
||||
"JES": {"1": 60, "2": 240},
|
||||
"HAY": {"1": 60, "2": 240},
|
||||
"MTS": {"1": 60, "2": 240},
|
||||
"MTW": {"3": 0, "4": 180},
|
||||
"CEN": {"1": 60, "2": 240},
|
||||
"GHD": {"1": 0, "2": 180},
|
||||
"GST": {"1": 0, "2": 180},
|
||||
"FEL": {"1": 0, "2": 180},
|
||||
"HTH": {"1": 0, "2": 180},
|
||||
"PLW": {"1": 0, "2": 180},
|
||||
"HEB": {"1": 0, "2": 180},
|
||||
"JAR": {"1": 0, "2": 180},
|
||||
"BDE": {"1": 0, "2": 180},
|
||||
"SMD": {"1": 0, "2": 180},
|
||||
"TDK": {"1": 0, "2": 180},
|
||||
"CHI": {"1": 0, "2": 180},
|
||||
"SSS": {"1": 0, "2": 180},
|
||||
"FGT": {"1": 0, "2": 180},
|
||||
"BYW": {"1": 0, "2": 180},
|
||||
"EBO": {"1": 0, "2": 180},
|
||||
"SBN": {"1": 90, "2": 270},
|
||||
"SFC": {"1": 90, "2": 270},
|
||||
"MSP": {"1": 90, "2": 270},
|
||||
"SUN": {"1": 90, "2": 270},
|
||||
"PLI": {"1": 180, "2": 0},
|
||||
"UNI": {"1": 180, "2": 0},
|
||||
"MLF": {"1": 180, "2": 0},
|
||||
"PAL": {"1": 180, "2": 0},
|
||||
"SHL": {"1": 180, "2": 0},
|
||||
"SJM": {"1": 0, "2": 180},
|
||||
"MAN": {"1": 0, "2": 180},
|
||||
"BYK": {"1": 0, "2": 180},
|
||||
"CRD": {"1": 0, "2": 180},
|
||||
"WKG": {"1": 0, "2": 180},
|
||||
"WSD": {"1": 0, "2": 180},
|
||||
"HDR": {"1": 0, "2": 180},
|
||||
"HOW": {"1": 0, "2": 180},
|
||||
"PCM": {"1": 0, "2": 180},
|
||||
"MWL": {"1": 0, "2": 180},
|
||||
"NSH": {"1": 0, "2": 180},
|
||||
"TYN": {"1": 270, "2": 90},
|
||||
"CUL": {"1": 270, "2": 90},
|
||||
"WTL": {"1": 270, "2": 90},
|
||||
"MSN": {"1": 180, "2": 0},
|
||||
"WMN": {"1": 180, "2": 0},
|
||||
"SMR": {"1": 180, "2": 0},
|
||||
"NPK": {"1": 180, "2": 0},
|
||||
"PMV": {"1": 180, "2": 0},
|
||||
"BTN": {"1": 180, "2": 0},
|
||||
"FLE": {"1": 180, "2": 0},
|
||||
"LBN": {"1": 180, "2": 0}
|
||||
}
|
1
station-latlons.json
Normal file
1
station-latlons.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"APT": [55.03577004909116, -1.7111265174504924], "CAL": [55.02800273311085, -1.7036043935197853], "BFT": [55.013882728284536, -1.6781041455925452], "KSP": [55.01438146469378, -1.6652818995828114], "FAW": [55.013643304033884, -1.644878111966044], "WBR": [55.01415869459053, -1.6350085965165628], "RGC": [55.011944179047475, -1.6217731502181474], "SGF": [55.00614726487779, -1.6082661067096817], "ILF": [55.00023693236148, -1.6108770317154648], "WJS": [54.99364830600748, -1.6098926355353478], "JES": [54.982881393953804, -1.6060319576069106], "HAY": [54.97749613672549, -1.6140033882511216], "MTS": [54.973896759913636, -1.6132420712024815], "CEN": [54.96915879926773, -1.6162313682444793], "GHD": [54.96180675090266, -1.6040450471579535], "GST": [54.957727365407564, -1.5886927377600415], "FEL": [54.95315392240506, -1.5720446784266642], "HTH": [54.9515340871179, -1.5555837237871162], "PLW": [54.95254705425207, -1.5420476251169053], "FGT": [54.95263065551402, -1.5420148963164952], "BYW": [54.957372994523446, -1.4860452243342233], "EBO": [54.959403577124995, -1.4612794918514254], "SBN": [54.946410152193415, -1.420438895595007], "SFC": [54.92919863560131, -1.3864468319158785], "MSP": [54.91811279473953, -1.3830167967642568], "SUN": [54.91100033399111, -1.3842235196597212], "PLI": [54.90232769869331, -1.3848277824608908], "UNI": [54.90289486139476, -1.3923841760816358], "MLF": [54.90651021486448, -1.4008007892610852], "PAL": [54.91279641288439, -1.4178747825280822], "SHL": [54.90420705667461, -1.4479615115961655], "SJM": [54.97421054355723, -1.6210669283330124], "MTW": [54.97384152715182, -1.6131521777521154], "MAN": [54.974003328855375, -1.6048376504104354], "BYK": [54.97612749380666, -1.5799477880096582], "CRD": [54.98285607730816, -1.5719087070215574], "WKG": [54.98545678531661, -1.559210279263152], "WSD": [54.98953748564867, -1.5325426692860964], "HDR": [54.99225461830697, -1.515811836724129], "HOW": [54.99569609087364, -1.4945280145610538], "PCM": [54.99947831652944, -1.4751452540962111], "MWL": [55.001529323712575, -1.4659424709549598], "NSH": [55.00840630699378, -1.4485778014421224], "TYN": [55.017274418617184, -1.4282651033617741], "CUL": [55.03511093879241, -1.4365420359749002], "WTL": [55.039919289125834, -1.4429108035106921], "MSN": [55.04230033202882, -1.45816343817505], "WMN": [55.04027612958929, -1.4767711487297275], "SMR": [55.03689435582033, -1.504929962316373], "NPK": [55.03308042684014, -1.5196901723469216], "PMV": [55.02386950238466, -1.5406811448399973], "BTN": [55.013811103383524, -1.567622747731078], "FLE": [55.01011405563572, -1.5786328955533446], "LBN": [55.00873413367473, -1.5914776477978119], "HEB": [54.97561399582167, -1.520598379333017], "JAR": [54.97942548654144, -1.4938358063276782], "BDE": [54.97439112480099, -1.4660181960559018], "SMD": [54.971690254811655, -1.4553294024831502], "TDK": [54.976276978091505, -1.4415400265511848], "CHI": [54.98635066064746, -1.4318251855177448], "SSS": [54.99763337335481, -1.433002848691932]}
|
79
station-positions.json
Normal file
79
station-positions.json
Normal file
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"green": [
|
||||
{"map": [51.80868911743164, 44.98532485961914], "latlon": [55.03577004909116, -1.7111265174504924]},
|
||||
{"map": [71.1099624633789, 44.98532485961914], "latlon": [55.02800273311085, -1.7036043935197853]},
|
||||
{"map": [90.9191665649414, 44.477394104003906], "latlon": [55.013882728284536, -1.6781041455925452]},
|
||||
{"map": [109.71251678466797, 44.98532485961914], "latlon": [55.01438146469378, -1.6652818995828114]},
|
||||
{"map": [129.0137939453125, 44.98532485961914], "latlon": [55.013643304033884, -1.644878111966044]},
|
||||
{"map": [148.822998046875, 44.477394104003906], "latlon": [55.01415869459053, -1.6350085965165628]},
|
||||
{"map": [167.61634826660156, 44.98532485961914], "latlon": [55.011944179047475, -1.6217731502181474]},
|
||||
{"map": [181.33041381835938, 54.12803268432617], "latlon": [55.00606753485614, -1.6082308131224372]},
|
||||
{"map": [187.42555236816406, 65.30245971679688], "latlon": [55.000116635459875, -1.6112647564719849]},
|
||||
{"map": [193.52069091796875, 76.98480987548828], "latlon": [54.99361592984736, -1.6098224294795975]},
|
||||
{"map": [200.63168334960938, 87.65130615234375], "latlon": [54.982874914767116, -1.605933105358663]},
|
||||
{"map": [206.21888732910156, 98.31779479980469], "latlon": [54.97752938692041, -1.6139804105070166]},
|
||||
{"map": [214.8536834716797, 110.5080795288086], "latlon": [54.973881889528585, -1.6132900680751794]},
|
||||
{"map": [221.9646759033203, 124.73007202148438], "latlon": [54.96909836537041, -1.6163186051663263]},
|
||||
{"map": [254.4720916748047, 152.66612243652344], "latlon": [54.969052114768495, -1.6164731976943978]},
|
||||
{"map": [282.91607666015625, 152.66612243652344], "latlon": [54.96188025721301, -1.6041557776219313]},
|
||||
{"map": [309.8362731933594, 152.66612243652344], "latlon": [54.957693981514865, -1.5893810383609956]},
|
||||
{"map": [339.8040466308594, 152.66612243652344], "latlon": [54.95309923554773, -1.5722545639671113]},
|
||||
{"map": [364.9465026855469, 152.158203125], "latlon": [54.95151785157374, -1.555949157488558]},
|
||||
{"map": [396.94598388671875, 173.49119567871094], "latlon": [54.95263065551402, -1.5420148963164952]},
|
||||
{"map": [434.0247497558594, 173.49119567871094], "latlon": [54.957372994523446, -1.4860452243342233]},
|
||||
{"map": [466.53216552734375, 173.49119567871094], "latlon": [54.959403577124995, -1.4612794918514254]},
|
||||
{"map": [517.8328857421875, 182.1259765625], "latlon": [54.946410152193415, -1.420438895595007]},
|
||||
{"map": [518.3408203125, 191.77662658691406], "latlon": [54.92919863560131, -1.3864468319158785]},
|
||||
{"map": [518.3408203125, 203.45896911621094], "latlon": [54.91811279473953, -1.3830167967642568]},
|
||||
{"map": [518.8487548828125, 226.01097106933594], "latlon": [54.91100033399111, -1.3842235196597212]},
|
||||
{"map": [509.7060546875, 239.21710205078125], "latlon": [54.90232769869331, -1.3848277824608908]},
|
||||
{"map": [484.81756591796875, 239.21710205078125], "latlon": [54.90289486139476, -1.3923841760816358]},
|
||||
{"map": [459.4211730957031, 239.21710205078125], "latlon": [54.90651021486448, -1.4008007892610852]},
|
||||
{"map": [434.0247497558594, 239.21710205078125], "latlon": [54.91279641288439, -1.4178747825280822]},
|
||||
{"map": [408.6283264160156, 238.7091827392578], "latlon": [54.90420705667461, -1.4479615115961655]}
|
||||
],
|
||||
"yellow": [
|
||||
{"map": [199.61582946777344, 111.52393341064453], "latlon": [54.97421054355723, -1.6210669283330124]},
|
||||
{"map": [218.4091796875, 111.52393341064453], "latlon": [54.97384152715182, -1.6131521777521154]},
|
||||
{"map": [267.67822265625, 111.52393341064453], "latlon": [54.974003328855375, -1.6048376504104354]},
|
||||
{"map": [294.0904846191406, 112.0318603515625], "latlon": [54.97612749380666, -1.5799477880096582]},
|
||||
{"map": [320.5027770996094, 111.01600646972656], "latlon": [54.98285607730816, -1.5719087070215574]},
|
||||
{"map": [347.5245666503906, 111.01600646972656], "latlon": [54.98545678531661, -1.559210279263152]},
|
||||
{"map": [374.44476318359375, 111.52393341064453], "latlon": [54.98953748564867, -1.5325426692860964]},
|
||||
{"map": [400.8570251464844, 111.52393341064453], "latlon": [54.99225461830697, -1.515811836724129]},
|
||||
{"map": [428.28515625, 111.52393341064453], "latlon": [54.99569609087364, -1.4945280145610538]},
|
||||
{"map": [455.7132873535156, 111.01600646972656], "latlon": [54.99947831652944, -1.4751452540962111]},
|
||||
{"map": [482.63348388671875, 111.52393341064453], "latlon": [55.001529323712575, -1.4659424709549598]},
|
||||
{"map": [508.537841796875, 111.52393341064453], "latlon": [55.00840630699378, -1.4485778014421224]},
|
||||
{"map": [518.6964111328125, 98.82572174072266], "latlon": [55.017274418617184, -1.4282651033617741]},
|
||||
{"map": [518.1884765625, 78.00066375732422], "latlon": [55.03511093879241, -1.4365420359749002]},
|
||||
{"map": [518.6964111328125, 57.175601959228516], "latlon": [55.039919289125834, -1.4429108035106921]},
|
||||
{"map": [485.1731262207031, 44.477394104003906], "latlon": [55.04230033202882, -1.45816343817505]},
|
||||
{"map": [447.0784912109375, 43.96946716308594], "latlon": [55.04027612958929, -1.4767711487297275]},
|
||||
{"map": [408.98388671875, 43.96946716308594], "latlon": [55.03689435582033, -1.504929962316373]},
|
||||
{"map": [370.3813171386719, 44.477394104003906], "latlon": [55.03308042684014, -1.5196901723469216]},
|
||||
{"map": [331.2708435058594, 43.46154022216797], "latlon": [55.02386950238466, -1.5406811448399973]},
|
||||
{"map": [293.1762390136719, 43.96946716308594], "latlon": [55.013811103383524, -1.567622747731078]},
|
||||
{"map": [254.87841796875, 43.96946716308594], "latlon": [55.01011405563572, -1.5786328955533446]},
|
||||
{"map": [216.27586364746094, 43.96946716308594], "latlon": [55.00873413367473, -1.5914776477978119]},
|
||||
{"map": [186.30810546875, 54.12803268432617], "latlon": [55.00614726487779, -1.6082661067096817]},
|
||||
{"map": [191.3873748779297, 64.79452514648438], "latlon": [55.00023693236148, -1.6108770317154648]},
|
||||
{"map": [197.99044799804688, 76.98480987548828], "latlon": [54.99364830600748, -1.6098926355353478]},
|
||||
{"map": [204.59352111816406, 87.14337158203125], "latlon": [54.982881393953804, -1.6060319576069106]},
|
||||
{"map": [210.68865966796875, 97.80986785888672], "latlon": [54.97749613672549, -1.6140033882511216]},
|
||||
{"map": [218.8155059814453, 112.0318603515625], "latlon": [54.973896759913636, -1.6132420712024815]},
|
||||
{"map": [225.926513671875, 124.22213745117188], "latlon": [54.96915879926773, -1.6162313682444793]},
|
||||
{"map": [254.37049865722656, 148.6027069091797], "latlon": [54.96180675090266, -1.6040450471579535]},
|
||||
{"map": [283.830322265625, 147.5868377685547], "latlon": [54.957727365407564, -1.5886927377600415]},
|
||||
{"map": [309.73468017578125, 147.5868377685547], "latlon": [54.95315392240506, -1.5720446784266642]},
|
||||
{"map": [339.70245361328125, 147.5868377685547], "latlon": [54.9515340871179, -1.5555837237871162]},
|
||||
{"map": [365.098876953125, 148.6027069091797], "latlon": [54.95254705425207, -1.5420476251169053]},
|
||||
{"map": [391.10479736328125, 148.0947723388672], "latlon": [54.97561399582167, -1.520598379333017]},
|
||||
{"map": [409.8981628417969, 148.0947723388672], "latlon": [54.97942548654144, -1.4938358063276782]},
|
||||
{"map": [429.7073669433594, 148.0947723388672], "latlon": [54.97439112480099, -1.4660181960559018]},
|
||||
{"map": [449.5165710449219, 147.5868377685547], "latlon": [54.971690254811655, -1.4553294024831502]},
|
||||
{"map": [469.83367919921875, 148.0947723388672], "latlon": [54.976276978091505, -1.4415400265511848]},
|
||||
{"map": [489.1349792480469, 148.0947723388672], "latlon": [54.98635066064746, -1.4318251855177448]},
|
||||
{"map": [509.96002197265625, 148.0947723388672], "latlon": [54.99763337335481, -1.433002848691932]}
|
||||
]
|
||||
}
|
63
stations.json
Normal file
63
stations.json
Normal file
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"APT":"Airport",
|
||||
"BDE":"Bede",
|
||||
"BFT":"Bank Foot",
|
||||
"BTN":"Benton",
|
||||
"BYK":"Byker",
|
||||
"BYW":"Brockley Whins",
|
||||
"CAL":"Callerton Parkway",
|
||||
"CEN":"Central",
|
||||
"CHI":"Chichester",
|
||||
"CRD":"Chillingham Road",
|
||||
"CUL":"Cullercoats",
|
||||
"EBO":"East Boldon",
|
||||
"FAW":"Fawdon",
|
||||
"FEL":"Felling",
|
||||
"FGT":"Fellgate",
|
||||
"FLE":"Four Lane Ends",
|
||||
"GHD":"Gateshead",
|
||||
"GST":"Gateshead Stadium",
|
||||
"HAY":"Haymarket",
|
||||
"HDR":"Hadrian Road",
|
||||
"HEB":"Hebburn",
|
||||
"HOW":"Howdon",
|
||||
"HTH":"Heworth",
|
||||
"ILF":"Ilford Road",
|
||||
"JAR":"Jarrow",
|
||||
"JES":"Jesmond",
|
||||
"KSP":"Kingston Park",
|
||||
"LBN":"Longbenton",
|
||||
"MAN":"Manors",
|
||||
"MLF":"Millfield",
|
||||
"MSN":"Monkseaton",
|
||||
"MSP":"St Peters",
|
||||
"MTS":"Monument N-S",
|
||||
"MTW":"Monument W-E",
|
||||
"MWL":"Meadow Well",
|
||||
"NPK":"Northumberland Park",
|
||||
"NSH":"North Shields",
|
||||
"PAL":"Pallion",
|
||||
"PCM":"Percy Main",
|
||||
"PLI":"Park Lane",
|
||||
"PLW":"Pelaw",
|
||||
"PMV":"Palmersville",
|
||||
"RGC":"Regent Centre",
|
||||
"SBN":"Seaburn",
|
||||
"SFC":"Stadium of Light",
|
||||
"SGF":"South Gosforth",
|
||||
"SHL":"South Hylton",
|
||||
"SJM":"St James",
|
||||
"SMD":"Simonside",
|
||||
"SMR":"Shiremoor",
|
||||
"SSS":"South Shields",
|
||||
"SUN":"Sunderland",
|
||||
"TDK":"Tyne Dock",
|
||||
"TYN":"Tynemouth",
|
||||
"UNI":"University",
|
||||
"WBR":"Wansbeck Road",
|
||||
"WJS":"West Jesmond",
|
||||
"WKG":"Walkergate",
|
||||
"WMN":"West Monkseaton",
|
||||
"WSD":"Wallsend",
|
||||
"WTL":"Whitley Bay"
|
||||
}
|
148
style.css
Normal file
148
style.css
Normal file
|
@ -0,0 +1,148 @@
|
|||
:root {
|
||||
--brand-color: #feb300;
|
||||
--bg-color: white;
|
||||
--text-color: black;
|
||||
--link-color: #551a8b;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sanchez';
|
||||
src: local('Calvert'),
|
||||
url('Sanchez-Regular.otf') format('opentype');
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: black;
|
||||
--text-color: white;
|
||||
--link-color: #6666ff;
|
||||
}
|
||||
|
||||
#map-container {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
input, select {
|
||||
font-family: inherit;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: min(16px,4svw);
|
||||
font-family: Sanchez, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
color: black;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
font-size: 1.5rem;
|
||||
background: var(--brand-color);
|
||||
padding: 0.5em;
|
||||
font-weight: bold;
|
||||
|
||||
& :is(select, input, button) {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
& select {
|
||||
background: white;
|
||||
border: thin solid black;
|
||||
}
|
||||
|
||||
& button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
& a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.platforms {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4em;
|
||||
list-style: none;
|
||||
justify-content: center;
|
||||
|
||||
& table {
|
||||
width: 36em;
|
||||
max-width: 95svw;
|
||||
font-variant: tabular-nums;
|
||||
border-collapse: collapse;
|
||||
|
||||
& caption {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
& .last-checked {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
& tr:not(:first-child) {
|
||||
border-top: thin dashed currentColor;
|
||||
}
|
||||
|
||||
& :is(td,th) {
|
||||
padding: 0.2em 0.5em;
|
||||
|
||||
border: thin dashed currentColor;
|
||||
}
|
||||
|
||||
& td {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
& th {
|
||||
text-align: center;
|
||||
border-bottom-width: medium;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
}
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#map-container {
|
||||
overflow: auto;
|
||||
background: white;
|
||||
}
|
||||
|
||||
#map {
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin: 5em 1em 0 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 37em) {
|
||||
#controls {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
table :is(td,th):is(:nth-child(4),:nth-child(5)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue