Making some progress

This commit is contained in:
Martin Donnelly 2020-04-23 01:06:16 +01:00
parent c41d4b4cec
commit 7d74fcc3e0
18 changed files with 631 additions and 67 deletions

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -7,6 +7,7 @@
<title>Svelte app</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<!--<link rel='stylesheet' href='/global.css'>-->
<link rel='stylesheet' href='/build/bundle.css'>
<script defer src='/build/bundle.js'></script>

View File

@ -5,6 +5,7 @@
// Import the list of routes
import routes from './routes'
import Header from "./components/Header.svelte";
let currentPage;
function conditionsFailed(event) {
// eslint-disable-next-line no-console
@ -15,6 +16,8 @@
function routeLoaded(event) {
// eslint-disable-next-line no-console
console.info('Caught event routeLoaded', event.detail)
currentPage = event.detail.name;
console.log('currentPage', currentPage);
}
// Handles event bubbling up from nested routes
@ -27,9 +30,25 @@
<style lang="scss" global>
@import "./css/custom.scss";
@import "node_modules/spectre.css/src/spectre.scss";
/* @import "./css/custom.scss";*/
/*@import './fonts/fonts.css';
@import './fonts/gotham.css';*/
@import './fonts/fujicons.css';
.up,
.ontime,
.trendUp {
color: #4CAF50 !important;
}
.down,
.delayed,
.trendDown {
color: #F44336 !important;
}
</style>
<Header/>
<Header page={currentPage}/>
<Router {routes} on:conditionsFailed={conditionsFailed} on:routeLoaded={routeLoaded} on:routeEvent={routeEvent} />

View File

@ -1,32 +1,51 @@
<script>
import {link, push, pop, replace, location, querystring} from 'svelte-spa-router';
import active from 'svelte-spa-router/active'
import {pop} from 'svelte-spa-router';
export let page;
let titleText = 'Traintimes';
let currentMode = 1;
$: currentMode = (page === 'Home') ? 0 : 1;
$: titleText = $location;
function goBack() {
console.log('>> Header:goBack');
pop();
}
</script>
<style>
/*#appbar-more-vert {
width: 31px;
height: 31px;
color: #FFF;
}
#appbar-more-vert {
line-height: 31px;
display: inline-block;
cursor: pointer;
text-align: center;
border-radius: 50%;
}*/
</style>
<header id="header">
<div class="mui-appbar mui--appbar-line-height mui--z2">
{#if currentMode === 1}
<div class='mui-col-xs-1 mui-col-md-1 mui--appbar-height'>
<a on:click={goBack} class="">
<i class="fa-3x fa fa-back mui--align-middle" style="color:white;"></i>
</a>
<header class="navbar bg-primary">
</div>
<section class="navbar-section">
{#if currentMode === 1}
<span on:click={goBack} class="" >
<i class="fa-2x fa fa-back" style="color:white;"></i>
</span>
{/if}
<span class="text-bold navbar-brand mx-2 text-uppercase">{titleText}</span>
</section>
<section class="navbar-section">
<a href="/#/settings" class="btn btn-link text-secondary">Settings</a>
<a href="/#/favourites" class="btn btn-link text-secondary">Favourites</a>
</section>
{/if}
<div class='mui-col-xs-11 mui-col-md-11 mui--appbar-height titleBar'>{titleText}</div>
</div>
</header>

View File

@ -0,0 +1,60 @@
<script>
import SettingsInput from "./SettingsInput.svelte";
let startStation;
let destStation;
let deleteEnabled = false;
function deleteItem(){
console.log('>> Delete item');
}
function closeEditor() {
console.log('>> Close item');
}
function saveEditor() {
console.log('>> Save editor');
}
</script>
<style>
</style>
<span>SettingsEditor</span>
<div class="mui-container ">
<div class="mui-row card">
<div class="mui--text-headline">New Route</div>
<div class="mui--align-middle">
<div class='mui-col-xs-12 mui-col-md-5'>
<SettingsInput bind:returnValue={startStation} label="Departure Station" name="startStation"/>
</div>
<div class='mui-col-xs-12 mui-col-md-2'>
<i class="fa fa-thick-arrow fa-2x mui--align-middle">
</div>
<div class='mui-col-xs-12 mui-col-md-5'>
<SettingsInput bind:returnValue={destStation} label="Destination Station" name="destStation"/>
</div>
</div>
<div class="my text-right">
<button class="btn btn-danger btn-sm" id="delete" type="button" disabled={deleteEnabled} on:click={deleteItem}>
Delete
</button>
<button class="btn btn-sm" type="button" on:click={closeEditor}>
Close
</button>
<button class="btn btn-primary btn-sm" id="save" type="button" on:click={saveEditor}>
Save
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,52 @@
<script>
import {debounce} from '../libs/utils';
import {onMount} from 'svelte';
import {searchStation} from '../libs/stations';
export let returnValue;
export let value;
export let name;
export let label;
let debouncedDoSearch;
let searchResults = [];
// $: visible = (searchResults.length > 0) ?
onMount(async () => {
debouncedDoSearch = debounce(doSearch, 500);
});
function doSearch() {
returnValue = '';
if (value.length >= 2)
searchResults = searchStation(value);
}
function selectItem(e) {
let [id, name] = e.target.dataset.content.split(',');
returnValue = id;
value = name;
searchResults = [];
}
</script>
<style>
</style>
<span class="mui-dropdown--right">
<label for={name}>{label}</label>
<input {name} on:keyup={debouncedDoSearch} bind:value />
{#if searchResults.length > 0}
<ul class="mui-dropdown__menu mui--is-open">
{#each searchResults as item, index}
<li><div on:click={selectItem} data-content="{item}">{item[1]} ({item[0]})</div></li>
{/each}
</ul>
{/if}
</span>

View File

@ -0,0 +1,12 @@
<script>
</script>
<style>
* {
background: #f55a4e;
padding: 3px;
}
</style>
<span>SettingsList</span>

View File

@ -1,17 +1,90 @@
<script>
import {onMount, onDestroy} from 'svelte';
import axios from 'axios';
import {push} from 'svelte-spa-router';
import reducer from '../libs/reducer';
export let fromStation;
export let destStation;
let list = []
let otherDetails = {};
let baseUrl = 'http://localhost:8100';
let doUpdate = true;
onMount(async () => {
await fetchData();
});
async function fetchData() {
if (doUpdate === true) {
const routeUrl = `/gettrains?from=${fromStation}&to=${destStation}`
const url = baseUrl.concat(routeUrl)
await axios.get(url)
.then((d) => {
list = reducer.reduceTrainTimetable(d.data)
otherDetails = reducer.reduceOtherDetails(d.data)
})
}
}
function viewService(e) {
push(`/service/${e}`);
}
</script>
<style>
* {
background: #f55a4e;
padding: 3px;
}
</style>
<div>
<span>TimetableList</span>
{fromStation} to {destStation}
<section>
{#if otherDetails.nrMessagesExist === true}
<div class="mui--bg-danger mui--text-white nrccAlert" style="padding:2px;">
<ul>
{#each otherDetails.nrMessages as item}
<li><i class="fa fa-info mui--align-middle"></i> {item.msg}
{#if item.link}
<a href={item.link}>{item.linkText}</a>
{/if}
</li>
{/each}
</ul>
</div>
{/if}
{#if list.length > 0}
{#each list as item}
<div class="mui-row card mui--align-bottom">
<div class="mui-col-xs-5 mui-col-md-4 mui--align-middle">
<span on:click={viewService(item.serviceIdUrlSafe)}>{item.location}</span>
<span class="mui--text-accent">{item.carriageCount}</span>
<div>
{#if item.via}
<em class="mui--text-accent via">{item.via}</em>
{/if}
</div>
</div>
<div class="mui-col-xs-2 mui-col-md-3 mui--text-center mui--align-middle time">{item.time}</div>
{#if item.isCancelled}
<div class="mui-col-xs-5 mui-col-md-5 mui--text-center mui--align-middle delayed"><i class="fa fa-alert fa-1x mui--align-middle"></i>{item.cancel}</div>
{:else}
<div class="mui-col-xs-3 mui-col-md-3 mui--text-center mui--align-middle {item.statusMode}">{item.status}</div>
<div class="mui-col-xs-2 mui-col-md-2 mui--text-center mui--align-middle">{item.platform}</div>
{/if}
</div>
{/each}
{/if}
</section>
</div>

View File

@ -18,6 +18,7 @@
let status;
let timetablePath;
let interval = 0;
let due = 0;
$: {
status = (trainData.eta === 'On time') ? 'ontime' : 'delayed';
@ -26,33 +27,49 @@
}
onMount(async () => {
if( LocalStorage.exists(`${startStation}${destStation}`)) {
if (LocalStorage.exists(`${startStation}${destStation}`)) {
const fromLS = JSON.parse(LocalStorage.load(`${startStation}${destStation}`));
trainData = {...trainData, ...fromLS};
trainData = {...trainData, ...fromLS.trainData};
due = fromLS.due;
startStationName = fromLS.startStationName;
destStationName = fromLS.destStationName;
url = fromLS.url;
} else {
startStationName = findStation(startStation);
destStationName = findStation(destStation);
url = `${baseUrl}/getnexttraintimes?from=${startStation}&to=${destStation}`;
}
const now = new Date().getTime();
if (now > due) {
updateTrain();
} else {
interval = 0
interval = setTimeout(updateTrain, due - now);
}
startStationName = findStation(startStation);
destStationName = findStation(destStation);
url = `${baseUrl}/getnexttraintimes?from=${startStation}&to=${destStation}`;
updateTrain();
});
onDestroy(async () => {
clearInterval(interval);
LocalStorage.save(`${startStation}${destStation}`, JSON.stringify(trainData));
const store = {due, trainData, startStationName, destStationName, url};
LocalStorage.save(`${startStation}${destStation}`, JSON.stringify(store));
});
function onClick() {
console.log('Onclick', timetablePath);
push(timetablePath);
}
async function updateTrain() {
console.log(`Update: ${startStation} / ${destStation}`)
const now = new Date()
const hours = now.getHours()
const limit = (hours < 6) ? 3600000 : 95000
const mod = limit - (now.getTime() % limit)
due = now.getTime() + mod;
await getTrain()
clearTimeout(interval)
interval = 0
@ -76,14 +93,29 @@
margin: 6px 0;
vertical-align: middle;
}
.TRcard {
position: relative;
background-color: #fff;
min-height: 48px;
margin: 0.5rem 8px;
border-bottom-color: #666666;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
vertical-align: middle;
padding: 0.5rem 0;
border-radius: 0.1rem;
}
</style>
<div class="mui-row card">
<div class='mui-col-xs-7 mui-col-md-7 entry'>
<div class="columns TRcard">
<div class='column col-7 entry'>
<div>{startStationName}</div>
<div>{destStationName}</div>
</div>
<div class='mui-col-xs-5 mui-col-md-5 mui--text-right'>
<span class="mui-btn mui-btn--flat time {status}" on:click={onClick}>{displayTime}</span>
<div class='column col-5 text-right entry'>
<span class="btn {status}" on:click={onClick}>{displayTime}</span>
</div>
</div>

View File

@ -1,12 +1,64 @@
<script>
import {onMount, onDestroy} from 'svelte';
import {minuteFloor, LocalStorage} from '../libs/utils'
import axios from 'axios';
import reducer from '../libs/reducer';
export let serviceId;
let list = [];
let baseUrl = 'http://localhost:8100';
let doUpdate = true;
let serviceInterval;
onMount(async () => {
fetchServiceData();
serviceInterval = setInterval(() => {
console.log('Do service update')
fetchServiceData()
}, 120000);
});
onDestroy(async () => {
clearInterval(serviceInterval);
});
function fetchServiceData() {
console.log('>> TrainService: fetchServiceData');
// http://localhost:8100/getservice?serviceid=TDKWvQdeuviRyNYP7lk7gA
if (doUpdate === true) {
const routeUrl = `/getservice?serviceid=${serviceId}`
const url = baseUrl.concat(routeUrl)
axios.get(url)
.then((d) => {
console.log(d);
list = reducer.reduceTrainService(d.data);
})
}
}
</script>
<style>
* {
background: #f55a4e;
padding: 3px;
}
</style>
<span>TrainService</span>
<section>
<div class="mui-row card mui--align-bottom">
<div class="mui-col-xs-3 mui-col-md-3 mui--align-middle">Station</div>
<div class="mui-col-xs-3 mui-col-md-3 mui--align-middle">Due</div>
<div class="mui-col-xs-3 mui-col-md-3 mui--align-middle">Estimated</div>
<div class="mui-col-xs-3 mui-col-md-3 mui--align-middle">Arrived</div>
</div>
{#if list.length > 0}
{#each list as item}
<div class="mui-row card mui--align-bottom {item.classCancel}">
<div class="mui-col-xs-3 mui-col-md-3 mui--align-middle">{item.locationName}</div>
<div class="mui-col-xs-3 mui-col-md-3 mui--align-middle">{item.st}</div>
<div class="mui-col-xs-3 mui-col-md-3 mui--align-middle {item.etMode}">{item.et}</div>
<div class="mui-col-xs-3 mui-col-md-3 mui--align-middle {item.atMode}">{item.at}</div>
</div>
{/each}
{/if}
</section>

132
src/libs/reducer.js Normal file
View File

@ -0,0 +1,132 @@
/**
* Created by WebStorm.
* User: martin
* Date: 20/04/2020
* Time: 12:01
*/
const reducer = {
reduceOtherDetails (data) {
const nrMessages = [];
// we have national rail messages so put a box at the top
// <div class="mui--bg-danger .mui--text-white" style="height:10px;"></div>
let index = 0;
const anchorRegex = /<\s*[aA].*?href\s*=\s*(?:"|')(.*?)(?:"|')[^>]*>(.*?)<\s*?\/\s*?[aA]\s*?>/;
if (typeof data.nrccMessages === 'object' && data.nrccMessages !== null)
for (const item of data.nrccMessages) {
const newObj = { 'index': 0, 'msg': '', 'link': null, 'linkText': '' };
let msg = item.value.replace(' ">', '">').replace('</A>', '</a>').replace('<A ', '<a ').replace(/<\/*[pP]>/gi, '');
const anchor = anchorRegex.exec(item.value);
msg = msg.replace(anchorRegex, '');
newObj.index = index;
newObj.msg = msg;
if (anchor !== null) {
newObj.link = anchor[1];
newObj.linkText = anchor[2];
}
nrMessages.push(newObj);
index++;
}
return { nrMessages, 'nrMessagesExist': nrMessages.length > 0 };
},
reduceTrainTimetable (data) {
const services = [];
let ws = '';
const symbol = ['💠', '🚉'];
if (typeof data === 'object' && data !== null) {
console.log('>> reduceTrainService');
console.log(data);
if (typeof data.trainServices === 'object' && data.trainServices !== null)
for (const item of data.trainServices) {
// console.log(item)
const dest = item.destination[0];
const via = dest.via !== null ? dest.via : '';
const platform = item.platform !== null ? item.platform : `${symbol[0]}`;
// 🚉 💠
// const time = item.sta !== null ? item.sta : `<em class="mui--text-accent-secondary">D</em> ${item.std}`
const time = item.sta !== null ? item.sta : `D ${item.std}`;
const isDeparture = item.sta === null;
const status = item.eta !== null ? item.eta : item.etd;
const trainLength = item.length;
const carriageCount = (trainLength > 0) ? ` (${trainLength} 🚃) ` : '';
const statusMode = (status.toLowerCase() === 'on time') ? 'ontime' : 'delayed';
const delayReason = (item.delayReason !== null) ? item.delayReason : '';
const cancelReason = (item.cancelReason !== null) ? item.cancelReason : 'No reason given 🤷';
const serviceIdUrlSafe = item.serviceIdUrlSafe;
services.push({ 'location': dest.locationName, 'time': time, 'status': status, 'platform': platform, 'cancel': cancelReason, 'type': 'train', 'delay': delayReason, 'carriageCount': carriageCount, 'via': via, 'statusMode': statusMode, 'isCancelled': item.isCancelled, 'isDeparture': isDeparture, 'serviceIdUrlSafe': serviceIdUrlSafe });
if (!item.isCancelled)
ws = `${ws}<tr><td data-id="${item.serviceIdUrlSafe}" class="station">${dest.locationName}${carriageCount}${via}</td>
<td class="mui--text-center time">${time}</td>
<td class="mui--text-center ${statusMode}">${status}</td>
<td class="mui--text-center">${platform}</td>
</tr>${delayReason}`;
else
ws = `${ws}<tr><td>${dest.locationName} ${via}</td><td>${time}</td>
<td colspan="2" class="delayed"> ${cancelReason}</td></tr>`;
}
if (typeof data.busServices === 'object' && data.busServices !== null)
for (const item of data.busServices) {
const dest = item.destination[0];
const via = dest.via !== null ? dest.via : '';
const platform = item.platform !== null ? item.platform : '';
const time = item.sta !== null ? item.sta : `D ${item.std}`;
const status = item.eta !== null ? item.eta : item.etd;
services.push({ 'location': dest.locationName, 'time': time, 'status': status, 'platform': platform, 'cancel': item.cancelReason, 'type': 'bus', 'via': via });
}
}
console.log(services);
return services;
},
reduceTrainService (d) {
let callingpoints = [];
const departureTime = d.sta || d.std;
const departureStatus = d.eta || d.etd;
const currentLocation = { 'locationName': d.locationName, 'crs': d.crs, 'st': d.sta, 'et': d.eta, 'at': d.ata, 'isCancelled': d.isCancelled, 'length': d.length, 'detachFront': d.detachFront, 'adhocAlerts': d.adhocAlerts };
if (d.previousCallingPoints !== null)
callingpoints = callingpoints.concat(d.previousCallingPoints[0].callingPoint);
callingpoints.push(currentLocation);
if (d.subsequentCallingPoints !== null)
callingpoints = callingpoints.concat(d.subsequentCallingPoints[0].callingPoint);
callingpoints = callingpoints.map((item) => {
// console.log(item)
item.et = (item.et === null) ? '' : item.et;
item.at = (item.at === null) ? '' : item.at;
item.etMode = (item.et.toLowerCase() === 'on time') ? 'ontime' : 'delayed';
item.atMode = (item.at.toLowerCase() === 'on time') ? 'ontime' : 'delayed';
item.delayReason = (item.delayReason !== null) ? item.delayReason : '';
item.cancelReason = (item.cancelReason !== null) ? item.cancelReason : 'No reason given 🤷';
item.classCancel = (item.isCancelled) ? 'cancelledRow' : '';
if (item.st === null && (item.et === null || item.et === '')) {
item.st = `D ${departureTime}`;
item.et = departureStatus;
}
return item;
});
return callingpoints;
}
};
module.exports = reducer;

File diff suppressed because one or more lines are too long

View File

@ -117,4 +117,73 @@ else
}
};
module.exports = { partOfDay, toHour, hourFloor, distance, maybePluralize, minuteFloor, LocalStorage };
/**
*
* @param fn
* @param time
* @returns {Function}
* @private
*/
function debounce(fn, time) {
let timeout;
return function (...args) { // <-- not an arrow function
const functionCall = () => fn.apply(this, args);
clearTimeout(timeout);
timeout = setTimeout(functionCall, time);
};
}
/**
*
* @param callback
* @param limit
* @returns {Function}
* @private
*/
function throttle (callback, limit) {
var wait = false;
return function () {
if (!wait) {
callback.apply(null, arguments);
wait = true;
setTimeout(function () {
wait = false;
}, limit);
}
};
}
/**
*
* @param func
* @returns {function(): *}
* @private
*/
function once(func) {
var alreadyCalled = false;
var result;
return function() {
if (!alreadyCalled) {
result = func.apply(this, arguments);
alreadyCalled = true;
}
return result;
};
};
function isEmpty(obj) {
for(const key in obj)
if(obj.hasOwnProperty(key)) return false;
return true;
}
module.exports = { partOfDay, toHour, hourFloor, distance, maybePluralize, minuteFloor, debounce, throttle, once, isEmpty, LocalStorage };

View File

@ -3,7 +3,7 @@
</script>
<div class="mui-container">
<div class="container">
<TrainRoute destStation="glq" startStation="dbe"/>
<TrainRoute destStation="dbe" startStation="glq"/>
<TrainRoute destStation="glc" startStation="ptk"/>

View File

@ -1,12 +1,17 @@
<script>
import TrainService from "../components/TrainService.svelte";
export let params = {};
let serviceId = params.serviceId;
</script>
<style>
* {
background: #f55a4e;
padding: 3px;
}
</style>
<span>Service</span>
<div class="mui--appbar-height"></div>
<div class="mui-container">
<TrainService {serviceId}/>
</div>

16
src/pages/Settings.svelte Normal file
View File

@ -0,0 +1,16 @@
<script>
import SettingsEditor from "../components/SettingsEditor.svelte";
import SettingsList from "../components/SettingsList.svelte";
</script>
<style>
</style>
<div class="container">
<span>Settings</span>
<SettingsEditor/>
<SettingsList/>
</div>

View File

@ -7,29 +7,20 @@
let fromStationName;
let destStationName;
let fromStation;
let destStation;
let fromStation = params.fromStation;
let destStation= params.destStation;
onMount(async () => {
console.log('>> Timetable:', params);
fromStation = params.fromStation;
destStation = params.destStation;
fromStationName = findStation(fromStation);
destStationName = findStation(destStation);
});
onDestroy(async () => {
console.log('>> onDestroy Timetable')
});
</script>
<style>
</style>
<div class="mui--appbar-height"></div>
<div class="mui-container">
<div class="container">
<div>
<div class="mui--text-center mui--text-accent">{fromStationName} TO {destStationName}</div>
<TimetableList {fromStation} {destStation}/>

View File

@ -10,12 +10,14 @@
import Home from './pages/Home.svelte';
import Service from './pages/Service.svelte';
import Timetable from './pages/Timetable.svelte';
import Settings from './pages/Settings.svelte';
import NotFound from './pages/NotFound.svelte';
const routes = new Map();
routes.set('/', Home);
routes.set('/timetable/:fromStation/:destStation', Timetable);
routes.set('/service/:serviceId', Service);
routes.set('/settings', Settings);
routes.set('*', NotFound);
export default routes;